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