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