RT# 76093 - Added ability to charge a processing fee when taking a payment on the...
[freeside.git] / FS / FS / cust_main / Billing_Realtime.pm
1 package FS::cust_main::Billing_Realtime;
2
3 use strict;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
6 use Data::Dumper;
7 use Business::CreditCard 0.35;
8 use Business::OnlinePayment;
9 use FS::UID qw( dbh myconnect );
10 use FS::Record qw( qsearch qsearchs );
11 use FS::Misc qw( send_email );
12 use FS::payby;
13 use FS::cust_pay;
14 use FS::cust_pay_pending;
15 use FS::cust_bill_pay;
16 use FS::cust_refund;
17 use FS::banned_pay;
18
19 $realtime_bop_decline_quiet = 0;
20
21 # 1 is mostly method/subroutine entry and options
22 # 2 traces progress of some operations
23 # 3 is even more information including possibly sensitive data
24 $DEBUG = 0;
25 $me = '[FS::cust_main::Billing_Realtime]';
26
27 our $BOP_TESTING = 0;
28 our $BOP_TESTING_SUCCESS = 1;
29
30 install_callback FS::UID sub { 
31   $conf = new FS::Conf;
32   #yes, need it for stuff below (prolly should be cached)
33 };
34
35 =head1 NAME
36
37 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
38
39 =head1 SYNOPSIS
40
41 =head1 DESCRIPTION
42
43 These methods are available on FS::cust_main objects.
44
45 =head1 METHODS
46
47 =over 4
48
49 =item realtime_collect [ OPTION => VALUE ... ]
50
51 Attempt to collect the customer's current balance with a realtime credit 
52 card or electronic check transaction (see realtime_bop() below).
53
54 Returns the result of realtime_bop(): nothing, an error message, or a 
55 hashref of state information for a third-party transaction.
56
57 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
58
59 I<method> is one of: I<CC> or I<ECHECK>.  If none is specified
60 then it is deduced from the customer record.
61
62 If no I<amount> is specified, then the customer balance is used.
63
64 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
65 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
66 if set, will override the value from the customer record.
67
68 I<description> is a free-text field passed to the gateway.  It defaults to
69 the value defined by the business-onlinepayment-description configuration
70 option, or "Internet services" if that is unset.
71
72 If an I<invnum> is specified, this payment (if successful) is applied to the
73 specified invoice.
74
75 I<apply> will automatically apply a resulting payment.
76
77 I<quiet> can be set true to suppress email decline notices.
78
79 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
80 resulting paynum, if any.
81
82 I<payunique> is a unique identifier for this payment.
83
84 I<session_id> is a session identifier associated with this payment.
85
86 I<depend_jobnum> allows payment capture to unlock export jobs
87
88 =cut
89
90 sub realtime_collect {
91   my( $self, %options ) = @_;
92
93   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
94
95   if ( $DEBUG ) {
96     warn "$me realtime_collect:\n";
97     warn "  $_ => $options{$_}\n" foreach keys %options;
98   }
99
100   $options{amount} = $self->balance unless exists( $options{amount} );
101   return '' unless $options{amount} > 0;
102
103   $options{method} = FS::payby->payby2bop($self->payby)
104     unless exists( $options{method} );
105
106   return $self->realtime_bop({%options});
107
108 }
109
110 =item realtime_bop { [ ARG => VALUE ... ] }
111
112 Runs a realtime credit card or ACH (electronic check) transaction
113 via a Business::OnlinePayment realtime gateway.  See
114 L<http://420.am/business-onlinepayment> for supported gateways.
115
116 Required arguments in the hashref are I<method>, and I<amount>
117
118 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
119
120 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
121
122 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
123 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
124 if set, will override the value from the customer record.
125
126 I<description> is a free-text field passed to the gateway.  It defaults to
127 the value defined by the business-onlinepayment-description configuration
128 option, or "Internet services" if that is unset.
129
130 If an I<invnum> is specified, this payment (if successful) is applied to the
131 specified invoice.  If the customer has exactly one open invoice, that 
132 invoice number will be assumed.  If you don't specify an I<invnum> you might 
133 want to call the B<apply_payments> method or set the I<apply> option.
134
135 I<no_invnum> can be set to true to prevent that default invnum from being set.
136
137 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
138
139 I<no_auto_apply> can be set to true to set that flag on the resulting payment
140 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
141 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
142
143 I<quiet> can be set true to surpress email decline notices.
144
145 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
146 resulting paynum, if any.
147
148 I<payunique> is a unique identifier for this payment.
149
150 I<session_id> is a session identifier associated with this payment.
151
152 I<depend_jobnum> allows payment capture to unlock export jobs
153
154 I<discount_term> attempts to take a discount by prepaying for discount_term.
155 The payment will fail if I<amount> is incorrect for this discount term.
156
157 A direct (Business::OnlinePayment) transaction will return nothing on success,
158 or an error message on failure.
159
160 A third-party transaction will return a hashref containing:
161
162 - popup_url: the URL to which a browser should be redirected to complete 
163   the transaction.
164 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
165 - reference: a reference ID for the transaction, to show the customer.
166
167 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
168
169 =cut
170
171 # some helper routines
172 sub _bop_recurring_billing {
173   my( $self, %opt ) = @_;
174
175   my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
176
177   if ( defined($method) && $method eq 'transaction_is_recur' ) {
178
179     return 1 if $opt{'trans_is_recur'};
180
181   } else {
182
183     # return 1 if the payinfo has been used for another payment
184     return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
185
186   }
187
188   return 0;
189
190 }
191
192 sub _payment_gateway {
193   my ($self, $options) = @_;
194
195   if ( $options->{'selfservice'} ) {
196     my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
197     if ( $gatewaynum ) {
198       return $options->{payment_gateway} ||= 
199           qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
200     }
201   }
202
203   if ( $options->{'fake_gatewaynum'} ) {
204         $options->{payment_gateway} =
205             qsearchs('payment_gateway',
206                       { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
207                     );
208   }
209
210   $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
211     unless exists($options->{payment_gateway});
212
213   $options->{payment_gateway};
214 }
215
216 sub _bop_auth {
217   my ($self, $options) = @_;
218
219   (
220     'login'    => $options->{payment_gateway}->gateway_username,
221     'password' => $options->{payment_gateway}->gateway_password,
222   );
223 }
224
225 sub _bop_options {
226   my ($self, $options) = @_;
227
228   $options->{payment_gateway}->gatewaynum
229     ? $options->{payment_gateway}->options
230     : @{ $options->{payment_gateway}->get('options') };
231
232 }
233
234 sub _bop_defaults {
235   my ($self, $options) = @_;
236
237   unless ( $options->{'description'} ) {
238     if ( $conf->exists('business-onlinepayment-description') ) {
239       my $dtempl = $conf->config('business-onlinepayment-description');
240
241       my $agent = $self->agent->agent;
242       #$pkgs... not here
243       $options->{'description'} = eval qq("$dtempl");
244     } else {
245       $options->{'description'} = 'Internet services';
246     }
247   }
248
249   unless ( exists( $options->{'payinfo'} ) ) {
250     $options->{'payinfo'} = $self->payinfo;
251     $options->{'paymask'} = $self->paymask;
252   }
253
254   # Default invoice number if the customer has exactly one open invoice.
255   unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
256     $options->{'invnum'} = '';
257     my @open = $self->open_cust_bill;
258     $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
259   }
260
261   $options->{payname} = $self->payname unless exists( $options->{payname} );
262 }
263
264 sub _bop_content {
265   my ($self, $options) = @_;
266   my %content = ();
267
268   my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
269   $content{customer_ip} = $payip if length($payip);
270
271   $content{invoice_number} = $options->{'invnum'}
272     if exists($options->{'invnum'}) && length($options->{'invnum'});
273
274   $content{email_customer} = 
275     (    $conf->exists('business-onlinepayment-email_customer')
276       || $conf->exists('business-onlinepayment-email-override') );
277       
278   my ($payname, $payfirst, $paylast);
279   if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
280     ($payname = $options->{payname}) =~
281       /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
282       or return "Illegal payname $payname";
283     ($payfirst, $paylast) = ($1, $2);
284   } else {
285     $payfirst = $self->getfield('first');
286     $paylast = $self->getfield('last');
287     $payname = "$payfirst $paylast";
288   }
289
290   $content{last_name} = $paylast;
291   $content{first_name} = $payfirst;
292
293   $content{name} = $payname;
294
295   $content{address} = exists($options->{'address1'})
296                         ? $options->{'address1'}
297                         : $self->address1;
298   my $address2 = exists($options->{'address2'})
299                    ? $options->{'address2'}
300                    : $self->address2;
301   $content{address} .= ", ". $address2 if length($address2);
302
303   $content{city} = exists($options->{city})
304                      ? $options->{city}
305                      : $self->city;
306   $content{state} = exists($options->{state})
307                       ? $options->{state}
308                       : $self->state;
309   $content{zip} = exists($options->{zip})
310                     ? $options->{'zip'}
311                     : $self->zip;
312   $content{country} = exists($options->{country})
313                         ? $options->{country}
314                         : $self->country;
315
316   #3.0 is a good a time as any to get rid of this... add a config to pass it
317   # if anyone still needs it
318   #$content{referer} = 'http://cleanwhisker.420.am/';
319
320   $content{phone} = $self->daytime || $self->night;
321
322   my $currency =    $conf->exists('business-onlinepayment-currency')
323                  && $conf->config('business-onlinepayment-currency');
324   $content{currency} = $currency if $currency;
325
326   \%content;
327 }
328
329 my %bop_method2payby = (
330   'CC'     => 'CARD',
331   'ECHECK' => 'CHEK',
332   'PAYPAL' => 'PPAL',
333 );
334
335 sub realtime_bop {
336   my $self = shift;
337
338   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
339  
340   my %options = ();
341   if (ref($_[0]) eq 'HASH') {
342     %options = %{$_[0]};
343   } else {
344     my ( $method, $amount ) = ( shift, shift );
345     %options = @_;
346     $options{method} = $method;
347     $options{amount} = $amount;
348   }
349
350
351   ### 
352   # optional credit card surcharge
353   ###
354
355   my $cc_surcharge = 0;
356   my $cc_surcharge_pct = 0;
357   $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum) 
358     if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
359     && $options{method} eq 'CC';
360
361   my $cc_surcharge_flat = 0;
362   $cc_surcharge_flat = $conf->config('credit-card-surcharge-flatfee', $self->agentnum)
363     if $conf->config('credit-card-surcharge-flatfee', $self->agentnum)
364     && $options{method} eq 'CC';
365
366   # always add cc surcharge if called from event 
367   if($options{'cc_surcharge_from_event'} && ($cc_surcharge_pct > 0 || $cc_surcharge_flat > 0)) {
368     if ($options{'amount'} > 0) {
369       $cc_surcharge = ($options{'amount'} * ($cc_surcharge_pct / 100)) + $cc_surcharge_flat;
370       $options{'amount'} += $cc_surcharge;
371       $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
372     }
373   }
374   elsif($cc_surcharge_pct > 0 || $cc_surcharge_flat > 0) {
375     # we're called not from event (i.e. from a
376     # payment screen), so consider the given
377                 # amount as post-surcharge-processing_fee
378     $cc_surcharge = $options{'amount'} - $options{'processing-fee'} - (($options{'amount'} - ($cc_surcharge_flat + $options{'processing-fee'})) / ( 1 + $cc_surcharge_pct/100 )) if $options{'amount'} > 0;
379   }
380   
381   $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
382   $options{'cc_surcharge'} = $cc_surcharge;
383
384
385   if ( $DEBUG ) {
386     warn "$me realtime_bop (new): $options{method} $options{amount}\n";
387     warn " cc_surcharge = $cc_surcharge\n";
388   }
389   if ( $DEBUG > 2 ) {
390     warn "  $_ => $options{$_}\n" foreach keys %options;
391   }
392
393   return $self->fake_bop(\%options) if $options{'fake'};
394
395   $self->_bop_defaults(\%options);
396
397   ###
398   # set trans_is_recur based on invnum if there is one
399   ###
400
401   my $trans_is_recur = 0;
402   if ( $options{'invnum'} ) {
403
404     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
405     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
406
407     my @part_pkg =
408       map  { $_->part_pkg }
409       grep { $_ }
410       map  { $_->cust_pkg }
411       $cust_bill->cust_bill_pkg;
412
413     $trans_is_recur = 1
414       if grep { $_->freq ne '0' } @part_pkg;
415
416   }
417
418   ###
419   # select a gateway
420   ###
421
422   my $payment_gateway =  $self->_payment_gateway( \%options );
423   my $namespace = $payment_gateway->gateway_namespace;
424
425   eval "use $namespace";  
426   die $@ if $@;
427
428   ###
429   # check for banned credit card/ACH
430   ###
431
432   my $ban = FS::banned_pay->ban_search(
433     'payby'   => $bop_method2payby{$options{method}},
434     'payinfo' => $options{payinfo},
435   );
436   return "Banned credit card" if $ban && $ban->bantype ne 'warn';
437
438   ###
439   # check for term discount validity
440   ###
441
442   my $discount_term = $options{discount_term};
443   if ( $discount_term ) {
444     my $bill = ($self->cust_bill)[-1]
445       or return "Can't apply a term discount to an unbilled customer";
446     my $plan = FS::discount_plan->new(
447       cust_bill => $bill,
448       months    => $discount_term
449     ) or return "No discount available for term '$discount_term'";
450     
451     if ( $plan->discounted_total != $options{amount} ) {
452       return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
453     }
454   }
455
456   ###
457   # massage data
458   ###
459
460   my $bop_content = $self->_bop_content(\%options);
461   return $bop_content unless ref($bop_content);
462
463   my @invoicing_list = $self->invoicing_list_emailonly;
464   if ( $conf->exists('emailinvoiceautoalways')
465        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
466        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
467     push @invoicing_list, $self->all_emails;
468   }
469
470   my $email = ($conf->exists('business-onlinepayment-email-override'))
471               ? $conf->config('business-onlinepayment-email-override')
472               : $invoicing_list[0];
473
474   my $paydate = '';
475   my %content = ();
476
477   if ( $namespace eq 'Business::OnlinePayment' ) {
478
479     if ( $options{method} eq 'CC' ) {
480
481       $content{card_number} = $options{payinfo};
482       $paydate = exists($options{'paydate'})
483                       ? $options{'paydate'}
484                       : $self->paydate;
485       $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
486       $content{expiration} = "$2/$1";
487
488       my $paycvv = exists($options{'paycvv'})
489                      ? $options{'paycvv'}
490                      : $self->paycvv;
491       $content{cvv2} = $paycvv
492         if length($paycvv);
493
494       my $paystart_month = exists($options{'paystart_month'})
495                              ? $options{'paystart_month'}
496                              : $self->paystart_month;
497
498       my $paystart_year  = exists($options{'paystart_year'})
499                              ? $options{'paystart_year'}
500                              : $self->paystart_year;
501
502       $content{card_start} = "$paystart_month/$paystart_year"
503         if $paystart_month && $paystart_year;
504
505       my $payissue       = exists($options{'payissue'})
506                              ? $options{'payissue'}
507                              : $self->payissue;
508       $content{issue_number} = $payissue if $payissue;
509
510       if ( $self->_bop_recurring_billing(
511              'payinfo'        => $options{'payinfo'},
512              'trans_is_recur' => $trans_is_recur,
513            )
514          )
515       {
516         $content{recurring_billing} = 'YES';
517         $content{acct_code} = 'rebill'
518           if $conf->exists('credit_card-recurring_billing_acct_code');
519       }
520
521     } elsif ( $options{method} eq 'ECHECK' ){
522
523       ( $content{account_number}, $content{routing_code} ) =
524         split('@', $options{payinfo});
525       $content{bank_name} = $options{payname};
526       $content{bank_state} = exists($options{'paystate'})
527                                ? $options{'paystate'}
528                                : $self->getfield('paystate');
529       $content{account_type}=
530         (exists($options{'paytype'}) && $options{'paytype'})
531           ? uc($options{'paytype'})
532           : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
533
534       $content{company} = $self->company if $self->company;
535
536       if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
537         $content{account_name} = $self->company;
538       } else {
539         $content{account_name} = $self->getfield('first'). ' '.
540                                  $self->getfield('last');
541       }
542
543       $content{customer_org} = $self->company ? 'B' : 'I';
544       $content{state_id}       = exists($options{'stateid'})
545                                    ? $options{'stateid'}
546                                    : $self->getfield('stateid');
547       $content{state_id_state} = exists($options{'stateid_state'})
548                                    ? $options{'stateid_state'}
549                                    : $self->getfield('stateid_state');
550       $content{customer_ssn} = exists($options{'ss'})
551                                  ? $options{'ss'}
552                                  : $self->ss;
553
554     } else {
555       die "unknown method ". $options{method};
556     }
557
558   } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
559     #move along
560   } else {
561     die "unknown namespace $namespace";
562   }
563
564   ###
565   # run transaction(s)
566   ###
567
568   my $balance = exists( $options{'balance'} )
569                   ? $options{'balance'}
570                   : $self->balance;
571
572   warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
573   $self->select_for_update; #mutex ... just until we get our pending record in
574   warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
575
576   #the checks here are intended to catch concurrent payments
577   #double-form-submission prevention is taken care of in cust_pay_pending::check
578
579   #check the balance
580   return "The customer's balance has changed; $options{method} transaction aborted."
581     if $self->balance < $balance;
582
583   #also check and make sure there aren't *other* pending payments for this cust
584
585   my @pending = qsearch('cust_pay_pending', {
586     'custnum' => $self->custnum,
587     'status'  => { op=>'!=', value=>'done' } 
588   });
589
590   #for third-party payments only, remove pending payments if they're in the 
591   #'thirdparty' (waiting for customer action) state.
592   if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
593     foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
594       my $error = $_->delete;
595       warn "error deleting unfinished third-party payment ".
596           $_->paypendingnum . ": $error\n"
597         if $error;
598     }
599     @pending = grep { $_->status ne 'thirdparty' } @pending;
600   }
601
602   return "A payment is already being processed for this customer (".
603          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
604          "); $options{method} transaction aborted."
605     if scalar(@pending);
606
607   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
608
609   my $cust_pay_pending = new FS::cust_pay_pending {
610     'custnum'           => $self->custnum,
611     'paid'              => $options{amount},
612     '_date'             => '',
613     'payby'             => $bop_method2payby{$options{method}},
614     'payinfo'           => $options{payinfo},
615     'paymask'           => $options{paymask},
616     'paydate'           => $paydate,
617     'recurring_billing' => $content{recurring_billing},
618     'pkgnum'            => $options{'pkgnum'},
619     'status'            => 'new',
620     'gatewaynum'        => $payment_gateway->gatewaynum || '',
621     'session_id'        => $options{session_id} || '',
622     'jobnum'            => $options{depend_jobnum} || '',
623   };
624   $cust_pay_pending->payunique( $options{payunique} )
625     if defined($options{payunique}) && length($options{payunique});
626
627   warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
628     if $DEBUG > 1;
629   my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
630   return $cpp_new_err if $cpp_new_err;
631
632   warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
633     if $DEBUG > 1;
634   warn Dumper($cust_pay_pending) if $DEBUG > 2;
635
636   my( $action1, $action2 ) =
637     split( /\s*\,\s*/, $payment_gateway->gateway_action );
638
639   my $transaction = new $namespace( $payment_gateway->gateway_module,
640                                     $self->_bop_options(\%options),
641                                   );
642
643   $transaction->content(
644     'type'           => $options{method},
645     $self->_bop_auth(\%options),          
646     'action'         => $action1,
647     'description'    => $options{'description'},
648     'amount'         => $options{amount},
649     #'invoice_number' => $options{'invnum'},
650     'customer_id'    => $self->custnum,
651     %$bop_content,
652     'reference'      => $cust_pay_pending->paypendingnum, #for now
653     'callback_url'   => $payment_gateway->gateway_callback_url,
654     'cancel_url'     => $payment_gateway->gateway_cancel_url,
655     'email'          => $email,
656     %content, #after
657   );
658
659   $cust_pay_pending->status('pending');
660   my $cpp_pending_err = $cust_pay_pending->replace;
661   return $cpp_pending_err if $cpp_pending_err;
662
663   warn Dumper($transaction) if $DEBUG > 2;
664
665   unless ( $BOP_TESTING ) {
666     $transaction->test_transaction(1)
667       if $conf->exists('business-onlinepayment-test_transaction');
668     $transaction->submit();
669   } else {
670     if ( $BOP_TESTING_SUCCESS ) {
671       $transaction->is_success(1);
672       $transaction->authorization('fake auth');
673     } else {
674       $transaction->is_success(0);
675       $transaction->error_message('fake failure');
676     }
677   }
678
679   if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
680
681     $cust_pay_pending->status('thirdparty');
682     my $cpp_err = $cust_pay_pending->replace;
683     return { error => $cpp_err } if $cpp_err;
684     return { reference => $cust_pay_pending->paypendingnum,
685              map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
686
687   } elsif ( $transaction->is_success() && $action2 ) {
688
689     $cust_pay_pending->status('authorized');
690     my $cpp_authorized_err = $cust_pay_pending->replace;
691     return $cpp_authorized_err if $cpp_authorized_err;
692
693     my $auth = $transaction->authorization;
694     my $ordernum = $transaction->can('order_number')
695                    ? $transaction->order_number
696                    : '';
697
698     my $capture =
699       new Business::OnlinePayment( $payment_gateway->gateway_module,
700                                    $self->_bop_options(\%options),
701                                  );
702
703     my %capture = (
704       %content,
705       type           => $options{method},
706       action         => $action2,
707       $self->_bop_auth(\%options),          
708       order_number   => $ordernum,
709       amount         => $options{amount},
710       authorization  => $auth,
711       description    => $options{'description'},
712     );
713
714     foreach my $field (qw( authorization_source_code returned_ACI
715                            transaction_identifier validation_code           
716                            transaction_sequence_num local_transaction_date    
717                            local_transaction_time AVS_result_code          )) {
718       $capture{$field} = $transaction->$field() if $transaction->can($field);
719     }
720
721     $capture->content( %capture );
722
723     $capture->test_transaction(1)
724       if $conf->exists('business-onlinepayment-test_transaction');
725     $capture->submit();
726
727     unless ( $capture->is_success ) {
728       my $e = "Authorization successful but capture failed, custnum #".
729               $self->custnum. ': '.  $capture->result_code.
730               ": ". $capture->error_message;
731       warn $e;
732       return $e;
733     }
734
735   }
736
737   ###
738   # remove paycvv after initial transaction
739   ###
740
741   #false laziness w/misc/process/payment.cgi - check both to make sure working
742   # correctly
743   if ( length($self->paycvv)
744        && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
745   ) {
746     my $error = $self->remove_cvv;
747     if ( $error ) {
748       warn "WARNING: error removing cvv: $error\n";
749     }
750   }
751
752   ###
753   # Tokenize
754   ###
755
756
757   if ( $transaction->can('card_token') && $transaction->card_token ) {
758
759     if ( $options{'payinfo'} eq $self->payinfo ) {
760       $self->payinfo($transaction->card_token);
761       my $error = $self->replace;
762       if ( $error ) {
763         warn "WARNING: error storing token: $error, but proceeding anyway\n";
764       }
765     }
766
767   }
768
769   ###
770   # result handling
771   ###
772
773   $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
774
775 }
776
777 =item fake_bop
778
779 =cut
780
781 sub fake_bop {
782   my $self = shift;
783
784   my %options = ();
785   if (ref($_[0]) eq 'HASH') {
786     %options = %{$_[0]};
787   } else {
788     my ( $method, $amount ) = ( shift, shift );
789     %options = @_;
790     $options{method} = $method;
791     $options{amount} = $amount;
792   }
793   
794   if ( $options{'fake_failure'} ) {
795      return "Error: No error; test failure requested with fake_failure";
796   }
797
798   my $cust_pay = new FS::cust_pay ( {
799      'custnum'  => $self->custnum,
800      'invnum'   => $options{'invnum'},
801      'paid'     => $options{amount},
802      '_date'    => '',
803      'payby'    => $bop_method2payby{$options{method}},
804      #'payinfo'  => $payinfo,
805      'payinfo'  => '4111111111111111',
806      #'paydate'  => $paydate,
807      'paydate'  => '2012-05-01',
808      'processor'      => 'FakeProcessor',
809      'auth'           => '54',
810      'order_number'   => '32',
811   } );
812   $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
813
814   if ( $DEBUG ) {
815       warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
816       warn "  $_ => $options{$_}\n" foreach keys %options;
817   }
818
819   my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
820
821   if ( $error ) {
822     $cust_pay->invnum(''); #try again with no specific invnum
823     my $error2 = $cust_pay->insert( $options{'manual'} ?
824                                     ( 'manual' => 1 ) : ()
825                                   );
826     if ( $error2 ) {
827       # gah, even with transactions.
828       my $e = 'WARNING: Card/ACH debited but database not updated - '.
829               "error inserting (fake!) payment: $error2".
830               " (previously tried insert with invnum #$options{'invnum'}" .
831               ": $error )";
832       warn $e;
833       return $e;
834     }
835   }
836
837   if ( $options{'paynum_ref'} ) {
838     ${ $options{'paynum_ref'} } = $cust_pay->paynum;
839   }
840
841   return ''; #no error
842
843 }
844
845
846 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
847
848 # Wraps up processing of a realtime credit card or ACH (electronic check)
849 # transaction.
850
851 sub _realtime_bop_result {
852   my( $self, $cust_pay_pending, $transaction, %options ) = @_;
853
854   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
855
856   if ( $DEBUG ) {
857     warn "$me _realtime_bop_result: pending transaction ".
858       $cust_pay_pending->paypendingnum. "\n";
859     warn "  $_ => $options{$_}\n" foreach keys %options;
860   }
861
862   my $payment_gateway = $options{payment_gateway}
863     or return "no payment gateway in arguments to _realtime_bop_result";
864
865   $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
866   my $cpp_captured_err = $cust_pay_pending->replace;
867   return $cpp_captured_err if $cpp_captured_err;
868
869   if ( $transaction->is_success() ) {
870
871     my $order_number = $transaction->order_number
872       if $transaction->can('order_number');
873
874     my $cust_pay = new FS::cust_pay ( {
875        'custnum'  => $self->custnum,
876        'invnum'   => $options{'invnum'},
877        'paid'     => $cust_pay_pending->paid,
878        '_date'    => '',
879        'payby'    => $cust_pay_pending->payby,
880        'payinfo'  => $options{'payinfo'},
881        'paymask'  => $options{'paymask'} || $cust_pay_pending->paymask,
882        'paydate'  => $cust_pay_pending->paydate,
883        'pkgnum'   => $cust_pay_pending->pkgnum,
884        'discount_term'  => $options{'discount_term'},
885        'gatewaynum'     => ($payment_gateway->gatewaynum || ''),
886        'processor'      => $payment_gateway->gateway_module,
887        'auth'           => $transaction->authorization,
888        'order_number'   => $order_number || '',
889        'no_auto_apply'  => $options{'no_auto_apply'} ? 'Y' : '',
890     } );
891     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
892     $cust_pay->payunique( $options{payunique} )
893       if defined($options{payunique}) && length($options{payunique});
894
895     my $oldAutoCommit = $FS::UID::AutoCommit;
896     local $FS::UID::AutoCommit = 0;
897     my $dbh = dbh;
898
899     #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
900
901     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
902
903     if ( $error ) {
904       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
905       $cust_pay->invnum(''); #try again with no specific invnum
906       $cust_pay->paynum('');
907       my $error2 = $cust_pay->insert( $options{'manual'} ?
908                                       ( 'manual' => 1 ) : ()
909                                     );
910       if ( $error2 ) {
911         # gah.  but at least we have a record of the state we had to abort in
912         # from cust_pay_pending now.
913         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
914         my $e = "WARNING: $options{method} captured but payment not recorded -".
915                 " error inserting payment (". $payment_gateway->gateway_module.
916                 "): $error2".
917                 " (previously tried insert with invnum #$options{'invnum'}" .
918                 ": $error ) - pending payment saved as paypendingnum ".
919                 $cust_pay_pending->paypendingnum. "\n";
920         warn $e;
921         return $e;
922       }
923     }
924
925     my $jobnum = $cust_pay_pending->jobnum;
926     if ( $jobnum ) {
927        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
928       
929        unless ( $placeholder ) {
930          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
931          my $e = "WARNING: $options{method} captured but job $jobnum not ".
932              "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
933          warn $e;
934          return $e;
935        }
936
937        $error = $placeholder->delete;
938
939        if ( $error ) {
940          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
941          my $e = "WARNING: $options{method} captured but could not delete ".
942               "job $jobnum for paypendingnum ".
943               $cust_pay_pending->paypendingnum. ": $error\n";
944          warn $e;
945          return $e;
946        }
947
948     }
949     
950     if ( $options{'paynum_ref'} ) {
951       ${ $options{'paynum_ref'} } = $cust_pay->paynum;
952     }
953
954     $cust_pay_pending->status('done');
955     $cust_pay_pending->statustext('captured');
956     $cust_pay_pending->paynum($cust_pay->paynum);
957     my $cpp_done_err = $cust_pay_pending->replace;
958
959     if ( $cpp_done_err ) {
960
961       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
962       my $e = "WARNING: $options{method} captured but payment not recorded - ".
963               "error updating status for paypendingnum ".
964               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
965       warn $e;
966       return $e;
967
968     } else {
969
970       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
971
972       if ( $options{'apply'} ) {
973         my $apply_error = $self->apply_payments_and_credits;
974         if ( $apply_error ) {
975           warn "WARNING: error applying payment: $apply_error\n";
976           #but we still should return no error cause the payment otherwise went
977           #through...
978         }
979       }
980
981       # have a CC surcharge portion --> one-time charge
982       if ( $options{'cc_surcharge'} > 0 || $options{'processing-fee'} > 0) {
983             # XXX: this whole block needs to be in a transaction?
984
985           my $invnum;
986           $invnum = $options{'invnum'} if $options{'invnum'};
987           unless ( $invnum ) { # probably from a payment screen
988              # do we have any open invoices? pick earliest
989              # uses the fact that cust_main->cust_bill sorts by date ascending
990              my @open = $self->open_cust_bill;
991              $invnum = $open[0]->invnum if scalar(@open);
992           }
993             
994           unless ( $invnum ) {  # still nothing? pick last closed invoice
995              # again uses fact that cust_main->cust_bill sorts by date ascending
996              my @closed = $self->cust_bill;
997              $invnum = $closed[$#closed]->invnum if scalar(@closed);
998           }
999
1000           unless ( $invnum ) {
1001             # XXX: unlikely case - pre-paying before any invoices generated
1002             # what it should do is create a new invoice and pick it
1003                 warn 'CC SURCHARGE OR PROCESS FEE AND NO INVOICES PICKED TO APPLY IT!';
1004                 return '';
1005           }
1006
1007     if ($options{'cc_surcharge'} > 0) {
1008             my $cust_pkg;
1009       my $cc_surcharge_text = 'Credit Card Surcharge';
1010       $cc_surcharge_text = $conf->config('credit-card-surcharge-text', $self->agentnum) if $conf->exists('credit-card-surcharge-text', $self->agentnum);
1011             my $charge_error = $self->charge({
1012                                     'amount'    => $options{'cc_surcharge'},
1013                                     'pkg'       => $cc_surcharge_text,
1014                                     'setuptax'  => 'Y',
1015                                     'cust_pkg_ref' => \$cust_pkg,
1016                         });
1017
1018             if($charge_error) {
1019                     warn 'Unable to add CC surcharge cust_pkg';
1020                     return '';
1021             }
1022
1023       $cust_pkg->setup(time);
1024       my $cp_error = $cust_pkg->replace;
1025       if($cp_error) {
1026         warn 'Unable to set setup time on cust_pkg for cc surcharge';
1027         # but keep going...
1028       }
1029
1030       my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1031       unless ( $cust_bill ) {
1032         warn "race condition + invoice deletion just happened";
1033         return '';
1034       }
1035
1036       my $grand_error =
1037         $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1038
1039       warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1040         if $grand_error;
1041     } # end if $options{'cc_surcharge'}
1042
1043     if ($options{'processing-fee'} > 0) {
1044       my $pf_cust_pkg;
1045       my $processing_fee_text = 'Payment Processing Fee';
1046       my $pf_change_error = $self->charge({
1047             'amount'  => $options{'processing-fee'},
1048             'pkg'   => $processing_fee_text,
1049             'setuptax'  => 'Y',
1050             'cust_pkg_ref' => \$pf_cust_pkg,
1051       });
1052
1053       if($pf_change_error) {
1054         warn 'Unable to add payment processing fee';
1055         return '';
1056       }
1057
1058       $pf_cust_pkg->setup(time);
1059       my $pf_error = $pf_cust_pkg->replace;
1060       if($pf_error) {
1061         warn 'Unable to set setup time on cust_pkg for processing fee';
1062         # but keep going...
1063       }
1064
1065       my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1066       unless ( $cust_bill ) {
1067         warn "race condition + invoice deletion just happened";
1068         return '';
1069       }
1070
1071       my $grand_pf_error =
1072         $cust_bill->add_cc_surcharge($pf_cust_pkg->pkgnum,$options{'processing-fee'});
1073
1074       warn "cannot add Processing fee to invoice #$invnum: $grand_pf_error"
1075         if $grand_pf_error;
1076     } #end if $options{'processing-fee'}
1077
1078       } #end if ( $options{'cc_surcharge'} > 0 || $options{'processing-fee'} > 0)
1079
1080       return ''; #no error
1081
1082     }
1083
1084   } else {
1085
1086     my $perror = $payment_gateway->gateway_module. " error: ".
1087       $transaction->error_message;
1088
1089     my $jobnum = $cust_pay_pending->jobnum;
1090     if ( $jobnum ) {
1091        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1092       
1093        if ( $placeholder ) {
1094          my $error = $placeholder->depended_delete;
1095          $error ||= $placeholder->delete;
1096          warn "error removing provisioning jobs after declined paypendingnum ".
1097            $cust_pay_pending->paypendingnum. ": $error\n";
1098        } else {
1099          my $e = "error finding job $jobnum for declined paypendingnum ".
1100               $cust_pay_pending->paypendingnum. "\n";
1101          warn $e;
1102        }
1103
1104     }
1105     
1106     unless ( $transaction->error_message ) {
1107
1108       my $t_response;
1109       if ( $transaction->can('response_page') ) {
1110         $t_response = {
1111                         'page'    => ( $transaction->can('response_page')
1112                                          ? $transaction->response_page
1113                                          : ''
1114                                      ),
1115                         'code'    => ( $transaction->can('response_code')
1116                                          ? $transaction->response_code
1117                                          : ''
1118                                      ),
1119                         'headers' => ( $transaction->can('response_headers')
1120                                          ? $transaction->response_headers
1121                                          : ''
1122                                      ),
1123                       };
1124       } else {
1125         $t_response .=
1126           "No additional debugging information available for ".
1127             $payment_gateway->gateway_module;
1128       }
1129
1130       $perror .= "No error_message returned from ".
1131                    $payment_gateway->gateway_module. " -- ".
1132                  ( ref($t_response) ? Dumper($t_response) : $t_response );
1133
1134     }
1135
1136     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1137          && $conf->exists('emaildecline', $self->agentnum)
1138          && grep { $_ ne 'POST' } $self->invoicing_list
1139          && ! grep { $transaction->error_message =~ /$_/ }
1140                    $conf->config('emaildecline-exclude', $self->agentnum)
1141     ) {
1142
1143       # Send a decline alert to the customer.
1144       my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1145       my $error = '';
1146       if ( $msgnum ) {
1147         # include the raw error message in the transaction state
1148         $cust_pay_pending->setfield('error', $transaction->error_message);
1149         my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1150         $error = $msg_template->send( 'cust_main' => $self,
1151                                       'object'    => $cust_pay_pending );
1152       }
1153       else { #!$msgnum
1154
1155         my @templ = $conf->config('declinetemplate');
1156         my $template = new Text::Template (
1157           TYPE   => 'ARRAY',
1158           SOURCE => [ map "$_\n", @templ ],
1159         ) or return "($perror) can't create template: $Text::Template::ERROR";
1160         $template->compile()
1161           or return "($perror) can't compile template: $Text::Template::ERROR";
1162
1163         my $templ_hash = {
1164           'company_name'    =>
1165             scalar( $conf->config('company_name', $self->agentnum ) ),
1166           'company_address' =>
1167             join("\n", $conf->config('company_address', $self->agentnum ) ),
1168           'error'           => $transaction->error_message,
1169         };
1170
1171         my $error = send_email(
1172           'from'    => $conf->invoice_from_full( $self->agentnum ),
1173           'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1174           'subject' => 'Your payment could not be processed',
1175           'body'    => [ $template->fill_in(HASH => $templ_hash) ],
1176         );
1177       }
1178
1179       $perror .= " (also received error sending decline notification: $error)"
1180         if $error;
1181
1182     }
1183
1184     $cust_pay_pending->status('done');
1185     $cust_pay_pending->statustext("declined: $perror");
1186     my $cpp_done_err = $cust_pay_pending->replace;
1187     if ( $cpp_done_err ) {
1188       my $e = "WARNING: $options{method} declined but pending payment not ".
1189               "resolved - error updating status for paypendingnum ".
1190               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1191       warn $e;
1192       $perror = "$e ($perror)";
1193     }
1194
1195     return $perror;
1196   }
1197
1198 }
1199
1200 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1201
1202 Verifies successful third party processing of a realtime credit card or
1203 ACH (electronic check) transaction via a
1204 Business::OnlineThirdPartyPayment realtime gateway.  See
1205 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1206
1207 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1208
1209 The additional options I<payname>, I<city>, I<state>,
1210 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1211 if set, will override the value from the customer record.
1212
1213 I<description> is a free-text field passed to the gateway.  It defaults to
1214 "Internet services".
1215
1216 If an I<invnum> is specified, this payment (if successful) is applied to the
1217 specified invoice.  If you don't specify an I<invnum> you might want to
1218 call the B<apply_payments> method.
1219
1220 I<quiet> can be set true to surpress email decline notices.
1221
1222 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
1223 resulting paynum, if any.
1224
1225 I<payunique> is a unique identifier for this payment.
1226
1227 Returns a hashref containing elements bill_error (which will be undefined
1228 upon success) and session_id of any associated session.
1229
1230 =cut
1231
1232 sub realtime_botpp_capture {
1233   my( $self, $cust_pay_pending, %options ) = @_;
1234
1235   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1236
1237   if ( $DEBUG ) {
1238     warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1239     warn "  $_ => $options{$_}\n" foreach keys %options;
1240   }
1241
1242   eval "use Business::OnlineThirdPartyPayment";  
1243   die $@ if $@;
1244
1245   ###
1246   # select the gateway
1247   ###
1248
1249   my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1250
1251   my $payment_gateway;
1252   my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1253   $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1254                 { gatewaynum => $gatewaynum }
1255               )
1256     : $self->agent->payment_gateway( 'method' => $method,
1257                                      # 'invnum'  => $cust_pay_pending->invnum,
1258                                      # 'payinfo' => $cust_pay_pending->payinfo,
1259                                    );
1260
1261   $options{payment_gateway} = $payment_gateway; # for the helper subs
1262
1263   ###
1264   # massage data
1265   ###
1266
1267   my @invoicing_list = $self->invoicing_list_emailonly;
1268   if ( $conf->exists('emailinvoiceautoalways')
1269        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1270        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1271     push @invoicing_list, $self->all_emails;
1272   }
1273
1274   my $email = ($conf->exists('business-onlinepayment-email-override'))
1275               ? $conf->config('business-onlinepayment-email-override')
1276               : $invoicing_list[0];
1277
1278   my %content = ();
1279
1280   $content{email_customer} = 
1281     (    $conf->exists('business-onlinepayment-email_customer')
1282       || $conf->exists('business-onlinepayment-email-override') );
1283       
1284   ###
1285   # run transaction(s)
1286   ###
1287
1288   my $transaction =
1289     new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1290                                            $self->_bop_options(\%options),
1291                                          );
1292
1293   $transaction->reference({ %options }); 
1294
1295   $transaction->content(
1296     'type'           => $method,
1297     $self->_bop_auth(\%options),
1298     'action'         => 'Post Authorization',
1299     'description'    => $options{'description'},
1300     'amount'         => $cust_pay_pending->paid,
1301     #'invoice_number' => $options{'invnum'},
1302     'customer_id'    => $self->custnum,
1303
1304     #3.0 is a good a time as any to get rid of this... add a config to pass it
1305     # if anyone still needs it
1306     #'referer'        => 'http://cleanwhisker.420.am/',
1307
1308     'reference'      => $cust_pay_pending->paypendingnum,
1309     'email'          => $email,
1310     'phone'          => $self->daytime || $self->night,
1311     %content, #after
1312     # plus whatever is required for bogus capture avoidance
1313   );
1314
1315   $transaction->submit();
1316
1317   my $error =
1318     $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1319
1320   if ( $options{'apply'} ) {
1321     my $apply_error = $self->apply_payments_and_credits;
1322     if ( $apply_error ) {
1323       warn "WARNING: error applying payment: $apply_error\n";
1324     }
1325   }
1326
1327   return {
1328     bill_error => $error,
1329     session_id => $cust_pay_pending->session_id,
1330   }
1331
1332 }
1333
1334 =item default_payment_gateway
1335
1336 DEPRECATED -- use agent->payment_gateway
1337
1338 =cut
1339
1340 sub default_payment_gateway {
1341   my( $self, $method ) = @_;
1342
1343   die "Real-time processing not enabled\n"
1344     unless $conf->exists('business-onlinepayment');
1345
1346   #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1347
1348   #load up config
1349   my $bop_config = 'business-onlinepayment';
1350   $bop_config .= '-ach'
1351     if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1352   my ( $processor, $login, $password, $action, @bop_options ) =
1353     $conf->config($bop_config);
1354   $action ||= 'normal authorization';
1355   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1356   die "No real-time processor is enabled - ".
1357       "did you set the business-onlinepayment configuration value?\n"
1358     unless $processor;
1359
1360   ( $processor, $login, $password, $action, @bop_options )
1361 }
1362
1363 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1364
1365 Refunds a realtime credit card or ACH (electronic check) transaction
1366 via a Business::OnlinePayment realtime gateway.  See
1367 L<http://420.am/business-onlinepayment> for supported gateways.
1368
1369 Available methods are: I<CC> or I<ECHECK>
1370
1371 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1372
1373 Most gateways require a reference to an original payment transaction to refund,
1374 so you probably need to specify a I<paynum>.
1375
1376 I<amount> defaults to the original amount of the payment if not specified.
1377
1378 I<reasonnum> specifies a reason for the refund.
1379
1380 I<paydate> specifies the expiration date for a credit card overriding the
1381 value from the customer record or the payment record. Specified as yyyy-mm-dd
1382
1383 Implementation note: If I<amount> is unspecified or equal to the amount of the
1384 orignal payment, first an attempt is made to "void" the transaction via
1385 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1386 the normal attempt is made to "refund" ("credit") the transaction via the
1387 gateway is attempted. No attempt to "void" the transaction is made if the 
1388 gateway has introspection data and doesn't support void.
1389
1390 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1391 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1392 #if set, will override the value from the customer record.
1393
1394 #If an I<invnum> is specified, this payment (if successful) is applied to the
1395 #specified invoice.  If you don't specify an I<invnum> you might want to
1396 #call the B<apply_payments> method.
1397
1398 =cut
1399
1400 #some false laziness w/realtime_bop, not enough to make it worth merging
1401 #but some useful small subs should be pulled out
1402 sub realtime_refund_bop {
1403   my $self = shift;
1404
1405   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1406
1407   my %options = ();
1408   if (ref($_[0]) eq 'HASH') {
1409     %options = %{$_[0]};
1410   } else {
1411     my $method = shift;
1412     %options = @_;
1413     $options{method} = $method;
1414   }
1415
1416   my ($reason, $reason_text);
1417   if ( $options{'reasonnum'} ) {
1418     # do this here, because we need the plain text reason string in case we
1419     # void the payment
1420     $reason = FS::reason->by_key($options{'reasonnum'});
1421     $reason_text = $reason->reason;
1422   } else {
1423     # support old 'reason' string parameter in case it's still used,
1424     # or else set a default
1425     $reason_text = $options{'reason'} || 'card or ACH refund';
1426     local $@;
1427     $reason = FS::reason->new_or_existing(
1428       reason  => $reason_text,
1429       type    => 'Refund reason',
1430       class   => 'F',
1431     );
1432     if ($@) {
1433       return "failed to add refund reason: $@";
1434     }
1435   }
1436
1437   if ( $DEBUG ) {
1438     warn "$me realtime_refund_bop (new): $options{method} refund\n";
1439     warn "  $_ => $options{$_}\n" foreach keys %options;
1440   }
1441
1442   my %content = ();
1443
1444   ###
1445   # look up the original payment and optionally a gateway for that payment
1446   ###
1447
1448   my $cust_pay = '';
1449   my $amount = $options{'amount'};
1450
1451   my( $processor, $login, $password, @bop_options, $namespace ) ;
1452   my( $auth, $order_number ) = ( '', '', '' );
1453   my $gatewaynum = '';
1454
1455   if ( $options{'paynum'} ) {
1456
1457     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
1458     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1459       or return "Unknown paynum $options{'paynum'}";
1460     $amount ||= $cust_pay->paid;
1461
1462     my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1463     $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1464
1465     if ( $cust_pay->get('processor') ) {
1466       ($gatewaynum, $processor, $auth, $order_number) =
1467       (
1468         $cust_pay->gatewaynum,
1469         $cust_pay->processor,
1470         $cust_pay->auth,
1471         $cust_pay->order_number,
1472       );
1473     } else {
1474       # this payment wasn't upgraded, which probably means this won't work,
1475       # but try it anyway
1476       $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1477         or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1478                   $cust_pay->paybatch;
1479       ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1480     }
1481
1482     if ( $gatewaynum ) { #gateway for the payment to be refunded
1483
1484       my $payment_gateway =
1485         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1486       die "payment gateway $gatewaynum not found"
1487         unless $payment_gateway;
1488
1489       $processor   = $payment_gateway->gateway_module;
1490       $login       = $payment_gateway->gateway_username;
1491       $password    = $payment_gateway->gateway_password;
1492       $namespace   = $payment_gateway->gateway_namespace;
1493       @bop_options = $payment_gateway->options;
1494
1495     } else { #try the default gateway
1496
1497       my $conf_processor;
1498       my $payment_gateway =
1499         $self->agent->payment_gateway('method' => $options{method});
1500
1501       ( $conf_processor, $login, $password, $namespace ) =
1502         map { my $method = "gateway_$_"; $payment_gateway->$method }
1503           qw( module username password namespace );
1504
1505       @bop_options = $payment_gateway->gatewaynum
1506                        ? $payment_gateway->options
1507                        : @{ $payment_gateway->get('options') };
1508
1509       return "processor of payment $options{'paynum'} $processor does not".
1510              " match default processor $conf_processor"
1511         unless $processor eq $conf_processor;
1512
1513     }
1514
1515
1516   } else { # didn't specify a paynum, so look for agent gateway overrides
1517            # like a normal transaction 
1518  
1519     my $payment_gateway =
1520       $self->agent->payment_gateway( 'method'  => $options{method},
1521                                      #'payinfo' => $payinfo,
1522                                    );
1523     ( $processor, $login, $password, $namespace ) =
1524       map { my $method = "gateway_$_"; $payment_gateway->$method }
1525         qw( module username password namespace );
1526
1527     my @bop_options = $payment_gateway->gatewaynum
1528                         ? $payment_gateway->options
1529                         : @{ $payment_gateway->get('options') };
1530
1531   }
1532   return "neither amount nor paynum specified" unless $amount;
1533
1534   eval "use $namespace";  
1535   die $@ if $@;
1536
1537   %content = (
1538     %content,
1539     'type'           => $options{method},
1540     'login'          => $login,
1541     'password'       => $password,
1542     'order_number'   => $order_number,
1543     'amount'         => $amount,
1544
1545     #3.0 is a good a time as any to get rid of this... add a config to pass it
1546     # if anyone still needs it
1547     #'referer'        => 'http://cleanwhisker.420.am/',
1548   );
1549   $content{authorization} = $auth
1550     if length($auth); #echeck/ACH transactions have an order # but no auth
1551                       #(at least with authorize.net)
1552
1553   my $currency =    $conf->exists('business-onlinepayment-currency')
1554                  && $conf->config('business-onlinepayment-currency');
1555   $content{currency} = $currency if $currency;
1556
1557   my $disable_void_after;
1558   if ($conf->exists('disable_void_after')
1559       && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1560     $disable_void_after = $1;
1561   }
1562
1563   #first try void if applicable
1564   my $void = new Business::OnlinePayment( $processor, @bop_options );
1565
1566   my $tryvoid = 1;
1567   if ($void->can('info')) {
1568       my $paytype = '';
1569       $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1570       $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1571       my %supported_actions = $void->info('supported_actions');
1572       $tryvoid = 0 
1573         if ( %supported_actions && $paytype 
1574                 && defined($supported_actions{$paytype}) 
1575                 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1576   }
1577
1578   if ( $cust_pay && $cust_pay->paid == $amount
1579     && (
1580       ( not defined($disable_void_after) )
1581       || ( time < ($cust_pay->_date + $disable_void_after ) )
1582     )
1583     && $tryvoid
1584   ) {
1585     warn "  attempting void\n" if $DEBUG > 1;
1586     if ( $void->can('info') ) {
1587       if ( $cust_pay->payby eq 'CARD'
1588            && $void->info('CC_void_requires_card') )
1589       {
1590         $content{'card_number'} = $cust_pay->payinfo;
1591       } elsif ( $cust_pay->payby eq 'CHEK'
1592                 && $void->info('ECHECK_void_requires_account') )
1593       {
1594         ( $content{'account_number'}, $content{'routing_code'} ) =
1595           split('@', $cust_pay->payinfo);
1596         $content{'name'} = $self->get('first'). ' '. $self->get('last');
1597       }
1598     }
1599     $void->content( 'action' => 'void', %content );
1600     $void->test_transaction(1)
1601       if $conf->exists('business-onlinepayment-test_transaction');
1602     $void->submit();
1603     if ( $void->is_success ) {
1604       my $error = $cust_pay->void($reason_text);
1605       if ( $error ) {
1606         # gah, even with transactions.
1607         my $e = 'WARNING: Card/ACH voided but database not updated - '.
1608                 "error voiding payment: $error";
1609         warn $e;
1610         return $e;
1611       }
1612       warn "  void successful\n" if $DEBUG > 1;
1613       return '';
1614     }
1615   }
1616
1617   warn "  void unsuccessful, trying refund\n"
1618     if $DEBUG > 1;
1619
1620   #massage data
1621   my $address = $self->address1;
1622   $address .= ", ". $self->address2 if $self->address2;
1623
1624   my($payname, $payfirst, $paylast);
1625   if ( $self->payname && $options{method} ne 'ECHECK' ) {
1626     $payname = $self->payname;
1627     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1628       or return "Illegal payname $payname";
1629     ($payfirst, $paylast) = ($1, $2);
1630   } else {
1631     $payfirst = $self->getfield('first');
1632     $paylast = $self->getfield('last');
1633     $payname =  "$payfirst $paylast";
1634   }
1635
1636   my @invoicing_list = $self->invoicing_list_emailonly;
1637   if ( $conf->exists('emailinvoiceautoalways')
1638        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1639        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1640     push @invoicing_list, $self->all_emails;
1641   }
1642
1643   my $email = ($conf->exists('business-onlinepayment-email-override'))
1644               ? $conf->config('business-onlinepayment-email-override')
1645               : $invoicing_list[0];
1646
1647   my $payip = exists($options{'payip'})
1648                 ? $options{'payip'}
1649                 : $self->payip;
1650   $content{customer_ip} = $payip
1651     if length($payip);
1652
1653   my $payinfo = '';
1654   if ( $options{method} eq 'CC' ) {
1655
1656     if ( $cust_pay ) {
1657       $content{card_number} = $payinfo = $cust_pay->payinfo;
1658       (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1659         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1660         ($content{expiration} = "$2/$1");  # where available
1661     } else {
1662       $content{card_number} = $payinfo = $self->payinfo;
1663       (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1664         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1665       $content{expiration} = "$2/$1";
1666     }
1667
1668   } elsif ( $options{method} eq 'ECHECK' ) {
1669
1670     if ( $cust_pay ) {
1671       $payinfo = $cust_pay->payinfo;
1672     } else {
1673       $payinfo = $self->payinfo;
1674     } 
1675     ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1676     $content{bank_name} = $self->payname;
1677     $content{account_type} = 'CHECKING';
1678     $content{account_name} = $payname;
1679     $content{customer_org} = $self->company ? 'B' : 'I';
1680     $content{customer_ssn} = $self->ss;
1681
1682   }
1683
1684   #then try refund
1685   my $refund = new Business::OnlinePayment( $processor, @bop_options );
1686   my %sub_content = $refund->content(
1687     'action'         => 'credit',
1688     'customer_id'    => $self->custnum,
1689     'last_name'      => $paylast,
1690     'first_name'     => $payfirst,
1691     'name'           => $payname,
1692     'address'        => $address,
1693     'city'           => $self->city,
1694     'state'          => $self->state,
1695     'zip'            => $self->zip,
1696     'country'        => $self->country,
1697     'email'          => $email,
1698     'phone'          => $self->daytime || $self->night,
1699     %content, #after
1700   );
1701   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
1702     if $DEBUG > 1;
1703   $refund->test_transaction(1)
1704     if $conf->exists('business-onlinepayment-test_transaction');
1705   $refund->submit();
1706
1707   return "$processor error: ". $refund->error_message
1708     unless $refund->is_success();
1709
1710   $order_number = $refund->order_number if $refund->can('order_number');
1711
1712   # change this to just use $cust_pay->delete_cust_bill_pay?
1713   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1714     my @cust_bill_pay = $cust_pay->cust_bill_pay;
1715     last unless @cust_bill_pay;
1716     my $cust_bill_pay = pop @cust_bill_pay;
1717     my $error = $cust_bill_pay->delete;
1718     last if $error;
1719   }
1720
1721   my $cust_refund = new FS::cust_refund ( {
1722     'custnum'  => $self->custnum,
1723     'paynum'   => $options{'paynum'},
1724     'source_paynum' => $options{'paynum'},
1725     'refund'   => $amount,
1726     '_date'    => '',
1727     'payby'    => $bop_method2payby{$options{method}},
1728     'payinfo'  => $payinfo,
1729     'reasonnum'   => $reason->reasonnum,
1730     'gatewaynum'    => $gatewaynum, # may be null
1731     'processor'     => $processor,
1732     'auth'          => $refund->authorization,
1733     'order_number'  => $order_number,
1734   } );
1735   my $error = $cust_refund->insert;
1736   if ( $error ) {
1737     $cust_refund->paynum(''); #try again with no specific paynum
1738     $cust_refund->source_paynum('');
1739     my $error2 = $cust_refund->insert;
1740     if ( $error2 ) {
1741       # gah, even with transactions.
1742       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1743               "error inserting refund ($processor): $error2".
1744               " (previously tried insert with paynum #$options{'paynum'}" .
1745               ": $error )";
1746       warn $e;
1747       return $e;
1748     }
1749   }
1750
1751   ''; #no error
1752
1753 }
1754
1755 =item realtime_verify_bop [ OPTION => VALUE ... ]
1756
1757 Runs an authorization-only transaction for $1 against this credit card (if
1758 successful, immediatly reverses the authorization).
1759
1760 Returns the empty string if the authorization was sucessful, or an error
1761 message otherwise.
1762
1763 I<payinfo>
1764
1765 I<payname>
1766
1767 I<paydate> specifies the expiration date for a credit card overriding the
1768 value from the customer record or the payment record. Specified as yyyy-mm-dd
1769
1770 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1771 #I<zip> are also available.  Any of these options,
1772 #if set, will override the value from the customer record.
1773
1774 =cut
1775
1776 #Available methods are: I<CC> or I<ECHECK>
1777
1778 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1779 #it worth merging but some useful small subs should be pulled out
1780 sub realtime_verify_bop {
1781   my $self = shift;
1782
1783   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1784   my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1785
1786   my %options = ();
1787   if (ref($_[0]) eq 'HASH') {
1788     %options = %{$_[0]};
1789   } else {
1790     %options = @_;
1791   }
1792
1793   if ( $DEBUG ) {
1794     warn "$me realtime_verify_bop\n";
1795     warn "  $_ => $options{$_}\n" foreach keys %options;
1796   }
1797
1798   ###
1799   # select a gateway
1800   ###
1801
1802   my $payment_gateway =  $self->_payment_gateway( \%options );
1803   my $namespace = $payment_gateway->gateway_namespace;
1804
1805   eval "use $namespace";  
1806   die $@ if $@;
1807
1808   ###
1809   # check for banned credit card/ACH
1810   ###
1811
1812   my $ban = FS::banned_pay->ban_search(
1813     'payby'   => $bop_method2payby{'CC'},
1814     'payinfo' => $options{payinfo} || $self->payinfo,
1815   );
1816   return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1817
1818   ###
1819   # massage data
1820   ###
1821
1822   my $bop_content = $self->_bop_content(\%options);
1823   return $bop_content unless ref($bop_content);
1824
1825   my @invoicing_list = $self->invoicing_list_emailonly;
1826   if ( $conf->exists('emailinvoiceautoalways')
1827        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1828        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1829     push @invoicing_list, $self->all_emails;
1830   }
1831
1832   my $email = ($conf->exists('business-onlinepayment-email-override'))
1833               ? $conf->config('business-onlinepayment-email-override')
1834               : $invoicing_list[0];
1835
1836   my $paydate = '';
1837   my %content = ();
1838
1839   if ( $namespace eq 'Business::OnlinePayment' ) {
1840
1841     if ( $options{method} eq 'CC' ) {
1842
1843       $content{card_number} = $options{payinfo} || $self->payinfo;
1844       $paydate = exists($options{'paydate'})
1845                       ? $options{'paydate'}
1846                       : $self->paydate;
1847       $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1848       $content{expiration} = "$2/$1";
1849
1850       my $paycvv = exists($options{'paycvv'})
1851                      ? $options{'paycvv'}
1852                      : $self->paycvv;
1853       $content{cvv2} = $paycvv
1854         if length($paycvv);
1855
1856       my $paystart_month = exists($options{'paystart_month'})
1857                              ? $options{'paystart_month'}
1858                              : $self->paystart_month;
1859
1860       my $paystart_year  = exists($options{'paystart_year'})
1861                              ? $options{'paystart_year'}
1862                              : $self->paystart_year;
1863
1864       $content{card_start} = "$paystart_month/$paystart_year"
1865         if $paystart_month && $paystart_year;
1866
1867       my $payissue       = exists($options{'payissue'})
1868                              ? $options{'payissue'}
1869                              : $self->payissue;
1870       $content{issue_number} = $payissue if $payissue;
1871
1872     } elsif ( $options{method} eq 'ECHECK' ){
1873
1874       #nop for checks (though it shouldn't be called...)
1875
1876     } else {
1877       die "unknown method ". $options{method};
1878     }
1879
1880   } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1881     #move along
1882   } else {
1883     die "unknown namespace $namespace";
1884   }
1885
1886   ###
1887   # run transaction(s)
1888   ###
1889
1890   my $error;
1891   my $transaction; #need this back so we can do _tokenize_card
1892   # don't mutex the customer here, because they might be uncommitted. and
1893   # this is only verification. it doesn't matter if they have other
1894   # unfinished verifications.
1895
1896   my $cust_pay_pending = new FS::cust_pay_pending {
1897     'custnum_pending'   => 1,
1898     'paid'              => '1.00',
1899     '_date'             => '',
1900     'payby'             => $bop_method2payby{'CC'},
1901     'payinfo'           => $options{payinfo} || $self->payinfo,
1902     'paymask'           => $options{paymask} || $self->paymask,
1903     'paydate'           => $paydate,
1904     #'recurring_billing' => $content{recurring_billing},
1905     'pkgnum'            => $options{'pkgnum'},
1906     'status'            => 'new',
1907     'gatewaynum'        => $payment_gateway->gatewaynum || '',
1908     'session_id'        => $options{session_id} || '',
1909     #'jobnum'            => $options{depend_jobnum} || '',
1910   };
1911   $cust_pay_pending->payunique( $options{payunique} )
1912     if defined($options{payunique}) && length($options{payunique});
1913
1914   IMMEDIATE: {
1915     # open a separate handle for creating/updating the cust_pay_pending
1916     # record
1917     local $FS::UID::dbh = myconnect();
1918     local $FS::UID::AutoCommit = 1;
1919
1920     # if this is an existing customer (and we can tell now because
1921     # this is a fresh transaction), it's safe to assign their custnum
1922     # to the cust_pay_pending record, and then the verification attempt
1923     # will remain linked to them even if it fails.
1924     if ( FS::cust_main->by_key($self->custnum) ) {
1925       $cust_pay_pending->set('custnum', $self->custnum);
1926     }
1927
1928     warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1929       if $DEBUG > 1;
1930
1931     # if this fails, just return; everything else will still allow the
1932     # cust_pay_pending to have its custnum set later
1933     my $cpp_new_err = $cust_pay_pending->insert;
1934     return $cpp_new_err if $cpp_new_err;
1935
1936     warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1937       if $DEBUG > 1;
1938     warn Dumper($cust_pay_pending) if $DEBUG > 2;
1939
1940     $transaction = new $namespace( $payment_gateway->gateway_module,
1941                                    $self->_bop_options(\%options),
1942                                     );
1943
1944     $transaction->content(
1945       'type'           => 'CC',
1946       $self->_bop_auth(\%options),          
1947       'action'         => 'Authorization Only',
1948       'description'    => $options{'description'},
1949       'amount'         => '1.00',
1950       #'invoice_number' => $options{'invnum'},
1951       'customer_id'    => $self->custnum,
1952       %$bop_content,
1953       'reference'      => $cust_pay_pending->paypendingnum, #for now
1954       'callback_url'   => $payment_gateway->gateway_callback_url,
1955       'cancel_url'     => $payment_gateway->gateway_cancel_url,
1956       'email'          => $email,
1957       %content, #after
1958     );
1959
1960     $cust_pay_pending->status('pending');
1961     my $cpp_pending_err = $cust_pay_pending->replace;
1962     return $cpp_pending_err if $cpp_pending_err;
1963
1964     warn Dumper($transaction) if $DEBUG > 2;
1965
1966     unless ( $BOP_TESTING ) {
1967       $transaction->test_transaction(1)
1968         if $conf->exists('business-onlinepayment-test_transaction');
1969       $transaction->submit();
1970     } else {
1971       if ( $BOP_TESTING_SUCCESS ) {
1972         $transaction->is_success(1);
1973         $transaction->authorization('fake auth');
1974       } else {
1975         $transaction->is_success(0);
1976         $transaction->error_message('fake failure');
1977       }
1978     }
1979
1980     if ( $transaction->is_success() ) {
1981
1982       $cust_pay_pending->status('authorized');
1983       my $cpp_authorized_err = $cust_pay_pending->replace;
1984       return $cpp_authorized_err if $cpp_authorized_err;
1985
1986       my $auth = $transaction->authorization;
1987       my $ordernum = $transaction->can('order_number')
1988                      ? $transaction->order_number
1989                      : '';
1990
1991       my $reverse = new $namespace( $payment_gateway->gateway_module,
1992                                     $self->_bop_options(\%options),
1993                                   );
1994
1995       $reverse->content( 'action'        => 'Reverse Authorization',
1996                          $self->_bop_auth(\%options),          
1997
1998                          # B:OP
1999                          'amount'        => '1.00',
2000                          'authorization' => $transaction->authorization,
2001                          'order_number'  => $ordernum,
2002
2003                          # vsecure
2004                          'result_code'   => $transaction->result_code,
2005                          'txn_date'      => $transaction->txn_date,
2006
2007                          %content,
2008                        );
2009       $reverse->test_transaction(1)
2010         if $conf->exists('business-onlinepayment-test_transaction');
2011       $reverse->submit();
2012
2013       if ( $reverse->is_success ) {
2014
2015         $cust_pay_pending->status('done');
2016         $cust_pay_pending->statustext('reversed');
2017         my $cpp_reversed_err = $cust_pay_pending->replace;
2018         return $cpp_reversed_err if $cpp_reversed_err;
2019
2020       } else {
2021
2022         my $e = "Authorization successful but reversal failed, custnum #".
2023                 $self->custnum. ': '.  $reverse->result_code.
2024                 ": ". $reverse->error_message;
2025         $log->warning($e);
2026         warn $e;
2027         return $e;
2028
2029       }
2030
2031       ### Address Verification ###
2032       #
2033       # Single-letter codes vary by cardtype.
2034       #
2035       # Erring on the side of accepting cards if avs is not available,
2036       # only rejecting if avs occurred and there's been an explicit mismatch
2037       #
2038       # Charts below taken from vSecure documentation,
2039       #    shows codes for Amex/Dscv/MC/Visa
2040       #
2041       # ACCEPTABLE AVS RESPONSES:
2042       # Both Address and 5-digit postal code match Y A Y Y
2043       # Both address and 9-digit postal code match Y A X Y
2044       # United Kingdom – Address and postal code match _ _ _ F
2045       # International transaction – Address and postal code match _ _ _ D/M
2046       #
2047       # ACCEPTABLE, BUT ISSUE A WARNING:
2048       # Ineligible transaction; or message contains a content error _ _ _ E
2049       # System unavailable; retry R U R R
2050       # Information unavailable U W U U
2051       # Issuer does not support AVS S U S S
2052       # AVS is not applicable _ _ _ S
2053       # Incompatible formats – Not verified _ _ _ C
2054       # Incompatible formats – Address not verified; postal code matches _ _ _ P
2055       # International transaction – address not verified _ G _ G/I
2056       #
2057       # UNACCEPTABLE AVS RESPONSES:
2058       # Only Address matches A Y A A
2059       # Only 5-digit postal code matches Z Z Z Z
2060       # Only 9-digit postal code matches Z Z W W
2061       # Neither address nor postal code matches N N N N
2062
2063       if (my $avscode = uc($transaction->avs_code)) {
2064
2065         # map codes to accept/warn/reject
2066         my $avs = {
2067           'American Express card' => {
2068             'A' => 'r',
2069             'N' => 'r',
2070             'R' => 'w',
2071             'S' => 'w',
2072             'U' => 'w',
2073             'Y' => 'a',
2074             'Z' => 'r',
2075           },
2076           'Discover card' => {
2077             'A' => 'a',
2078             'G' => 'w',
2079             'N' => 'r',
2080             'U' => 'w',
2081             'W' => 'w',
2082             'Y' => 'r',
2083             'Z' => 'r',
2084           },
2085           'MasterCard' => {
2086             'A' => 'r',
2087             'N' => 'r',
2088             'R' => 'w',
2089             'S' => 'w',
2090             'U' => 'w',
2091             'W' => 'r',
2092             'X' => 'a',
2093             'Y' => 'a',
2094             'Z' => 'r',
2095           },
2096           'VISA card' => {
2097             'A' => 'r',
2098             'C' => 'w',
2099             'D' => 'a',
2100             'E' => 'w',
2101             'F' => 'a',
2102             'G' => 'w',
2103             'I' => 'w',
2104             'M' => 'a',
2105             'N' => 'r',
2106             'P' => 'w',
2107             'R' => 'w',
2108             'S' => 'w',
2109             'U' => 'w',
2110             'W' => 'r',
2111             'Y' => 'a',
2112             'Z' => 'r',
2113           },
2114         };
2115         my $cardtype = cardtype($content{card_number});
2116         if ($avs->{$cardtype}) {
2117           my $avsact = $avs->{$cardtype}->{$avscode};
2118           my $warning = '';
2119           if ($avsact eq 'r') {
2120             return "AVS code verification failed, cardtype $cardtype, code $avscode";
2121           } elsif ($avsact eq 'w') {
2122             $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2123           } elsif (!$avsact) {
2124             $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2125           } # else $avsact eq 'a'
2126           if ($warning) {
2127             $log->warning($warning);
2128             warn $warning;
2129           }
2130         } # else $cardtype avs handling not implemented
2131       } # else !$transaction->avs_code
2132
2133     } else { # is not success
2134
2135       # status is 'done' not 'declined', as in _realtime_bop_result
2136       $cust_pay_pending->status('done');
2137       $error = $transaction->error_message || 'Unknown error';
2138       $cust_pay_pending->statustext($error);
2139       # could also record failure_status here,
2140       #   but it's not supported by B::OP::vSecureProcessing...
2141       #   need a B::OP module with (reverse) auth only to test it with
2142       my $cpp_declined_err = $cust_pay_pending->replace;
2143       return $cpp_declined_err if $cpp_declined_err;
2144
2145     }
2146
2147   } # end of IMMEDIATE; we now have our $error and $transaction
2148
2149   ###
2150   # Save the custnum (as part of the main transaction, so it can reference
2151   # the cust_main)
2152   ###
2153
2154   if (!$cust_pay_pending->custnum) {
2155     $cust_pay_pending->set('custnum', $self->custnum);
2156     my $set_custnum_err = $cust_pay_pending->replace;
2157     if ($set_custnum_err) {
2158       $log->error($set_custnum_err);
2159       $error ||= $set_custnum_err;
2160       # but if there was a real verification error also, return that one
2161     }
2162   }
2163
2164   ###
2165   # Tokenize
2166   ###
2167
2168   if ( $transaction->can('card_token') && $transaction->card_token ) {
2169
2170     if ( $options{'payinfo'} eq $self->payinfo ) {
2171       $self->payinfo($transaction->card_token);
2172       my $error = $self->replace;
2173       if ( $error ) {
2174         my $warning = "WARNING: error storing token: $error, but proceeding anyway\n";
2175         $log->warning($warning);
2176         warn $warning;
2177       }
2178     }
2179
2180   }
2181
2182   ###
2183   # result handling
2184   ###
2185
2186   # $error contains the transaction error_message, if is_success was false.
2187  
2188   return $error;
2189
2190 }
2191
2192 =back
2193
2194 =head1 BUGS
2195
2196 Not autoloaded.
2197
2198 =head1 SEE ALSO
2199
2200 L<FS::cust_main>, L<FS::cust_main::Billing>
2201
2202 =cut
2203
2204 1;