97e7c94eb11f57852298d2536183881063195d24
[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   $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 = FS::banned_pay->ban_search(
371     'payby'   => $bop_method2payby{$options{method}},
372     'payinfo' => $options{payinfo},
373   );
374   return "Banned credit card" if $ban && $ban->bantype ne 'warn';
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
492   #for third-party payments only, remove pending payments if they're in the 
493   #'thirdparty' (waiting for customer action) state.
494   if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
495     foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
496       my $error = $_->delete;
497       warn "error deleting unfinished third-party payment ".
498           $_->paypendingnum . ": $error\n"
499         if $error;
500     }
501     @pending = grep { $_->status ne 'thirdparty' } @pending;
502   }
503
504   return "A payment is already being processed for this customer (".
505          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
506          "); $options{method} transaction aborted."
507     if scalar(@pending);
508
509   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
510
511   my $cust_pay_pending = new FS::cust_pay_pending {
512     'custnum'           => $self->custnum,
513     'paid'              => $options{amount},
514     '_date'             => '',
515     'payby'             => $bop_method2payby{$options{method}},
516     'payinfo'           => $options{payinfo},
517     'paydate'           => $paydate,
518     'recurring_billing' => $content{recurring_billing},
519     'pkgnum'            => $options{'pkgnum'},
520     'status'            => 'new',
521     'gatewaynum'        => $payment_gateway->gatewaynum || '',
522     'session_id'        => $options{session_id} || '',
523     'jobnum'            => $options{depend_jobnum} || '',
524   };
525   $cust_pay_pending->payunique( $options{payunique} )
526     if defined($options{payunique}) && length($options{payunique});
527   my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
528   return $cpp_new_err if $cpp_new_err;
529
530   my( $action1, $action2 ) =
531     split( /\s*\,\s*/, $payment_gateway->gateway_action );
532
533   my $transaction = new $namespace( $payment_gateway->gateway_module,
534                                     $self->_bop_options(\%options),
535                                   );
536
537   $transaction->content(
538     'type'           => $options{method},
539     $self->_bop_auth(\%options),          
540     'action'         => $action1,
541     'description'    => $options{'description'},
542     'amount'         => $options{amount},
543     #'invoice_number' => $options{'invnum'},
544     'customer_id'    => $self->custnum,
545     %$bop_content,
546     'reference'      => $cust_pay_pending->paypendingnum, #for now
547     'callback_url'   => $payment_gateway->gateway_callback_url,
548     'email'          => $email,
549     %content, #after
550   );
551
552   $cust_pay_pending->status('pending');
553   my $cpp_pending_err = $cust_pay_pending->replace;
554   return $cpp_pending_err if $cpp_pending_err;
555
556   #config?
557   my $BOP_TESTING = 0;
558   my $BOP_TESTING_SUCCESS = 1;
559
560   unless ( $BOP_TESTING ) {
561     $transaction->test_transaction(1)
562       if $conf->exists('business-onlinepayment-test_transaction');
563     $transaction->submit();
564   } else {
565     if ( $BOP_TESTING_SUCCESS ) {
566       $transaction->is_success(1);
567       $transaction->authorization('fake auth');
568     } else {
569       $transaction->is_success(0);
570       $transaction->error_message('fake failure');
571     }
572   }
573
574   if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
575
576     $cust_pay_pending->status('thirdparty');
577     my $cpp_err = $cust_pay_pending->replace;
578     return { error => $cpp_err } if $cpp_err;
579     return { reference => $cust_pay_pending->paypendingnum,
580              map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
581
582   } elsif ( $transaction->is_success() && $action2 ) {
583
584     $cust_pay_pending->status('authorized');
585     my $cpp_authorized_err = $cust_pay_pending->replace;
586     return $cpp_authorized_err if $cpp_authorized_err;
587
588     my $auth = $transaction->authorization;
589     my $ordernum = $transaction->can('order_number')
590                    ? $transaction->order_number
591                    : '';
592
593     my $capture =
594       new Business::OnlinePayment( $payment_gateway->gateway_module,
595                                    $self->_bop_options(\%options),
596                                  );
597
598     my %capture = (
599       %content,
600       type           => $options{method},
601       action         => $action2,
602       $self->_bop_auth(\%options),          
603       order_number   => $ordernum,
604       amount         => $options{amount},
605       authorization  => $auth,
606       description    => $options{'description'},
607     );
608
609     foreach my $field (qw( authorization_source_code returned_ACI
610                            transaction_identifier validation_code           
611                            transaction_sequence_num local_transaction_date    
612                            local_transaction_time AVS_result_code          )) {
613       $capture{$field} = $transaction->$field() if $transaction->can($field);
614     }
615
616     $capture->content( %capture );
617
618     $capture->test_transaction(1)
619       if $conf->exists('business-onlinepayment-test_transaction');
620     $capture->submit();
621
622     unless ( $capture->is_success ) {
623       my $e = "Authorization successful but capture failed, custnum #".
624               $self->custnum. ': '.  $capture->result_code.
625               ": ". $capture->error_message;
626       warn $e;
627       return $e;
628     }
629
630   }
631
632   ###
633   # remove paycvv after initial transaction
634   ###
635
636   #false laziness w/misc/process/payment.cgi - check both to make sure working
637   # correctly
638   if ( length($self->paycvv)
639        && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
640   ) {
641     my $error = $self->remove_cvv;
642     if ( $error ) {
643       warn "WARNING: error removing cvv: $error\n";
644     }
645   }
646
647   ###
648   # Tokenize
649   ###
650
651
652   if ( $transaction->can('card_token') && $transaction->card_token ) {
653
654     $self->card_token($transaction->card_token);
655
656     if ( $options{'payinfo'} eq $self->payinfo ) {
657       $self->payinfo($transaction->card_token);
658       my $error = $self->replace;
659       if ( $error ) {
660         warn "WARNING: error storing token: $error, but proceeding anyway\n";
661       }
662     }
663
664   }
665
666   ###
667   # result handling
668   ###
669
670   $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
671
672 }
673
674 =item fake_bop
675
676 =cut
677
678 sub fake_bop {
679   my $self = shift;
680
681   my %options = ();
682   if (ref($_[0]) eq 'HASH') {
683     %options = %{$_[0]};
684   } else {
685     my ( $method, $amount ) = ( shift, shift );
686     %options = @_;
687     $options{method} = $method;
688     $options{amount} = $amount;
689   }
690   
691   if ( $options{'fake_failure'} ) {
692      return "Error: No error; test failure requested with fake_failure";
693   }
694
695   #my $paybatch = '';
696   #if ( $payment_gateway->gatewaynum ) { # agent override
697   #  $paybatch = $payment_gateway->gatewaynum. '-';
698   #}
699   #
700   #$paybatch .= "$processor:". $transaction->authorization;
701   #
702   #$paybatch .= ':'. $transaction->order_number
703   #  if $transaction->can('order_number')
704   #  && length($transaction->order_number);
705
706   my $paybatch = 'FakeProcessor:54:32';
707
708   my $cust_pay = new FS::cust_pay ( {
709      'custnum'  => $self->custnum,
710      'invnum'   => $options{'invnum'},
711      'paid'     => $options{amount},
712      '_date'    => '',
713      'payby'    => $bop_method2payby{$options{method}},
714      #'payinfo'  => $payinfo,
715      'payinfo'  => '4111111111111111',
716      'paybatch' => $paybatch,
717      #'paydate'  => $paydate,
718      'paydate'  => '2012-05-01',
719   } );
720   $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
721
722   my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
723
724   if ( $error ) {
725     $cust_pay->invnum(''); #try again with no specific invnum
726     my $error2 = $cust_pay->insert( $options{'manual'} ?
727                                     ( 'manual' => 1 ) : ()
728                                   );
729     if ( $error2 ) {
730       # gah, even with transactions.
731       my $e = 'WARNING: Card/ACH debited but database not updated - '.
732               "error inserting (fake!) payment: $error2".
733               " (previously tried insert with invnum #$options{'invnum'}" .
734               ": $error )";
735       warn $e;
736       return $e;
737     }
738   }
739
740   if ( $options{'paynum_ref'} ) {
741     ${ $options{'paynum_ref'} } = $cust_pay->paynum;
742   }
743
744   return ''; #no error
745
746 }
747
748
749 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
750
751 # Wraps up processing of a realtime credit card, ACH (electronic check) or
752 # phone bill transaction.
753
754 sub _realtime_bop_result {
755   my( $self, $cust_pay_pending, $transaction, %options ) = @_;
756
757   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
758
759   if ( $DEBUG ) {
760     warn "$me _realtime_bop_result: pending transaction ".
761       $cust_pay_pending->paypendingnum. "\n";
762     warn "  $_ => $options{$_}\n" foreach keys %options;
763   }
764
765   my $payment_gateway = $options{payment_gateway}
766     or return "no payment gateway in arguments to _realtime_bop_result";
767
768   $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
769   my $cpp_captured_err = $cust_pay_pending->replace;
770   return $cpp_captured_err if $cpp_captured_err;
771
772   if ( $transaction->is_success() ) {
773
774     my $paybatch = '';
775     if ( $payment_gateway->gatewaynum ) { # agent override
776       $paybatch = $payment_gateway->gatewaynum. '-';
777     }
778
779     $paybatch .= $payment_gateway->gateway_module. ":".
780       $transaction->authorization;
781
782     $paybatch .= ':'. $transaction->order_number
783       if $transaction->can('order_number')
784       && length($transaction->order_number);
785
786     my $cust_pay = new FS::cust_pay ( {
787        'custnum'  => $self->custnum,
788        'invnum'   => $options{'invnum'},
789        'paid'     => $cust_pay_pending->paid,
790        '_date'    => '',
791        'payby'    => $cust_pay_pending->payby,
792        'payinfo'  => $options{'payinfo'},
793        'paybatch' => $paybatch,
794        'paydate'  => $cust_pay_pending->paydate,
795        'pkgnum'   => $cust_pay_pending->pkgnum,
796        'discount_term' => $options{'discount_term'},
797     } );
798     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
799     $cust_pay->payunique( $options{payunique} )
800       if defined($options{payunique}) && length($options{payunique});
801
802     my $oldAutoCommit = $FS::UID::AutoCommit;
803     local $FS::UID::AutoCommit = 0;
804     my $dbh = dbh;
805
806     #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
807
808     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
809
810     if ( $error ) {
811       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
812       $cust_pay->invnum(''); #try again with no specific invnum
813       $cust_pay->paynum('');
814       my $error2 = $cust_pay->insert( $options{'manual'} ?
815                                       ( 'manual' => 1 ) : ()
816                                     );
817       if ( $error2 ) {
818         # gah.  but at least we have a record of the state we had to abort in
819         # from cust_pay_pending now.
820         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
821         my $e = "WARNING: $options{method} captured but payment not recorded -".
822                 " error inserting payment (". $payment_gateway->gateway_module.
823                 "): $error2".
824                 " (previously tried insert with invnum #$options{'invnum'}" .
825                 ": $error ) - pending payment saved as paypendingnum ".
826                 $cust_pay_pending->paypendingnum. "\n";
827         warn $e;
828         return $e;
829       }
830     }
831
832     my $jobnum = $cust_pay_pending->jobnum;
833     if ( $jobnum ) {
834        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
835       
836        unless ( $placeholder ) {
837          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
838          my $e = "WARNING: $options{method} captured but job $jobnum not ".
839              "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
840          warn $e;
841          return $e;
842        }
843
844        $error = $placeholder->delete;
845
846        if ( $error ) {
847          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
848          my $e = "WARNING: $options{method} captured but could not delete ".
849               "job $jobnum for paypendingnum ".
850               $cust_pay_pending->paypendingnum. ": $error\n";
851          warn $e;
852          return $e;
853        }
854
855     }
856     
857     if ( $options{'paynum_ref'} ) {
858       ${ $options{'paynum_ref'} } = $cust_pay->paynum;
859     }
860
861     $cust_pay_pending->status('done');
862     $cust_pay_pending->statustext('captured');
863     $cust_pay_pending->paynum($cust_pay->paynum);
864     my $cpp_done_err = $cust_pay_pending->replace;
865
866     if ( $cpp_done_err ) {
867
868       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
869       my $e = "WARNING: $options{method} captured but payment not recorded - ".
870               "error updating status for paypendingnum ".
871               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
872       warn $e;
873       return $e;
874
875     } else {
876
877       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
878
879       if ( $options{'apply'} ) {
880         my $apply_error = $self->apply_payments_and_credits;
881         if ( $apply_error ) {
882           warn "WARNING: error applying payment: $apply_error\n";
883           #but we still should return no error cause the payment otherwise went
884           #through...
885         }
886       }
887
888       return ''; #no error
889
890     }
891
892   } else {
893
894     my $perror = $payment_gateway->gateway_module. " error: ".
895       $transaction->error_message;
896
897     my $jobnum = $cust_pay_pending->jobnum;
898     if ( $jobnum ) {
899        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
900       
901        if ( $placeholder ) {
902          my $error = $placeholder->depended_delete;
903          $error ||= $placeholder->delete;
904          warn "error removing provisioning jobs after declined paypendingnum ".
905            $cust_pay_pending->paypendingnum. ": $error\n";
906        } else {
907          my $e = "error finding job $jobnum for declined paypendingnum ".
908               $cust_pay_pending->paypendingnum. "\n";
909          warn $e;
910        }
911
912     }
913     
914     unless ( $transaction->error_message ) {
915
916       my $t_response;
917       if ( $transaction->can('response_page') ) {
918         $t_response = {
919                         'page'    => ( $transaction->can('response_page')
920                                          ? $transaction->response_page
921                                          : ''
922                                      ),
923                         'code'    => ( $transaction->can('response_code')
924                                          ? $transaction->response_code
925                                          : ''
926                                      ),
927                         'headers' => ( $transaction->can('response_headers')
928                                          ? $transaction->response_headers
929                                          : ''
930                                      ),
931                       };
932       } else {
933         $t_response .=
934           "No additional debugging information available for ".
935             $payment_gateway->gateway_module;
936       }
937
938       $perror .= "No error_message returned from ".
939                    $payment_gateway->gateway_module. " -- ".
940                  ( ref($t_response) ? Dumper($t_response) : $t_response );
941
942     }
943
944     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
945          && $conf->exists('emaildecline', $self->agentnum)
946          && grep { $_ ne 'POST' } $self->invoicing_list
947          && ! grep { $transaction->error_message =~ /$_/ }
948                    $conf->config('emaildecline-exclude', $self->agentnum)
949     ) {
950
951       # Send a decline alert to the customer.
952       my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
953       my $error = '';
954       if ( $msgnum ) {
955         # include the raw error message in the transaction state
956         $cust_pay_pending->setfield('error', $transaction->error_message);
957         my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
958         $error = $msg_template->send( 'cust_main' => $self,
959                                       'object'    => $cust_pay_pending );
960       }
961       else { #!$msgnum
962
963         my @templ = $conf->config('declinetemplate');
964         my $template = new Text::Template (
965           TYPE   => 'ARRAY',
966           SOURCE => [ map "$_\n", @templ ],
967         ) or return "($perror) can't create template: $Text::Template::ERROR";
968         $template->compile()
969           or return "($perror) can't compile template: $Text::Template::ERROR";
970
971         my $templ_hash = {
972           'company_name'    =>
973             scalar( $conf->config('company_name', $self->agentnum ) ),
974           'company_address' =>
975             join("\n", $conf->config('company_address', $self->agentnum ) ),
976           'error'           => $transaction->error_message,
977         };
978
979         my $error = send_email(
980           'from'    => $conf->config('invoice_from', $self->agentnum ),
981           'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
982           'subject' => 'Your payment could not be processed',
983           'body'    => [ $template->fill_in(HASH => $templ_hash) ],
984         );
985       }
986
987       $perror .= " (also received error sending decline notification: $error)"
988         if $error;
989
990     }
991
992     $cust_pay_pending->status('done');
993     $cust_pay_pending->statustext("declined: $perror");
994     my $cpp_done_err = $cust_pay_pending->replace;
995     if ( $cpp_done_err ) {
996       my $e = "WARNING: $options{method} declined but pending payment not ".
997               "resolved - error updating status for paypendingnum ".
998               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
999       warn $e;
1000       $perror = "$e ($perror)";
1001     }
1002
1003     return $perror;
1004   }
1005
1006 }
1007
1008 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1009
1010 Verifies successful third party processing of a realtime credit card,
1011 ACH (electronic check) or phone bill transaction via a
1012 Business::OnlineThirdPartyPayment realtime gateway.  See
1013 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1014
1015 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1016
1017 The additional options I<payname>, I<city>, I<state>,
1018 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1019 if set, will override the value from the customer record.
1020
1021 I<description> is a free-text field passed to the gateway.  It defaults to
1022 "Internet services".
1023
1024 If an I<invnum> is specified, this payment (if successful) is applied to the
1025 specified invoice.  If you don't specify an I<invnum> you might want to
1026 call the B<apply_payments> method.
1027
1028 I<quiet> can be set true to surpress email decline notices.
1029
1030 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
1031 resulting paynum, if any.
1032
1033 I<payunique> is a unique identifier for this payment.
1034
1035 Returns a hashref containing elements bill_error (which will be undefined
1036 upon success) and session_id of any associated session.
1037
1038 =cut
1039
1040 sub realtime_botpp_capture {
1041   my( $self, $cust_pay_pending, %options ) = @_;
1042
1043   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1044
1045   if ( $DEBUG ) {
1046     warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1047     warn "  $_ => $options{$_}\n" foreach keys %options;
1048   }
1049
1050   eval "use Business::OnlineThirdPartyPayment";  
1051   die $@ if $@;
1052
1053   ###
1054   # select the gateway
1055   ###
1056
1057   my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1058
1059   my $payment_gateway;
1060   my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1061   $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1062                 { gatewaynum => $gatewaynum }
1063               )
1064     : $self->agent->payment_gateway( 'method' => $method,
1065                                      # 'invnum'  => $cust_pay_pending->invnum,
1066                                      # 'payinfo' => $cust_pay_pending->payinfo,
1067                                    );
1068
1069   $options{payment_gateway} = $payment_gateway; # for the helper subs
1070
1071   ###
1072   # massage data
1073   ###
1074
1075   my @invoicing_list = $self->invoicing_list_emailonly;
1076   if ( $conf->exists('emailinvoiceautoalways')
1077        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1078        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1079     push @invoicing_list, $self->all_emails;
1080   }
1081
1082   my $email = ($conf->exists('business-onlinepayment-email-override'))
1083               ? $conf->config('business-onlinepayment-email-override')
1084               : $invoicing_list[0];
1085
1086   my %content = ();
1087
1088   $content{email_customer} = 
1089     (    $conf->exists('business-onlinepayment-email_customer')
1090       || $conf->exists('business-onlinepayment-email-override') );
1091       
1092   ###
1093   # run transaction(s)
1094   ###
1095
1096   my $transaction =
1097     new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1098                                            $self->_bop_options(\%options),
1099                                          );
1100
1101   $transaction->reference({ %options }); 
1102
1103   $transaction->content(
1104     'type'           => $method,
1105     $self->_bop_auth(\%options),
1106     'action'         => 'Post Authorization',
1107     'description'    => $options{'description'},
1108     'amount'         => $cust_pay_pending->paid,
1109     #'invoice_number' => $options{'invnum'},
1110     'customer_id'    => $self->custnum,
1111     'referer'        => 'http://cleanwhisker.420.am/',
1112     'reference'      => $cust_pay_pending->paypendingnum,
1113     'email'          => $email,
1114     'phone'          => $self->daytime || $self->night,
1115     %content, #after
1116     # plus whatever is required for bogus capture avoidance
1117   );
1118
1119   $transaction->submit();
1120
1121   my $error =
1122     $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1123
1124   if ( $options{'apply'} ) {
1125     my $apply_error = $self->apply_payments_and_credits;
1126     if ( $apply_error ) {
1127       warn "WARNING: error applying payment: $apply_error\n";
1128     }
1129   }
1130
1131   return {
1132     bill_error => $error,
1133     session_id => $cust_pay_pending->session_id,
1134   }
1135
1136 }
1137
1138 =item default_payment_gateway
1139
1140 DEPRECATED -- use agent->payment_gateway
1141
1142 =cut
1143
1144 sub default_payment_gateway {
1145   my( $self, $method ) = @_;
1146
1147   die "Real-time processing not enabled\n"
1148     unless $conf->exists('business-onlinepayment');
1149
1150   #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1151
1152   #load up config
1153   my $bop_config = 'business-onlinepayment';
1154   $bop_config .= '-ach'
1155     if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1156   my ( $processor, $login, $password, $action, @bop_options ) =
1157     $conf->config($bop_config);
1158   $action ||= 'normal authorization';
1159   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1160   die "No real-time processor is enabled - ".
1161       "did you set the business-onlinepayment configuration value?\n"
1162     unless $processor;
1163
1164   ( $processor, $login, $password, $action, @bop_options )
1165 }
1166
1167 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1168
1169 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1170 via a Business::OnlinePayment realtime gateway.  See
1171 L<http://420.am/business-onlinepayment> for supported gateways.
1172
1173 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1174
1175 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1176
1177 Most gateways require a reference to an original payment transaction to refund,
1178 so you probably need to specify a I<paynum>.
1179
1180 I<amount> defaults to the original amount of the payment if not specified.
1181
1182 I<reason> specifies a reason for the refund.
1183
1184 I<paydate> specifies the expiration date for a credit card overriding the
1185 value from the customer record or the payment record. Specified as yyyy-mm-dd
1186
1187 Implementation note: If I<amount> is unspecified or equal to the amount of the
1188 orignal payment, first an attempt is made to "void" the transaction via
1189 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1190 the normal attempt is made to "refund" ("credit") the transaction via the
1191 gateway is attempted.
1192
1193 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1194 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1195 #if set, will override the value from the customer record.
1196
1197 #If an I<invnum> is specified, this payment (if successful) is applied to the
1198 #specified invoice.  If you don't specify an I<invnum> you might want to
1199 #call the B<apply_payments> method.
1200
1201 =cut
1202
1203 #some false laziness w/realtime_bop, not enough to make it worth merging
1204 #but some useful small subs should be pulled out
1205 sub realtime_refund_bop {
1206   my $self = shift;
1207
1208   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1209
1210   my %options = ();
1211   if (ref($_[0]) eq 'HASH') {
1212     %options = %{$_[0]};
1213   } else {
1214     my $method = shift;
1215     %options = @_;
1216     $options{method} = $method;
1217   }
1218
1219   if ( $DEBUG ) {
1220     warn "$me realtime_refund_bop (new): $options{method} refund\n";
1221     warn "  $_ => $options{$_}\n" foreach keys %options;
1222   }
1223
1224   ###
1225   # look up the original payment and optionally a gateway for that payment
1226   ###
1227
1228   my $cust_pay = '';
1229   my $amount = $options{'amount'};
1230
1231   my( $processor, $login, $password, @bop_options, $namespace ) ;
1232   my( $auth, $order_number ) = ( '', '', '' );
1233
1234   if ( $options{'paynum'} ) {
1235
1236     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
1237     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1238       or return "Unknown paynum $options{'paynum'}";
1239     $amount ||= $cust_pay->paid;
1240
1241     $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1242       or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1243                 $cust_pay->paybatch;
1244     my $gatewaynum = '';
1245     ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1246
1247     if ( $gatewaynum ) { #gateway for the payment to be refunded
1248
1249       my $payment_gateway =
1250         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1251       die "payment gateway $gatewaynum not found"
1252         unless $payment_gateway;
1253
1254       $processor   = $payment_gateway->gateway_module;
1255       $login       = $payment_gateway->gateway_username;
1256       $password    = $payment_gateway->gateway_password;
1257       $namespace   = $payment_gateway->gateway_namespace;
1258       @bop_options = $payment_gateway->options;
1259
1260     } else { #try the default gateway
1261
1262       my $conf_processor;
1263       my $payment_gateway =
1264         $self->agent->payment_gateway('method' => $options{method});
1265
1266       ( $conf_processor, $login, $password, $namespace ) =
1267         map { my $method = "gateway_$_"; $payment_gateway->$method }
1268           qw( module username password namespace );
1269
1270       @bop_options = $payment_gateway->gatewaynum
1271                        ? $payment_gateway->options
1272                        : @{ $payment_gateway->get('options') };
1273
1274       return "processor of payment $options{'paynum'} $processor does not".
1275              " match default processor $conf_processor"
1276         unless $processor eq $conf_processor;
1277
1278     }
1279
1280
1281   } else { # didn't specify a paynum, so look for agent gateway overrides
1282            # like a normal transaction 
1283  
1284     my $payment_gateway =
1285       $self->agent->payment_gateway( 'method'  => $options{method},
1286                                      #'payinfo' => $payinfo,
1287                                    );
1288     my( $processor, $login, $password, $namespace ) =
1289       map { my $method = "gateway_$_"; $payment_gateway->$method }
1290         qw( module username password namespace );
1291
1292     my @bop_options = $payment_gateway->gatewaynum
1293                         ? $payment_gateway->options
1294                         : @{ $payment_gateway->get('options') };
1295
1296   }
1297   return "neither amount nor paynum specified" unless $amount;
1298
1299   eval "use $namespace";  
1300   die $@ if $@;
1301
1302   my %content = (
1303     'type'           => $options{method},
1304     'login'          => $login,
1305     'password'       => $password,
1306     'order_number'   => $order_number,
1307     'amount'         => $amount,
1308     'referer'        => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1309   );
1310   $content{authorization} = $auth
1311     if length($auth); #echeck/ACH transactions have an order # but no auth
1312                       #(at least with authorize.net)
1313
1314   my $disable_void_after;
1315   if ($conf->exists('disable_void_after')
1316       && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1317     $disable_void_after = $1;
1318   }
1319
1320   #first try void if applicable
1321   if ( $cust_pay && $cust_pay->paid == $amount
1322     && (
1323       ( not defined($disable_void_after) )
1324       || ( time < ($cust_pay->_date + $disable_void_after ) )
1325     )
1326   ) {
1327     warn "  attempting void\n" if $DEBUG > 1;
1328     my $void = new Business::OnlinePayment( $processor, @bop_options );
1329     if ( $void->can('info') ) {
1330       if ( $cust_pay->payby eq 'CARD'
1331            && $void->info('CC_void_requires_card') )
1332       {
1333         $content{'card_number'} = $cust_pay->payinfo;
1334       } elsif ( $cust_pay->payby eq 'CHEK'
1335                 && $void->info('ECHECK_void_requires_account') )
1336       {
1337         ( $content{'account_number'}, $content{'routing_code'} ) =
1338           split('@', $cust_pay->payinfo);
1339         $content{'name'} = $self->get('first'). ' '. $self->get('last');
1340       }
1341     }
1342     $void->content( 'action' => 'void', %content );
1343     $void->test_transaction(1)
1344       if $conf->exists('business-onlinepayment-test_transaction');
1345     $void->submit();
1346     if ( $void->is_success ) {
1347       my $error = $cust_pay->void($options{'reason'});
1348       if ( $error ) {
1349         # gah, even with transactions.
1350         my $e = 'WARNING: Card/ACH voided but database not updated - '.
1351                 "error voiding payment: $error";
1352         warn $e;
1353         return $e;
1354       }
1355       warn "  void successful\n" if $DEBUG > 1;
1356       return '';
1357     }
1358   }
1359
1360   warn "  void unsuccessful, trying refund\n"
1361     if $DEBUG > 1;
1362
1363   #massage data
1364   my $address = $self->address1;
1365   $address .= ", ". $self->address2 if $self->address2;
1366
1367   my($payname, $payfirst, $paylast);
1368   if ( $self->payname && $options{method} ne 'ECHECK' ) {
1369     $payname = $self->payname;
1370     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1371       or return "Illegal payname $payname";
1372     ($payfirst, $paylast) = ($1, $2);
1373   } else {
1374     $payfirst = $self->getfield('first');
1375     $paylast = $self->getfield('last');
1376     $payname =  "$payfirst $paylast";
1377   }
1378
1379   my @invoicing_list = $self->invoicing_list_emailonly;
1380   if ( $conf->exists('emailinvoiceautoalways')
1381        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1382        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1383     push @invoicing_list, $self->all_emails;
1384   }
1385
1386   my $email = ($conf->exists('business-onlinepayment-email-override'))
1387               ? $conf->config('business-onlinepayment-email-override')
1388               : $invoicing_list[0];
1389
1390   my $payip = exists($options{'payip'})
1391                 ? $options{'payip'}
1392                 : $self->payip;
1393   $content{customer_ip} = $payip
1394     if length($payip);
1395
1396   my $payinfo = '';
1397   if ( $options{method} eq 'CC' ) {
1398
1399     if ( $cust_pay ) {
1400       $content{card_number} = $payinfo = $cust_pay->payinfo;
1401       (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1402         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1403         ($content{expiration} = "$2/$1");  # where available
1404     } else {
1405       $content{card_number} = $payinfo = $self->payinfo;
1406       (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1407         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1408       $content{expiration} = "$2/$1";
1409     }
1410
1411   } elsif ( $options{method} eq 'ECHECK' ) {
1412
1413     if ( $cust_pay ) {
1414       $payinfo = $cust_pay->payinfo;
1415     } else {
1416       $payinfo = $self->payinfo;
1417     } 
1418     ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1419     $content{bank_name} = $self->payname;
1420     $content{account_type} = 'CHECKING';
1421     $content{account_name} = $payname;
1422     $content{customer_org} = $self->company ? 'B' : 'I';
1423     $content{customer_ssn} = $self->ss;
1424   } elsif ( $options{method} eq 'LEC' ) {
1425     $content{phone} = $payinfo = $self->payinfo;
1426   }
1427
1428   #then try refund
1429   my $refund = new Business::OnlinePayment( $processor, @bop_options );
1430   my %sub_content = $refund->content(
1431     'action'         => 'credit',
1432     'customer_id'    => $self->custnum,
1433     'last_name'      => $paylast,
1434     'first_name'     => $payfirst,
1435     'name'           => $payname,
1436     'address'        => $address,
1437     'city'           => $self->city,
1438     'state'          => $self->state,
1439     'zip'            => $self->zip,
1440     'country'        => $self->country,
1441     'email'          => $email,
1442     'phone'          => $self->daytime || $self->night,
1443     %content, #after
1444   );
1445   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
1446     if $DEBUG > 1;
1447   $refund->test_transaction(1)
1448     if $conf->exists('business-onlinepayment-test_transaction');
1449   $refund->submit();
1450
1451   return "$processor error: ". $refund->error_message
1452     unless $refund->is_success();
1453
1454   my $paybatch = "$processor:". $refund->authorization;
1455   $paybatch .= ':'. $refund->order_number
1456     if $refund->can('order_number') && $refund->order_number;
1457
1458   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1459     my @cust_bill_pay = $cust_pay->cust_bill_pay;
1460     last unless @cust_bill_pay;
1461     my $cust_bill_pay = pop @cust_bill_pay;
1462     my $error = $cust_bill_pay->delete;
1463     last if $error;
1464   }
1465
1466   my $cust_refund = new FS::cust_refund ( {
1467     'custnum'  => $self->custnum,
1468     'paynum'   => $options{'paynum'},
1469     'refund'   => $amount,
1470     '_date'    => '',
1471     'payby'    => $bop_method2payby{$options{method}},
1472     'payinfo'  => $payinfo,
1473     'paybatch' => $paybatch,
1474     'reason'   => $options{'reason'} || 'card or ACH refund',
1475   } );
1476   my $error = $cust_refund->insert;
1477   if ( $error ) {
1478     $cust_refund->paynum(''); #try again with no specific paynum
1479     my $error2 = $cust_refund->insert;
1480     if ( $error2 ) {
1481       # gah, even with transactions.
1482       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1483               "error inserting refund ($processor): $error2".
1484               " (previously tried insert with paynum #$options{'paynum'}" .
1485               ": $error )";
1486       warn $e;
1487       return $e;
1488     }
1489   }
1490
1491   ''; #no error
1492
1493 }
1494
1495 =back
1496
1497 =head1 BUGS
1498
1499 Not autoloaded.
1500
1501 =head1 SEE ALSO
1502
1503 L<FS::cust_main>, L<FS::cust_main::Billing>
1504
1505 =cut
1506
1507 1;