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