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