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