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