add business-onlinepayment-currency config setting, RT#13904
[freeside.git] / FS / FS / cust_main / Billing_Realtime.pm
1 package FS::cust_main::Billing_Realtime;
2
3 use strict;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
6 use Data::Dumper;
7 use Business::CreditCard 0.28;
8 use FS::UID qw( dbh );
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
11 use FS::payby;
12 use FS::cust_pay;
13 use FS::cust_pay_pending;
14 use FS::cust_refund;
15 use FS::banned_pay;
16
17 $realtime_bop_decline_quiet = 0;
18
19 # 1 is mostly method/subroutine entry and options
20 # 2 traces progress of some operations
21 # 3 is even more information including possibly sensitive data
22 $DEBUG = 0;
23 $me = '[FS::cust_main::Billing_Realtime]';
24
25 install_callback FS::UID sub { 
26   $conf = new FS::Conf;
27   #yes, need it for stuff below (prolly should be cached)
28 };
29
30 =head1 NAME
31
32 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
33
34 =head1 SYNOPSIS
35
36 =head1 DESCRIPTION
37
38 These methods are available on FS::cust_main objects.
39
40 =head1 METHODS
41
42 =over 4
43
44 =item realtime_collect [ OPTION => VALUE ... ]
45
46 Attempt to collect the customer's current balance with a realtime credit 
47 card, electronic check, or phone bill transaction (see realtime_bop() below).
48
49 Returns the result of realtime_bop(): nothing, an error message, or a 
50 hashref of state information for a third-party transaction.
51
52 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
53
54 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>.  If none is specified
55 then it is deduced from the customer record.
56
57 If no I<amount> is specified, then the customer balance is used.
58
59 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
60 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
61 if set, will override the value from the customer record.
62
63 I<description> is a free-text field passed to the gateway.  It defaults to
64 the value defined by the business-onlinepayment-description configuration
65 option, or "Internet services" if that is unset.
66
67 If an I<invnum> is specified, this payment (if successful) is applied to the
68 specified invoice.
69
70 I<apply> will automatically apply a resulting payment.
71
72 I<quiet> can be set true to suppress email decline notices.
73
74 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
75 resulting paynum, if any.
76
77 I<payunique> is a unique identifier for this payment.
78
79 I<session_id> is a session identifier associated with this payment.
80
81 I<depend_jobnum> allows payment capture to unlock export jobs
82
83 =cut
84
85 sub realtime_collect {
86   my( $self, %options ) = @_;
87
88   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
89
90   if ( $DEBUG ) {
91     warn "$me realtime_collect:\n";
92     warn "  $_ => $options{$_}\n" foreach keys %options;
93   }
94
95   $options{amount} = $self->balance unless exists( $options{amount} );
96   $options{method} = FS::payby->payby2bop($self->payby)
97     unless exists( $options{method} );
98
99   return $self->realtime_bop({%options});
100
101 }
102
103 =item realtime_bop { [ ARG => VALUE ... ] }
104
105 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
106 via a Business::OnlinePayment realtime gateway.  See
107 L<http://420.am/business-onlinepayment> for supported gateways.
108
109 Required arguments in the hashref are I<method>, and I<amount>
110
111 Available methods are: I<CC>, I<ECHECK> and I<LEC>
112
113 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
114
115 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
116 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
117 if set, will override the value from the customer record.
118
119 I<description> is a free-text field passed to the gateway.  It defaults to
120 the value defined by the business-onlinepayment-description configuration
121 option, or "Internet services" if that is unset.
122
123 If an I<invnum> is specified, this payment (if successful) is applied to the
124 specified invoice.  If the customer has exactly one open invoice, that 
125 invoice number will be assumed.  If you don't specify an I<invnum> you might 
126 want to call the B<apply_payments> method or set the I<apply> option.
127
128 I<apply> can be set to true to apply a resulting payment.
129
130 I<quiet> can be set true to surpress email decline notices.
131
132 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
133 resulting paynum, if any.
134
135 I<payunique> is a unique identifier for this payment.
136
137 I<session_id> is a session identifier associated with this payment.
138
139 I<depend_jobnum> allows payment capture to unlock export jobs
140
141 I<discount_term> attempts to take a discount by prepaying for discount_term
142
143 A direct (Business::OnlinePayment) transaction will return nothing on success,
144 or an error message on failure.
145
146 A third-party transaction will return a hashref containing:
147
148 - popup_url: the URL to which a browser should be redirected to complete 
149   the transaction.
150 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
151 - reference: a reference ID for the transaction, to show the customer.
152
153 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
154
155 =cut
156
157 # some helper routines
158 sub _bop_recurring_billing {
159   my( $self, %opt ) = @_;
160
161   my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
162
163   if ( defined($method) && $method eq 'transaction_is_recur' ) {
164
165     return 1 if $opt{'trans_is_recur'};
166
167   } else {
168
169     my %hash = ( 'custnum' => $self->custnum,
170                  'payby'   => 'CARD',
171                );
172
173     return 1 
174       if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
175       || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
176                                                                $opt{'payinfo'} )
177                              } );
178
179   }
180
181   return 0;
182
183 }
184
185 sub _payment_gateway {
186   my ($self, $options) = @_;
187
188   if ( $options->{'selfservice'} ) {
189     my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
190     if ( $gatewaynum ) {
191       return $options->{payment_gateway} ||= 
192           qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
193     }
194   }
195
196   if ( $options->{'fake_gatewaynum'} ) {
197         $options->{payment_gateway} =
198             qsearchs('payment_gateway',
199                       { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
200                     );
201   }
202
203   $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
204     unless exists($options->{payment_gateway});
205
206   $options->{payment_gateway};
207 }
208
209 sub _bop_auth {
210   my ($self, $options) = @_;
211
212   (
213     'login'    => $options->{payment_gateway}->gateway_username,
214     'password' => $options->{payment_gateway}->gateway_password,
215   );
216 }
217
218 sub _bop_options {
219   my ($self, $options) = @_;
220
221   $options->{payment_gateway}->gatewaynum
222     ? $options->{payment_gateway}->options
223     : @{ $options->{payment_gateway}->get('options') };
224
225 }
226
227 sub _bop_defaults {
228   my ($self, $options) = @_;
229
230   unless ( $options->{'description'} ) {
231     if ( $conf->exists('business-onlinepayment-description') ) {
232       my $dtempl = $conf->config('business-onlinepayment-description');
233
234       my $agent = $self->agent->agent;
235       #$pkgs... not here
236       $options->{'description'} = eval qq("$dtempl");
237     } else {
238       $options->{'description'} = 'Internet services';
239     }
240   }
241
242   $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
243
244   # Default invoice number if the customer has exactly one open invoice.
245   if( ! $options->{'invnum'} ) {
246     $options->{'invnum'} = '';
247     my @open = $self->open_cust_bill;
248     $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
249   }
250
251   $options->{payname} = $self->payname unless exists( $options->{payname} );
252 }
253
254 sub _bop_content {
255   my ($self, $options) = @_;
256   my %content = ();
257
258   my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
259   $content{customer_ip} = $payip if length($payip);
260
261   $content{invoice_number} = $options->{'invnum'}
262     if exists($options->{'invnum'}) && length($options->{'invnum'});
263
264   $content{email_customer} = 
265     (    $conf->exists('business-onlinepayment-email_customer')
266       || $conf->exists('business-onlinepayment-email-override') );
267       
268   my ($payname, $payfirst, $paylast);
269   if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
270     ($payname = $options->{payname}) =~
271       /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
272       or return "Illegal payname $payname";
273     ($payfirst, $paylast) = ($1, $2);
274   } else {
275     $payfirst = $self->getfield('first');
276     $paylast = $self->getfield('last');
277     $payname = "$payfirst $paylast";
278   }
279
280   $content{last_name} = $paylast;
281   $content{first_name} = $payfirst;
282
283   $content{name} = $payname;
284
285   $content{address} = exists($options->{'address1'})
286                         ? $options->{'address1'}
287                         : $self->address1;
288   my $address2 = exists($options->{'address2'})
289                    ? $options->{'address2'}
290                    : $self->address2;
291   $content{address} .= ", ". $address2 if length($address2);
292
293   $content{city} = exists($options->{city})
294                      ? $options->{city}
295                      : $self->city;
296   $content{state} = exists($options->{state})
297                       ? $options->{state}
298                       : $self->state;
299   $content{zip} = exists($options->{zip})
300                     ? $options->{'zip'}
301                     : $self->zip;
302   $content{country} = exists($options->{country})
303                         ? $options->{country}
304                         : $self->country;
305
306   $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
307   $content{phone} = $self->daytime || $self->night;
308
309   my $currency =    $conf->exists('business-onlinepayment-currency')
310                  && $conf->config('business-onlinepayment-currency');
311   $content{currency} = $currency if $currency;
312
313   \%content;
314 }
315
316 my %bop_method2payby = (
317   'CC'     => 'CARD',
318   'ECHECK' => 'CHEK',
319   'LEC'    => 'LECB',
320 );
321
322 sub realtime_bop {
323   my $self = shift;
324
325   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
326  
327   my %options = ();
328   if (ref($_[0]) eq 'HASH') {
329     %options = %{$_[0]};
330   } else {
331     my ( $method, $amount ) = ( shift, shift );
332     %options = @_;
333     $options{method} = $method;
334     $options{amount} = $amount;
335   }
336
337
338   ### 
339   # optional credit card surcharge
340   ###
341
342   my $cc_surcharge = 0;
343   my $cc_surcharge_pct = 0;
344   $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage') 
345     if $conf->config('credit-card-surcharge-percentage');
346   
347   # always add cc surcharge if called from event 
348   if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
349       $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
350       $options{'amount'} += $cc_surcharge;
351       $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
352   }
353   elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a 
354                                  # payment screen), so consider the given 
355                                  # amount as post-surcharge
356     $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
357   }
358   
359   $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
360   $options{'cc_surcharge'} = $cc_surcharge;
361
362
363   if ( $DEBUG ) {
364     warn "$me realtime_bop (new): $options{method} $options{amount}\n";
365     warn " cc_surcharge = $cc_surcharge\n";
366     warn "  $_ => $options{$_}\n" foreach keys %options;
367   }
368
369   return $self->fake_bop(\%options) if $options{'fake'};
370
371   $self->_bop_defaults(\%options);
372
373   ###
374   # set trans_is_recur based on invnum if there is one
375   ###
376
377   my $trans_is_recur = 0;
378   if ( $options{'invnum'} ) {
379
380     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
381     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
382
383     my @part_pkg =
384       map  { $_->part_pkg }
385       grep { $_ }
386       map  { $_->cust_pkg }
387       $cust_bill->cust_bill_pkg;
388
389     $trans_is_recur = 1
390       if grep { $_->freq ne '0' } @part_pkg;
391
392   }
393
394   ###
395   # select a gateway
396   ###
397
398   my $payment_gateway =  $self->_payment_gateway( \%options );
399   my $namespace = $payment_gateway->gateway_namespace;
400
401   eval "use $namespace";  
402   die $@ if $@;
403
404   ###
405   # check for banned credit card/ACH
406   ###
407
408   my $ban = FS::banned_pay->ban_search(
409     'payby'   => $bop_method2payby{$options{method}},
410     'payinfo' => $options{payinfo},
411   );
412   return "Banned credit card" if $ban && $ban->bantype ne 'warn';
413
414   ###
415   # massage data
416   ###
417
418   my $bop_content = $self->_bop_content(\%options);
419   return $bop_content unless ref($bop_content);
420
421   my @invoicing_list = $self->invoicing_list_emailonly;
422   if ( $conf->exists('emailinvoiceautoalways')
423        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
424        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
425     push @invoicing_list, $self->all_emails;
426   }
427
428   my $email = ($conf->exists('business-onlinepayment-email-override'))
429               ? $conf->config('business-onlinepayment-email-override')
430               : $invoicing_list[0];
431
432   my $paydate = '';
433   my %content = ();
434
435   if ( $namespace eq 'Business::OnlinePayment' ) {
436
437     if ( $options{method} eq 'CC' ) {
438
439       $content{card_number} = $options{payinfo};
440       $paydate = exists($options{'paydate'})
441                       ? $options{'paydate'}
442                       : $self->paydate;
443       $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
444       $content{expiration} = "$2/$1";
445
446       my $paycvv = exists($options{'paycvv'})
447                      ? $options{'paycvv'}
448                      : $self->paycvv;
449       $content{cvv2} = $paycvv
450         if length($paycvv);
451
452       my $paystart_month = exists($options{'paystart_month'})
453                              ? $options{'paystart_month'}
454                              : $self->paystart_month;
455
456       my $paystart_year  = exists($options{'paystart_year'})
457                              ? $options{'paystart_year'}
458                              : $self->paystart_year;
459
460       $content{card_start} = "$paystart_month/$paystart_year"
461         if $paystart_month && $paystart_year;
462
463       my $payissue       = exists($options{'payissue'})
464                              ? $options{'payissue'}
465                              : $self->payissue;
466       $content{issue_number} = $payissue if $payissue;
467
468       if ( $self->_bop_recurring_billing(
469              'payinfo'        => $options{'payinfo'},
470              'trans_is_recur' => $trans_is_recur,
471            )
472          )
473       {
474         $content{recurring_billing} = 'YES';
475         $content{acct_code} = 'rebill'
476           if $conf->exists('credit_card-recurring_billing_acct_code');
477       }
478
479     } elsif ( $options{method} eq 'ECHECK' ){
480
481       ( $content{account_number}, $content{routing_code} ) =
482         split('@', $options{payinfo});
483       $content{bank_name} = $options{payname};
484       $content{bank_state} = exists($options{'paystate'})
485                                ? $options{'paystate'}
486                                : $self->getfield('paystate');
487       $content{account_type}=
488         (exists($options{'paytype'}) && $options{'paytype'})
489           ? uc($options{'paytype'})
490           : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
491       $content{account_name} = $self->getfield('first'). ' '.
492                                $self->getfield('last');
493
494       $content{customer_org} = $self->company ? 'B' : 'I';
495       $content{state_id}       = exists($options{'stateid'})
496                                    ? $options{'stateid'}
497                                    : $self->getfield('stateid');
498       $content{state_id_state} = exists($options{'stateid_state'})
499                                    ? $options{'stateid_state'}
500                                    : $self->getfield('stateid_state');
501       $content{customer_ssn} = exists($options{'ss'})
502                                  ? $options{'ss'}
503                                  : $self->ss;
504
505     } elsif ( $options{method} eq 'LEC' ) {
506       $content{phone} = $options{payinfo};
507     } else {
508       die "unknown method ". $options{method};
509     }
510
511   } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
512     #move along
513   } else {
514     die "unknown namespace $namespace";
515   }
516
517   ###
518   # run transaction(s)
519   ###
520
521   my $balance = exists( $options{'balance'} )
522                   ? $options{'balance'}
523                   : $self->balance;
524
525   $self->select_for_update; #mutex ... just until we get our pending record in
526
527   #the checks here are intended to catch concurrent payments
528   #double-form-submission prevention is taken care of in cust_pay_pending::check
529
530   #check the balance
531   return "The customer's balance has changed; $options{method} transaction aborted."
532     if $self->balance < $balance;
533
534   #also check and make sure there aren't *other* pending payments for this cust
535
536   my @pending = qsearch('cust_pay_pending', {
537     'custnum' => $self->custnum,
538     'status'  => { op=>'!=', value=>'done' } 
539   });
540
541   #for third-party payments only, remove pending payments if they're in the 
542   #'thirdparty' (waiting for customer action) state.
543   if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
544     foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
545       my $error = $_->delete;
546       warn "error deleting unfinished third-party payment ".
547           $_->paypendingnum . ": $error\n"
548         if $error;
549     }
550     @pending = grep { $_->status ne 'thirdparty' } @pending;
551   }
552
553   return "A payment is already being processed for this customer (".
554          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
555          "); $options{method} transaction aborted."
556     if scalar(@pending);
557
558   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
559
560   my $cust_pay_pending = new FS::cust_pay_pending {
561     'custnum'           => $self->custnum,
562     'paid'              => $options{amount},
563     '_date'             => '',
564     'payby'             => $bop_method2payby{$options{method}},
565     'payinfo'           => $options{payinfo},
566     'paydate'           => $paydate,
567     'recurring_billing' => $content{recurring_billing},
568     'pkgnum'            => $options{'pkgnum'},
569     'status'            => 'new',
570     'gatewaynum'        => $payment_gateway->gatewaynum || '',
571     'session_id'        => $options{session_id} || '',
572     'jobnum'            => $options{depend_jobnum} || '',
573   };
574   $cust_pay_pending->payunique( $options{payunique} )
575     if defined($options{payunique}) && length($options{payunique});
576   my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
577   return $cpp_new_err if $cpp_new_err;
578
579   my( $action1, $action2 ) =
580     split( /\s*\,\s*/, $payment_gateway->gateway_action );
581
582   my $transaction = new $namespace( $payment_gateway->gateway_module,
583                                     $self->_bop_options(\%options),
584                                   );
585
586   $transaction->content(
587     'type'           => $options{method},
588     $self->_bop_auth(\%options),          
589     'action'         => $action1,
590     'description'    => $options{'description'},
591     'amount'         => $options{amount},
592     #'invoice_number' => $options{'invnum'},
593     'customer_id'    => $self->custnum,
594     %$bop_content,
595     'reference'      => $cust_pay_pending->paypendingnum, #for now
596     'callback_url'   => $payment_gateway->gateway_callback_url,
597     'email'          => $email,
598     %content, #after
599   );
600
601   $cust_pay_pending->status('pending');
602   my $cpp_pending_err = $cust_pay_pending->replace;
603   return $cpp_pending_err if $cpp_pending_err;
604
605   #config?
606   my $BOP_TESTING = 0;
607   my $BOP_TESTING_SUCCESS = 1;
608
609   unless ( $BOP_TESTING ) {
610     $transaction->test_transaction(1)
611       if $conf->exists('business-onlinepayment-test_transaction');
612     $transaction->submit();
613   } else {
614     if ( $BOP_TESTING_SUCCESS ) {
615       $transaction->is_success(1);
616       $transaction->authorization('fake auth');
617     } else {
618       $transaction->is_success(0);
619       $transaction->error_message('fake failure');
620     }
621   }
622
623   if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
624
625     $cust_pay_pending->status('thirdparty');
626     my $cpp_err = $cust_pay_pending->replace;
627     return { error => $cpp_err } if $cpp_err;
628     return { reference => $cust_pay_pending->paypendingnum,
629              map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
630
631   } elsif ( $transaction->is_success() && $action2 ) {
632
633     $cust_pay_pending->status('authorized');
634     my $cpp_authorized_err = $cust_pay_pending->replace;
635     return $cpp_authorized_err if $cpp_authorized_err;
636
637     my $auth = $transaction->authorization;
638     my $ordernum = $transaction->can('order_number')
639                    ? $transaction->order_number
640                    : '';
641
642     my $capture =
643       new Business::OnlinePayment( $payment_gateway->gateway_module,
644                                    $self->_bop_options(\%options),
645                                  );
646
647     my %capture = (
648       %content,
649       type           => $options{method},
650       action         => $action2,
651       $self->_bop_auth(\%options),          
652       order_number   => $ordernum,
653       amount         => $options{amount},
654       authorization  => $auth,
655       description    => $options{'description'},
656     );
657
658     foreach my $field (qw( authorization_source_code returned_ACI
659                            transaction_identifier validation_code           
660                            transaction_sequence_num local_transaction_date    
661                            local_transaction_time AVS_result_code          )) {
662       $capture{$field} = $transaction->$field() if $transaction->can($field);
663     }
664
665     $capture->content( %capture );
666
667     $capture->test_transaction(1)
668       if $conf->exists('business-onlinepayment-test_transaction');
669     $capture->submit();
670
671     unless ( $capture->is_success ) {
672       my $e = "Authorization successful but capture failed, custnum #".
673               $self->custnum. ': '.  $capture->result_code.
674               ": ". $capture->error_message;
675       warn $e;
676       return $e;
677     }
678
679   }
680
681   ###
682   # remove paycvv after initial transaction
683   ###
684
685   #false laziness w/misc/process/payment.cgi - check both to make sure working
686   # correctly
687   if ( length($self->paycvv)
688        && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
689   ) {
690     my $error = $self->remove_cvv;
691     if ( $error ) {
692       warn "WARNING: error removing cvv: $error\n";
693     }
694   }
695
696   ###
697   # Tokenize
698   ###
699
700
701   if ( $transaction->can('card_token') && $transaction->card_token ) {
702
703     $self->card_token($transaction->card_token);
704
705     if ( $options{'payinfo'} eq $self->payinfo ) {
706       $self->payinfo($transaction->card_token);
707       my $error = $self->replace;
708       if ( $error ) {
709         warn "WARNING: error storing token: $error, but proceeding anyway\n";
710       }
711     }
712
713   }
714
715   ###
716   # result handling
717   ###
718
719   $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
720
721 }
722
723 =item fake_bop
724
725 =cut
726
727 sub fake_bop {
728   my $self = shift;
729
730   my %options = ();
731   if (ref($_[0]) eq 'HASH') {
732     %options = %{$_[0]};
733   } else {
734     my ( $method, $amount ) = ( shift, shift );
735     %options = @_;
736     $options{method} = $method;
737     $options{amount} = $amount;
738   }
739   
740   if ( $options{'fake_failure'} ) {
741      return "Error: No error; test failure requested with fake_failure";
742   }
743
744   #my $paybatch = '';
745   #if ( $payment_gateway->gatewaynum ) { # agent override
746   #  $paybatch = $payment_gateway->gatewaynum. '-';
747   #}
748   #
749   #$paybatch .= "$processor:". $transaction->authorization;
750   #
751   #$paybatch .= ':'. $transaction->order_number
752   #  if $transaction->can('order_number')
753   #  && length($transaction->order_number);
754
755   my $paybatch = 'FakeProcessor:54:32';
756
757   my $cust_pay = new FS::cust_pay ( {
758      'custnum'  => $self->custnum,
759      'invnum'   => $options{'invnum'},
760      'paid'     => $options{amount},
761      '_date'    => '',
762      'payby'    => $bop_method2payby{$options{method}},
763      #'payinfo'  => $payinfo,
764      'payinfo'  => '4111111111111111',
765      'paybatch' => $paybatch,
766      #'paydate'  => $paydate,
767      'paydate'  => '2012-05-01',
768   } );
769   $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
770
771   if ( $DEBUG ) {
772       warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
773       warn "  $_ => $options{$_}\n" foreach keys %options;
774   }
775
776   my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
777
778   if ( $error ) {
779     $cust_pay->invnum(''); #try again with no specific invnum
780     my $error2 = $cust_pay->insert( $options{'manual'} ?
781                                     ( 'manual' => 1 ) : ()
782                                   );
783     if ( $error2 ) {
784       # gah, even with transactions.
785       my $e = 'WARNING: Card/ACH debited but database not updated - '.
786               "error inserting (fake!) payment: $error2".
787               " (previously tried insert with invnum #$options{'invnum'}" .
788               ": $error )";
789       warn $e;
790       return $e;
791     }
792   }
793
794   if ( $options{'paynum_ref'} ) {
795     ${ $options{'paynum_ref'} } = $cust_pay->paynum;
796   }
797
798   return ''; #no error
799
800 }
801
802
803 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
804
805 # Wraps up processing of a realtime credit card, ACH (electronic check) or
806 # phone bill transaction.
807
808 sub _realtime_bop_result {
809   my( $self, $cust_pay_pending, $transaction, %options ) = @_;
810
811   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
812
813   if ( $DEBUG ) {
814     warn "$me _realtime_bop_result: pending transaction ".
815       $cust_pay_pending->paypendingnum. "\n";
816     warn "  $_ => $options{$_}\n" foreach keys %options;
817   }
818
819   my $payment_gateway = $options{payment_gateway}
820     or return "no payment gateway in arguments to _realtime_bop_result";
821
822   $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
823   my $cpp_captured_err = $cust_pay_pending->replace;
824   return $cpp_captured_err if $cpp_captured_err;
825
826   if ( $transaction->is_success() ) {
827
828     my $paybatch = '';
829     if ( $payment_gateway->gatewaynum ) { # agent override
830       $paybatch = $payment_gateway->gatewaynum. '-';
831     }
832
833     $paybatch .= $payment_gateway->gateway_module. ":".
834       $transaction->authorization;
835
836     $paybatch .= ':'. $transaction->order_number
837       if $transaction->can('order_number')
838       && length($transaction->order_number);
839
840     my $cust_pay = new FS::cust_pay ( {
841        'custnum'  => $self->custnum,
842        'invnum'   => $options{'invnum'},
843        'paid'     => $cust_pay_pending->paid,
844        '_date'    => '',
845        'payby'    => $cust_pay_pending->payby,
846        'payinfo'  => $options{'payinfo'},
847        'paybatch' => $paybatch,
848        'paydate'  => $cust_pay_pending->paydate,
849        'pkgnum'   => $cust_pay_pending->pkgnum,
850        'discount_term' => $options{'discount_term'},
851     } );
852     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
853     $cust_pay->payunique( $options{payunique} )
854       if defined($options{payunique}) && length($options{payunique});
855
856     my $oldAutoCommit = $FS::UID::AutoCommit;
857     local $FS::UID::AutoCommit = 0;
858     my $dbh = dbh;
859
860     #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
861
862     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
863
864     if ( $error ) {
865       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
866       $cust_pay->invnum(''); #try again with no specific invnum
867       $cust_pay->paynum('');
868       my $error2 = $cust_pay->insert( $options{'manual'} ?
869                                       ( 'manual' => 1 ) : ()
870                                     );
871       if ( $error2 ) {
872         # gah.  but at least we have a record of the state we had to abort in
873         # from cust_pay_pending now.
874         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
875         my $e = "WARNING: $options{method} captured but payment not recorded -".
876                 " error inserting payment (". $payment_gateway->gateway_module.
877                 "): $error2".
878                 " (previously tried insert with invnum #$options{'invnum'}" .
879                 ": $error ) - pending payment saved as paypendingnum ".
880                 $cust_pay_pending->paypendingnum. "\n";
881         warn $e;
882         return $e;
883       }
884     }
885
886     my $jobnum = $cust_pay_pending->jobnum;
887     if ( $jobnum ) {
888        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
889       
890        unless ( $placeholder ) {
891          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
892          my $e = "WARNING: $options{method} captured but job $jobnum not ".
893              "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
894          warn $e;
895          return $e;
896        }
897
898        $error = $placeholder->delete;
899
900        if ( $error ) {
901          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
902          my $e = "WARNING: $options{method} captured but could not delete ".
903               "job $jobnum for paypendingnum ".
904               $cust_pay_pending->paypendingnum. ": $error\n";
905          warn $e;
906          return $e;
907        }
908
909     }
910     
911     if ( $options{'paynum_ref'} ) {
912       ${ $options{'paynum_ref'} } = $cust_pay->paynum;
913     }
914
915     $cust_pay_pending->status('done');
916     $cust_pay_pending->statustext('captured');
917     $cust_pay_pending->paynum($cust_pay->paynum);
918     my $cpp_done_err = $cust_pay_pending->replace;
919
920     if ( $cpp_done_err ) {
921
922       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
923       my $e = "WARNING: $options{method} captured but payment not recorded - ".
924               "error updating status for paypendingnum ".
925               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
926       warn $e;
927       return $e;
928
929     } else {
930
931       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
932
933       if ( $options{'apply'} ) {
934         my $apply_error = $self->apply_payments_and_credits;
935         if ( $apply_error ) {
936           warn "WARNING: error applying payment: $apply_error\n";
937           #but we still should return no error cause the payment otherwise went
938           #through...
939         }
940       }
941
942       # have a CC surcharge portion --> one-time charge
943       if ( $options{'cc_surcharge'} > 0 ) { 
944             # XXX: this whole block needs to be in a transaction?
945
946           my $invnum;
947           $invnum = $options{'invnum'} if $options{'invnum'};
948           unless ( $invnum ) { # probably from a payment screen
949              # do we have any open invoices? pick earliest
950              # uses the fact that cust_main->cust_bill sorts by date ascending
951              my @open = $self->open_cust_bill;
952              $invnum = $open[0]->invnum if scalar(@open);
953           }
954             
955           unless ( $invnum ) {  # still nothing? pick last closed invoice
956              # again uses fact that cust_main->cust_bill sorts by date ascending
957              my @closed = $self->cust_bill;
958              $invnum = $closed[$#closed]->invnum if scalar(@closed);
959           }
960
961           unless ( $invnum ) {
962             # XXX: unlikely case - pre-paying before any invoices generated
963             # what it should do is create a new invoice and pick it
964                 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
965                 return '';
966           }
967
968           my $cust_pkg;
969           my $charge_error = $self->charge({
970                                     'amount'    => $options{'cc_surcharge'},
971                                     'pkg'       => 'Credit Card Surcharge',
972                                     'setuptax'  => 'Y',
973                                     'cust_pkg_ref' => \$cust_pkg,
974                                 });
975           if($charge_error) {
976                 warn 'Unable to add CC surcharge cust_pkg';
977                 return '';
978           }
979
980           $cust_pkg->setup(time);
981           my $cp_error = $cust_pkg->replace;
982           if($cp_error) {
983               warn 'Unable to set setup time on cust_pkg for cc surcharge';
984             # but keep going...
985           }
986                                     
987           my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
988           unless ( $cust_bill ) {
989               warn "race condition + invoice deletion just happened";
990               return '';
991           }
992
993           my $grand_error = 
994             $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
995
996           warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
997             if $grand_error;
998       }
999
1000       return ''; #no error
1001
1002     }
1003
1004   } else {
1005
1006     my $perror = $payment_gateway->gateway_module. " error: ".
1007       $transaction->error_message;
1008
1009     my $jobnum = $cust_pay_pending->jobnum;
1010     if ( $jobnum ) {
1011        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1012       
1013        if ( $placeholder ) {
1014          my $error = $placeholder->depended_delete;
1015          $error ||= $placeholder->delete;
1016          warn "error removing provisioning jobs after declined paypendingnum ".
1017            $cust_pay_pending->paypendingnum. ": $error\n";
1018        } else {
1019          my $e = "error finding job $jobnum for declined paypendingnum ".
1020               $cust_pay_pending->paypendingnum. "\n";
1021          warn $e;
1022        }
1023
1024     }
1025     
1026     unless ( $transaction->error_message ) {
1027
1028       my $t_response;
1029       if ( $transaction->can('response_page') ) {
1030         $t_response = {
1031                         'page'    => ( $transaction->can('response_page')
1032                                          ? $transaction->response_page
1033                                          : ''
1034                                      ),
1035                         'code'    => ( $transaction->can('response_code')
1036                                          ? $transaction->response_code
1037                                          : ''
1038                                      ),
1039                         'headers' => ( $transaction->can('response_headers')
1040                                          ? $transaction->response_headers
1041                                          : ''
1042                                      ),
1043                       };
1044       } else {
1045         $t_response .=
1046           "No additional debugging information available for ".
1047             $payment_gateway->gateway_module;
1048       }
1049
1050       $perror .= "No error_message returned from ".
1051                    $payment_gateway->gateway_module. " -- ".
1052                  ( ref($t_response) ? Dumper($t_response) : $t_response );
1053
1054     }
1055
1056     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1057          && $conf->exists('emaildecline', $self->agentnum)
1058          && grep { $_ ne 'POST' } $self->invoicing_list
1059          && ! grep { $transaction->error_message =~ /$_/ }
1060                    $conf->config('emaildecline-exclude', $self->agentnum)
1061     ) {
1062
1063       # Send a decline alert to the customer.
1064       my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1065       my $error = '';
1066       if ( $msgnum ) {
1067         # include the raw error message in the transaction state
1068         $cust_pay_pending->setfield('error', $transaction->error_message);
1069         my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1070         $error = $msg_template->send( 'cust_main' => $self,
1071                                       'object'    => $cust_pay_pending );
1072       }
1073       else { #!$msgnum
1074
1075         my @templ = $conf->config('declinetemplate');
1076         my $template = new Text::Template (
1077           TYPE   => 'ARRAY',
1078           SOURCE => [ map "$_\n", @templ ],
1079         ) or return "($perror) can't create template: $Text::Template::ERROR";
1080         $template->compile()
1081           or return "($perror) can't compile template: $Text::Template::ERROR";
1082
1083         my $templ_hash = {
1084           'company_name'    =>
1085             scalar( $conf->config('company_name', $self->agentnum ) ),
1086           'company_address' =>
1087             join("\n", $conf->config('company_address', $self->agentnum ) ),
1088           'error'           => $transaction->error_message,
1089         };
1090
1091         my $error = send_email(
1092           'from'    => $conf->config('invoice_from', $self->agentnum ),
1093           'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1094           'subject' => 'Your payment could not be processed',
1095           'body'    => [ $template->fill_in(HASH => $templ_hash) ],
1096         );
1097       }
1098
1099       $perror .= " (also received error sending decline notification: $error)"
1100         if $error;
1101
1102     }
1103
1104     $cust_pay_pending->status('done');
1105     $cust_pay_pending->statustext("declined: $perror");
1106     my $cpp_done_err = $cust_pay_pending->replace;
1107     if ( $cpp_done_err ) {
1108       my $e = "WARNING: $options{method} declined but pending payment not ".
1109               "resolved - error updating status for paypendingnum ".
1110               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1111       warn $e;
1112       $perror = "$e ($perror)";
1113     }
1114
1115     return $perror;
1116   }
1117
1118 }
1119
1120 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1121
1122 Verifies successful third party processing of a realtime credit card,
1123 ACH (electronic check) or phone bill transaction via a
1124 Business::OnlineThirdPartyPayment realtime gateway.  See
1125 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1126
1127 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1128
1129 The additional options I<payname>, I<city>, I<state>,
1130 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1131 if set, will override the value from the customer record.
1132
1133 I<description> is a free-text field passed to the gateway.  It defaults to
1134 "Internet services".
1135
1136 If an I<invnum> is specified, this payment (if successful) is applied to the
1137 specified invoice.  If you don't specify an I<invnum> you might want to
1138 call the B<apply_payments> method.
1139
1140 I<quiet> can be set true to surpress email decline notices.
1141
1142 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
1143 resulting paynum, if any.
1144
1145 I<payunique> is a unique identifier for this payment.
1146
1147 Returns a hashref containing elements bill_error (which will be undefined
1148 upon success) and session_id of any associated session.
1149
1150 =cut
1151
1152 sub realtime_botpp_capture {
1153   my( $self, $cust_pay_pending, %options ) = @_;
1154
1155   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1156
1157   if ( $DEBUG ) {
1158     warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1159     warn "  $_ => $options{$_}\n" foreach keys %options;
1160   }
1161
1162   eval "use Business::OnlineThirdPartyPayment";  
1163   die $@ if $@;
1164
1165   ###
1166   # select the gateway
1167   ###
1168
1169   my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1170
1171   my $payment_gateway;
1172   my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1173   $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1174                 { gatewaynum => $gatewaynum }
1175               )
1176     : $self->agent->payment_gateway( 'method' => $method,
1177                                      # 'invnum'  => $cust_pay_pending->invnum,
1178                                      # 'payinfo' => $cust_pay_pending->payinfo,
1179                                    );
1180
1181   $options{payment_gateway} = $payment_gateway; # for the helper subs
1182
1183   ###
1184   # massage data
1185   ###
1186
1187   my @invoicing_list = $self->invoicing_list_emailonly;
1188   if ( $conf->exists('emailinvoiceautoalways')
1189        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1190        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1191     push @invoicing_list, $self->all_emails;
1192   }
1193
1194   my $email = ($conf->exists('business-onlinepayment-email-override'))
1195               ? $conf->config('business-onlinepayment-email-override')
1196               : $invoicing_list[0];
1197
1198   my %content = ();
1199
1200   $content{email_customer} = 
1201     (    $conf->exists('business-onlinepayment-email_customer')
1202       || $conf->exists('business-onlinepayment-email-override') );
1203       
1204   ###
1205   # run transaction(s)
1206   ###
1207
1208   my $transaction =
1209     new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1210                                            $self->_bop_options(\%options),
1211                                          );
1212
1213   $transaction->reference({ %options }); 
1214
1215   $transaction->content(
1216     'type'           => $method,
1217     $self->_bop_auth(\%options),
1218     'action'         => 'Post Authorization',
1219     'description'    => $options{'description'},
1220     'amount'         => $cust_pay_pending->paid,
1221     #'invoice_number' => $options{'invnum'},
1222     'customer_id'    => $self->custnum,
1223     'referer'        => 'http://cleanwhisker.420.am/',
1224     'reference'      => $cust_pay_pending->paypendingnum,
1225     'email'          => $email,
1226     'phone'          => $self->daytime || $self->night,
1227     %content, #after
1228     # plus whatever is required for bogus capture avoidance
1229   );
1230
1231   $transaction->submit();
1232
1233   my $error =
1234     $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1235
1236   if ( $options{'apply'} ) {
1237     my $apply_error = $self->apply_payments_and_credits;
1238     if ( $apply_error ) {
1239       warn "WARNING: error applying payment: $apply_error\n";
1240     }
1241   }
1242
1243   return {
1244     bill_error => $error,
1245     session_id => $cust_pay_pending->session_id,
1246   }
1247
1248 }
1249
1250 =item default_payment_gateway
1251
1252 DEPRECATED -- use agent->payment_gateway
1253
1254 =cut
1255
1256 sub default_payment_gateway {
1257   my( $self, $method ) = @_;
1258
1259   die "Real-time processing not enabled\n"
1260     unless $conf->exists('business-onlinepayment');
1261
1262   #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1263
1264   #load up config
1265   my $bop_config = 'business-onlinepayment';
1266   $bop_config .= '-ach'
1267     if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1268   my ( $processor, $login, $password, $action, @bop_options ) =
1269     $conf->config($bop_config);
1270   $action ||= 'normal authorization';
1271   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1272   die "No real-time processor is enabled - ".
1273       "did you set the business-onlinepayment configuration value?\n"
1274     unless $processor;
1275
1276   ( $processor, $login, $password, $action, @bop_options )
1277 }
1278
1279 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1280
1281 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1282 via a Business::OnlinePayment realtime gateway.  See
1283 L<http://420.am/business-onlinepayment> for supported gateways.
1284
1285 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1286
1287 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1288
1289 Most gateways require a reference to an original payment transaction to refund,
1290 so you probably need to specify a I<paynum>.
1291
1292 I<amount> defaults to the original amount of the payment if not specified.
1293
1294 I<reason> specifies a reason for the refund.
1295
1296 I<paydate> specifies the expiration date for a credit card overriding the
1297 value from the customer record or the payment record. Specified as yyyy-mm-dd
1298
1299 Implementation note: If I<amount> is unspecified or equal to the amount of the
1300 orignal payment, first an attempt is made to "void" the transaction via
1301 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1302 the normal attempt is made to "refund" ("credit") the transaction via the
1303 gateway is attempted. No attempt to "void" the transaction is made if the 
1304 gateway has introspection data and doesn't support void.
1305
1306 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1307 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1308 #if set, will override the value from the customer record.
1309
1310 #If an I<invnum> is specified, this payment (if successful) is applied to the
1311 #specified invoice.  If you don't specify an I<invnum> you might want to
1312 #call the B<apply_payments> method.
1313
1314 =cut
1315
1316 #some false laziness w/realtime_bop, not enough to make it worth merging
1317 #but some useful small subs should be pulled out
1318 sub realtime_refund_bop {
1319   my $self = shift;
1320
1321   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1322
1323   my %options = ();
1324   if (ref($_[0]) eq 'HASH') {
1325     %options = %{$_[0]};
1326   } else {
1327     my $method = shift;
1328     %options = @_;
1329     $options{method} = $method;
1330   }
1331
1332   if ( $DEBUG ) {
1333     warn "$me realtime_refund_bop (new): $options{method} refund\n";
1334     warn "  $_ => $options{$_}\n" foreach keys %options;
1335   }
1336
1337   ###
1338   # look up the original payment and optionally a gateway for that payment
1339   ###
1340
1341   my $cust_pay = '';
1342   my $amount = $options{'amount'};
1343
1344   my( $processor, $login, $password, @bop_options, $namespace ) ;
1345   my( $auth, $order_number ) = ( '', '', '' );
1346
1347   if ( $options{'paynum'} ) {
1348
1349     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
1350     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1351       or return "Unknown paynum $options{'paynum'}";
1352     $amount ||= $cust_pay->paid;
1353
1354     $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1355       or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1356                 $cust_pay->paybatch;
1357     my $gatewaynum = '';
1358     ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1359
1360     if ( $gatewaynum ) { #gateway for the payment to be refunded
1361
1362       my $payment_gateway =
1363         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1364       die "payment gateway $gatewaynum not found"
1365         unless $payment_gateway;
1366
1367       $processor   = $payment_gateway->gateway_module;
1368       $login       = $payment_gateway->gateway_username;
1369       $password    = $payment_gateway->gateway_password;
1370       $namespace   = $payment_gateway->gateway_namespace;
1371       @bop_options = $payment_gateway->options;
1372
1373     } else { #try the default gateway
1374
1375       my $conf_processor;
1376       my $payment_gateway =
1377         $self->agent->payment_gateway('method' => $options{method});
1378
1379       ( $conf_processor, $login, $password, $namespace ) =
1380         map { my $method = "gateway_$_"; $payment_gateway->$method }
1381           qw( module username password namespace );
1382
1383       @bop_options = $payment_gateway->gatewaynum
1384                        ? $payment_gateway->options
1385                        : @{ $payment_gateway->get('options') };
1386
1387       return "processor of payment $options{'paynum'} $processor does not".
1388              " match default processor $conf_processor"
1389         unless $processor eq $conf_processor;
1390
1391     }
1392
1393
1394   } else { # didn't specify a paynum, so look for agent gateway overrides
1395            # like a normal transaction 
1396  
1397     my $payment_gateway =
1398       $self->agent->payment_gateway( 'method'  => $options{method},
1399                                      #'payinfo' => $payinfo,
1400                                    );
1401     my( $processor, $login, $password, $namespace ) =
1402       map { my $method = "gateway_$_"; $payment_gateway->$method }
1403         qw( module username password namespace );
1404
1405     my @bop_options = $payment_gateway->gatewaynum
1406                         ? $payment_gateway->options
1407                         : @{ $payment_gateway->get('options') };
1408
1409   }
1410   return "neither amount nor paynum specified" unless $amount;
1411
1412   eval "use $namespace";  
1413   die $@ if $@;
1414
1415   my %content = (
1416     'type'           => $options{method},
1417     'login'          => $login,
1418     'password'       => $password,
1419     'order_number'   => $order_number,
1420     'amount'         => $amount,
1421     'referer'        => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1422   );
1423   $content{authorization} = $auth
1424     if length($auth); #echeck/ACH transactions have an order # but no auth
1425                       #(at least with authorize.net)
1426
1427   my $disable_void_after;
1428   if ($conf->exists('disable_void_after')
1429       && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1430     $disable_void_after = $1;
1431   }
1432
1433   #first try void if applicable
1434   my $void = new Business::OnlinePayment( $processor, @bop_options );
1435
1436   my $tryvoid = 1;
1437   if ($void->can('info')) {
1438       my $paytype = '';
1439       $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1440       $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1441       my %supported_actions = $void->info('supported_actions');
1442       $tryvoid = 0 
1443         if ( %supported_actions && $paytype 
1444                 && defined($supported_actions{$paytype}) 
1445                 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1446   }
1447
1448   if ( $cust_pay && $cust_pay->paid == $amount
1449     && (
1450       ( not defined($disable_void_after) )
1451       || ( time < ($cust_pay->_date + $disable_void_after ) )
1452     )
1453     && $tryvoid
1454   ) {
1455     warn "  attempting void\n" if $DEBUG > 1;
1456     if ( $void->can('info') ) {
1457       if ( $cust_pay->payby eq 'CARD'
1458            && $void->info('CC_void_requires_card') )
1459       {
1460         $content{'card_number'} = $cust_pay->payinfo;
1461       } elsif ( $cust_pay->payby eq 'CHEK'
1462                 && $void->info('ECHECK_void_requires_account') )
1463       {
1464         ( $content{'account_number'}, $content{'routing_code'} ) =
1465           split('@', $cust_pay->payinfo);
1466         $content{'name'} = $self->get('first'). ' '. $self->get('last');
1467       }
1468     }
1469     $void->content( 'action' => 'void', %content );
1470     $void->test_transaction(1)
1471       if $conf->exists('business-onlinepayment-test_transaction');
1472     $void->submit();
1473     if ( $void->is_success ) {
1474       my $error = $cust_pay->void($options{'reason'});
1475       if ( $error ) {
1476         # gah, even with transactions.
1477         my $e = 'WARNING: Card/ACH voided but database not updated - '.
1478                 "error voiding payment: $error";
1479         warn $e;
1480         return $e;
1481       }
1482       warn "  void successful\n" if $DEBUG > 1;
1483       return '';
1484     }
1485   }
1486
1487   warn "  void unsuccessful, trying refund\n"
1488     if $DEBUG > 1;
1489
1490   #massage data
1491   my $address = $self->address1;
1492   $address .= ", ". $self->address2 if $self->address2;
1493
1494   my($payname, $payfirst, $paylast);
1495   if ( $self->payname && $options{method} ne 'ECHECK' ) {
1496     $payname = $self->payname;
1497     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1498       or return "Illegal payname $payname";
1499     ($payfirst, $paylast) = ($1, $2);
1500   } else {
1501     $payfirst = $self->getfield('first');
1502     $paylast = $self->getfield('last');
1503     $payname =  "$payfirst $paylast";
1504   }
1505
1506   my @invoicing_list = $self->invoicing_list_emailonly;
1507   if ( $conf->exists('emailinvoiceautoalways')
1508        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1509        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1510     push @invoicing_list, $self->all_emails;
1511   }
1512
1513   my $email = ($conf->exists('business-onlinepayment-email-override'))
1514               ? $conf->config('business-onlinepayment-email-override')
1515               : $invoicing_list[0];
1516
1517   my $payip = exists($options{'payip'})
1518                 ? $options{'payip'}
1519                 : $self->payip;
1520   $content{customer_ip} = $payip
1521     if length($payip);
1522
1523   my $payinfo = '';
1524   if ( $options{method} eq 'CC' ) {
1525
1526     if ( $cust_pay ) {
1527       $content{card_number} = $payinfo = $cust_pay->payinfo;
1528       (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1529         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1530         ($content{expiration} = "$2/$1");  # where available
1531     } else {
1532       $content{card_number} = $payinfo = $self->payinfo;
1533       (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1534         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1535       $content{expiration} = "$2/$1";
1536     }
1537
1538   } elsif ( $options{method} eq 'ECHECK' ) {
1539
1540     if ( $cust_pay ) {
1541       $payinfo = $cust_pay->payinfo;
1542     } else {
1543       $payinfo = $self->payinfo;
1544     } 
1545     ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1546     $content{bank_name} = $self->payname;
1547     $content{account_type} = 'CHECKING';
1548     $content{account_name} = $payname;
1549     $content{customer_org} = $self->company ? 'B' : 'I';
1550     $content{customer_ssn} = $self->ss;
1551   } elsif ( $options{method} eq 'LEC' ) {
1552     $content{phone} = $payinfo = $self->payinfo;
1553   }
1554
1555   #then try refund
1556   my $refund = new Business::OnlinePayment( $processor, @bop_options );
1557   my %sub_content = $refund->content(
1558     'action'         => 'credit',
1559     'customer_id'    => $self->custnum,
1560     'last_name'      => $paylast,
1561     'first_name'     => $payfirst,
1562     'name'           => $payname,
1563     'address'        => $address,
1564     'city'           => $self->city,
1565     'state'          => $self->state,
1566     'zip'            => $self->zip,
1567     'country'        => $self->country,
1568     'email'          => $email,
1569     'phone'          => $self->daytime || $self->night,
1570     %content, #after
1571   );
1572   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
1573     if $DEBUG > 1;
1574   $refund->test_transaction(1)
1575     if $conf->exists('business-onlinepayment-test_transaction');
1576   $refund->submit();
1577
1578   return "$processor error: ". $refund->error_message
1579     unless $refund->is_success();
1580
1581   my $paybatch = "$processor:". $refund->authorization;
1582   $paybatch .= ':'. $refund->order_number
1583     if $refund->can('order_number') && $refund->order_number;
1584
1585   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1586     my @cust_bill_pay = $cust_pay->cust_bill_pay;
1587     last unless @cust_bill_pay;
1588     my $cust_bill_pay = pop @cust_bill_pay;
1589     my $error = $cust_bill_pay->delete;
1590     last if $error;
1591   }
1592
1593   my $cust_refund = new FS::cust_refund ( {
1594     'custnum'  => $self->custnum,
1595     'paynum'   => $options{'paynum'},
1596     'refund'   => $amount,
1597     '_date'    => '',
1598     'payby'    => $bop_method2payby{$options{method}},
1599     'payinfo'  => $payinfo,
1600     'paybatch' => $paybatch,
1601     'reason'   => $options{'reason'} || 'card or ACH refund',
1602   } );
1603   my $error = $cust_refund->insert;
1604   if ( $error ) {
1605     $cust_refund->paynum(''); #try again with no specific paynum
1606     my $error2 = $cust_refund->insert;
1607     if ( $error2 ) {
1608       # gah, even with transactions.
1609       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1610               "error inserting refund ($processor): $error2".
1611               " (previously tried insert with paynum #$options{'paynum'}" .
1612               ": $error )";
1613       warn $e;
1614       return $e;
1615     }
1616   }
1617
1618   ''; #no error
1619
1620 }
1621
1622 =back
1623
1624 =head1 BUGS
1625
1626 Not autoloaded.
1627
1628 =head1 SEE ALSO
1629
1630 L<FS::cust_main>, L<FS::cust_main::Billing>
1631
1632 =cut
1633
1634 1;