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