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