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