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