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