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