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