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       #XXX internal system log $e (what's going on?)
1268       $perror = "$e ($perror)";
1269     }
1270
1271     return $perror;
1272   }
1273
1274 }
1275
1276 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1277
1278 Verifies successful third party processing of a realtime credit card or
1279 ACH (electronic check) transaction via a
1280 Business::OnlineThirdPartyPayment realtime gateway.  See
1281 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1282
1283 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1284
1285 The additional options I<payname>, I<city>, I<state>,
1286 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1287 if set, will override the value from the customer record.
1288
1289 I<description> is a free-text field passed to the gateway.  It defaults to
1290 "Internet services".
1291
1292 If an I<invnum> is specified, this payment (if successful) is applied to the
1293 specified invoice.  If you don't specify an I<invnum> you might want to
1294 call the B<apply_payments> method.
1295
1296 I<quiet> can be set true to surpress email decline notices.
1297
1298 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
1299 resulting paynum, if any.
1300
1301 I<payunique> is a unique identifier for this payment.
1302
1303 Returns a hashref containing elements bill_error (which will be undefined
1304 upon success) and session_id of any associated session.
1305
1306 =cut
1307
1308 sub realtime_botpp_capture {
1309   my( $self, $cust_pay_pending, %options ) = @_;
1310
1311   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1312
1313   if ( $DEBUG ) {
1314     warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1315     warn "  $_ => $options{$_}\n" foreach keys %options;
1316   }
1317
1318   eval "use Business::OnlineThirdPartyPayment";  
1319   die $@ if $@;
1320
1321   ###
1322   # select the gateway
1323   ###
1324
1325   my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1326
1327   my $payment_gateway;
1328   my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1329   $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1330                 { gatewaynum => $gatewaynum }
1331               )
1332     : $self->agent->payment_gateway( 'method' => $method,
1333                                      # 'invnum'  => $cust_pay_pending->invnum,
1334                                      # 'payinfo' => $cust_pay_pending->payinfo,
1335                                    );
1336
1337   $options{payment_gateway} = $payment_gateway; # for the helper subs
1338
1339   ###
1340   # massage data
1341   ###
1342
1343   my @invoicing_list = $self->invoicing_list_emailonly;
1344   if ( $conf->exists('emailinvoiceautoalways')
1345        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1346        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1347     push @invoicing_list, $self->all_emails;
1348   }
1349
1350   my $email = ($conf->exists('business-onlinepayment-email-override'))
1351               ? $conf->config('business-onlinepayment-email-override')
1352               : $invoicing_list[0];
1353
1354   my %content = ();
1355
1356   $content{email_customer} = 
1357     (    $conf->exists('business-onlinepayment-email_customer')
1358       || $conf->exists('business-onlinepayment-email-override') );
1359       
1360   ###
1361   # run transaction(s)
1362   ###
1363
1364   my $transaction =
1365     new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1366                                            _bop_options(\%options),
1367                                          );
1368
1369   $transaction->reference({ %options }); 
1370
1371   $transaction->content(
1372     'type'           => $method,
1373     _bop_auth(\%options),
1374     'action'         => 'Post Authorization',
1375     'description'    => $options{'description'},
1376     'amount'         => $cust_pay_pending->paid,
1377     #'invoice_number' => $options{'invnum'},
1378     'customer_id'    => $self->custnum,
1379     'reference'      => $cust_pay_pending->paypendingnum,
1380     'email'          => $email,
1381     'phone'          => $self->daytime || $self->night,
1382     %content, #after
1383     # plus whatever is required for bogus capture avoidance
1384   );
1385
1386   $transaction->submit();
1387
1388   my $error =
1389     $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1390
1391   if ( $options{'apply'} ) {
1392     my $apply_error = $self->apply_payments_and_credits;
1393     if ( $apply_error ) {
1394       warn "WARNING: error applying payment: $apply_error\n";
1395     }
1396   }
1397
1398   return {
1399     bill_error => $error,
1400     session_id => $cust_pay_pending->session_id,
1401   }
1402
1403 }
1404
1405 =item default_payment_gateway
1406
1407 DEPRECATED -- use agent->payment_gateway
1408
1409 =cut
1410
1411 sub default_payment_gateway {
1412   my( $self, $method ) = @_;
1413
1414   die "Real-time processing not enabled\n"
1415     unless $conf->exists('business-onlinepayment');
1416
1417   #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1418
1419   #load up config
1420   my $bop_config = 'business-onlinepayment';
1421   $bop_config .= '-ach'
1422     if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1423   my ( $processor, $login, $password, $action, @bop_options ) =
1424     $conf->config($bop_config);
1425   $action ||= 'normal authorization';
1426   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1427   die "No real-time processor is enabled - ".
1428       "did you set the business-onlinepayment configuration value?\n"
1429     unless $processor;
1430
1431   ( $processor, $login, $password, $action, @bop_options )
1432 }
1433
1434 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1435
1436 Refunds a realtime credit card or ACH (electronic check) transaction
1437 via a Business::OnlinePayment realtime gateway.  See
1438 L<http://420.am/business-onlinepayment> for supported gateways.
1439
1440 Available methods are: I<CC> or I<ECHECK>
1441
1442 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1443
1444 Most gateways require a reference to an original payment transaction to refund,
1445 so you probably need to specify a I<paynum>.
1446
1447 I<amount> defaults to the original amount of the payment if not specified.
1448
1449 I<reasonnum> specified an existing refund reason for the refund
1450
1451 I<paydate> specifies the expiration date for a credit card overriding the
1452 value from the customer record or the payment record. Specified as yyyy-mm-dd
1453
1454 Implementation note: If I<amount> is unspecified or equal to the amount of the
1455 orignal payment, first an attempt is made to "void" the transaction via
1456 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1457 the normal attempt is made to "refund" ("credit") the transaction via the
1458 gateway is attempted. No attempt to "void" the transaction is made if the 
1459 gateway has introspection data and doesn't support void.
1460
1461 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1462 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1463 #if set, will override the value from the customer record.
1464
1465 #If an I<invnum> is specified, this payment (if successful) is applied to the
1466 #specified invoice.  If you don't specify an I<invnum> you might want to
1467 #call the B<apply_payments> method.
1468
1469 =cut
1470
1471 #some false laziness w/realtime_bop, not enough to make it worth merging
1472 #but some useful small subs should be pulled out
1473 sub realtime_refund_bop {
1474   my $self = shift;
1475
1476   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1477
1478   my %options = ();
1479   if (ref($_[0]) eq 'HASH') {
1480     %options = %{$_[0]};
1481   } else {
1482     my $method = shift;
1483     %options = @_;
1484     $options{method} = $method;
1485   }
1486
1487   if ( $DEBUG ) {
1488     warn "$me realtime_refund_bop (new): $options{method} refund\n";
1489     warn "  $_ => $options{$_}\n" foreach keys %options;
1490   }
1491
1492   return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1493
1494   my %content = ();
1495
1496   ###
1497   # look up the original payment and optionally a gateway for that payment
1498   ###
1499
1500   my $cust_pay = '';
1501   my $amount = $options{'amount'};
1502
1503   my( $processor, $login, $password, @bop_options, $namespace ) ;
1504   my( $auth, $order_number ) = ( '', '', '' );
1505   my $gatewaynum = '';
1506
1507   if ( $options{'paynum'} ) {
1508
1509     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
1510     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1511       or return "Unknown paynum $options{'paynum'}";
1512     $amount ||= $cust_pay->paid;
1513
1514     my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1515     $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1516
1517     if ( $cust_pay->get('processor') ) {
1518       ($gatewaynum, $processor, $auth, $order_number) =
1519       (
1520         $cust_pay->gatewaynum,
1521         $cust_pay->processor,
1522         $cust_pay->auth,
1523         $cust_pay->order_number,
1524       );
1525     } else {
1526       # this payment wasn't upgraded, which probably means this won't work,
1527       # but try it anyway
1528       $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1529         or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1530                   $cust_pay->paybatch;
1531       ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1532     }
1533
1534     my $payment_gateway;
1535     if ( $gatewaynum ) { #gateway for the payment to be refunded
1536
1537       $payment_gateway =
1538         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1539       die "payment gateway $gatewaynum not found"
1540         unless $payment_gateway;
1541
1542       $processor   = $payment_gateway->gateway_module;
1543       $login       = $payment_gateway->gateway_username;
1544       $password    = $payment_gateway->gateway_password;
1545       $namespace   = $payment_gateway->gateway_namespace;
1546       @bop_options = $payment_gateway->options;
1547
1548     } else { #try the default gateway
1549
1550       my $conf_processor;
1551       $payment_gateway =
1552         $self->agent->payment_gateway('method' => $options{method});
1553
1554       ( $conf_processor, $login, $password, $namespace ) =
1555         map { my $method = "gateway_$_"; $payment_gateway->$method }
1556           qw( module username password namespace );
1557
1558       @bop_options = $payment_gateway->gatewaynum
1559                        ? $payment_gateway->options
1560                        : @{ $payment_gateway->get('options') };
1561       my %bop_options = @bop_options;
1562
1563       return "processor of payment $options{'paynum'} $processor does not".
1564              " match default processor $conf_processor"
1565         unless ($processor eq $conf_processor)
1566             || (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'}));
1567
1568       $processor = $conf_processor;
1569
1570     }
1571
1572     # if gateway has switched to CardFortress but token_check hasn't run yet,
1573     # tokenize just this record now, so that token gets passed/set appropriately
1574     if ($cust_pay->payby eq 'CARD' && !$cust_pay->tokenized) {
1575       my %tokenopts = (
1576         'payment_gateway' => $payment_gateway,
1577         'method'          => 'CC',
1578         'payinfo'         => $cust_pay->payinfo,
1579         'paydate'         => $cust_pay->paydate,
1580       );
1581       my $error = $self->realtime_tokenize(\%tokenopts); # no-op unless gateway can tokenize
1582       if ($self->tokenized($tokenopts{'payinfo'})) { # implies no error
1583         warn "  tokenizing cust_pay\n" if $DEBUG > 1;
1584         $cust_pay->payinfo($tokenopts{'payinfo'});
1585         $error = $cust_pay->replace;
1586       }
1587       return $error if $error;
1588     }
1589
1590   } else { # didn't specify a paynum, so look for agent gateway overrides
1591            # like a normal transaction 
1592  
1593     my $payment_gateway =
1594       $self->agent->payment_gateway( 'method'  => $options{method} );
1595     ( $processor, $login, $password, $namespace ) =
1596       map { my $method = "gateway_$_"; $payment_gateway->$method }
1597         qw( module username password namespace );
1598
1599     my @bop_options = $payment_gateway->gatewaynum
1600                         ? $payment_gateway->options
1601                         : @{ $payment_gateway->get('options') };
1602
1603   }
1604   return "neither amount nor paynum specified" unless $amount;
1605
1606   eval "use $namespace";  
1607   die $@ if $@;
1608
1609   %content = (
1610     %content,
1611     'type'           => $options{method},
1612     'login'          => $login,
1613     'password'       => $password,
1614     'order_number'   => $order_number,
1615     'amount'         => $amount,
1616   );
1617   $content{authorization} = $auth
1618     if length($auth); #echeck/ACH transactions have an order # but no auth
1619                       #(at least with authorize.net)
1620
1621   my $currency =    $conf->exists('business-onlinepayment-currency')
1622                  && $conf->config('business-onlinepayment-currency');
1623   $content{currency} = $currency if $currency;
1624
1625   my $disable_void_after;
1626   if ($conf->exists('disable_void_after')
1627       && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1628     $disable_void_after = $1;
1629   }
1630
1631   #first try void if applicable
1632   my $void = new Business::OnlinePayment( $processor, @bop_options );
1633
1634   my $tryvoid = 1;
1635   if ($void->can('info')) {
1636       my $paytype = '';
1637       $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1638       $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1639       my %supported_actions = $void->info('supported_actions');
1640       $tryvoid = 0 
1641         if ( %supported_actions && $paytype 
1642                 && defined($supported_actions{$paytype}) 
1643                 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1644   }
1645
1646   if ( $cust_pay && $cust_pay->paid == $amount
1647     && (
1648       ( not defined($disable_void_after) )
1649       || ( time < ($cust_pay->_date + $disable_void_after ) )
1650     )
1651     && $tryvoid
1652   ) {
1653     warn "  attempting void\n" if $DEBUG > 1;
1654     if ( $void->can('info') ) {
1655       if ( $cust_pay->payby eq 'CARD'
1656            && $void->info('CC_void_requires_card') )
1657       {
1658         $content{'card_number'} = $cust_pay->payinfo;
1659       } elsif ( $cust_pay->payby eq 'CHEK'
1660                 && $void->info('ECHECK_void_requires_account') )
1661       {
1662         ( $content{'account_number'}, $content{'routing_code'} ) =
1663           split('@', $cust_pay->payinfo);
1664         $content{'name'} = $self->get('first'). ' '. $self->get('last');
1665       }
1666     }
1667     $void->content( 'action' => 'void', %content );
1668     $void->test_transaction(1)
1669       if $conf->exists('business-onlinepayment-test_transaction');
1670     $void->submit();
1671     if ( $void->is_success ) {
1672       # specified as a refund reason, but now we want a payment void reason
1673       # extract just the reason text, let cust_pay::void handle new_or_existing
1674       my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1675       my $error;
1676       $error = 'Reason could not be loaded' unless $reason;      
1677       $error = $cust_pay->void($reason->reason) unless $error;
1678       if ( $error ) {
1679         # gah, even with transactions.
1680         my $e = 'WARNING: Card/ACH voided but database not updated - '.
1681                 "error voiding payment: $error";
1682         warn $e;
1683         return $e;
1684       }
1685       warn "  void successful\n" if $DEBUG > 1;
1686       return '';
1687     }
1688   }
1689
1690   warn "  void unsuccessful, trying refund\n"
1691     if $DEBUG > 1;
1692
1693   #massage data
1694   my $address = $self->address1;
1695   $address .= ", ". $self->address2 if $self->address2;
1696
1697   my($payname, $payfirst, $paylast);
1698   if ( $self->payname && $options{method} ne 'ECHECK' ) {
1699     $payname = $self->payname;
1700     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1701       or return "Illegal payname $payname";
1702     ($payfirst, $paylast) = ($1, $2);
1703   } else {
1704     $payfirst = $self->getfield('first');
1705     $paylast = $self->getfield('last');
1706     $payname =  "$payfirst $paylast";
1707   }
1708
1709   my @invoicing_list = $self->invoicing_list_emailonly;
1710   if ( $conf->exists('emailinvoiceautoalways')
1711        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1712        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1713     push @invoicing_list, $self->all_emails;
1714   }
1715
1716   my $email = ($conf->exists('business-onlinepayment-email-override'))
1717               ? $conf->config('business-onlinepayment-email-override')
1718               : $invoicing_list[0];
1719
1720   my $payip = exists($options{'payip'})
1721                 ? $options{'payip'}
1722                 : $self->payip;
1723   $content{customer_ip} = $payip
1724     if length($payip);
1725
1726   my $payinfo = '';
1727   my $paymask = ''; # for refund record
1728   if ( $options{method} eq 'CC' ) {
1729
1730     if ( $cust_pay ) {
1731       $content{card_number} = $payinfo = $cust_pay->payinfo;
1732       $paymask = $cust_pay->paymask;
1733       (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1734         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1735         ($content{expiration} = "$2/$1");  # where available
1736     } else {
1737       # this really needs a better cleanup
1738       die "Refund without paynum not supported";
1739 #      $content{card_number} = $payinfo = $self->payinfo;
1740 #      (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1741 #        =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1742 #      $content{expiration} = "$2/$1";
1743     }
1744
1745   } elsif ( $options{method} eq 'ECHECK' ) {
1746
1747     if ( $cust_pay ) {
1748       $payinfo = $cust_pay->payinfo;
1749     } else {
1750       $payinfo = $self->payinfo;
1751     } 
1752     ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1753     $content{bank_name} = $self->payname;
1754     $content{account_type} = 'CHECKING';
1755     $content{account_name} = $payname;
1756     $content{customer_org} = $self->company ? 'B' : 'I';
1757     $content{customer_ssn} = $self->ss;
1758
1759   }
1760
1761   #then try refund
1762   my $refund = new Business::OnlinePayment( $processor, @bop_options );
1763   my %sub_content = $refund->content(
1764     'action'         => 'credit',
1765     'customer_id'    => $self->custnum,
1766     'last_name'      => $paylast,
1767     'first_name'     => $payfirst,
1768     'name'           => $payname,
1769     'address'        => $address,
1770     'city'           => $self->city,
1771     'state'          => $self->state,
1772     'zip'            => $self->zip,
1773     'country'        => $self->country,
1774     'email'          => $email,
1775     'phone'          => $self->daytime || $self->night,
1776     %content, #after
1777   );
1778   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
1779     if $DEBUG > 1;
1780   $refund->test_transaction(1)
1781     if $conf->exists('business-onlinepayment-test_transaction');
1782   $refund->submit();
1783
1784   return "$processor error: ". $refund->error_message
1785     unless $refund->is_success();
1786
1787   $order_number = $refund->order_number if $refund->can('order_number');
1788
1789   # change this to just use $cust_pay->delete_cust_bill_pay?
1790   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1791     my @cust_bill_pay = $cust_pay->cust_bill_pay;
1792     last unless @cust_bill_pay;
1793     my $cust_bill_pay = pop @cust_bill_pay;
1794     my $error = $cust_bill_pay->delete;
1795     last if $error;
1796   }
1797
1798   my $cust_refund = new FS::cust_refund ( {
1799     'custnum'  => $self->custnum,
1800     'paynum'   => $options{'paynum'},
1801     'source_paynum' => $options{'paynum'},
1802     'refund'   => $amount,
1803     '_date'    => '',
1804     'payby'    => $bop_method2payby{$options{method}},
1805     'payinfo'  => $payinfo,
1806     'paymask'  => $paymask,
1807     'reasonnum'     => $options{'reasonnum'},
1808     'gatewaynum'    => $gatewaynum, # may be null
1809     'processor'     => $processor,
1810     'auth'          => $refund->authorization,
1811     'order_number'  => $order_number,
1812   } );
1813   my $error = $cust_refund->insert;
1814   if ( $error ) {
1815     $cust_refund->paynum(''); #try again with no specific paynum
1816     $cust_refund->source_paynum('');
1817     my $error2 = $cust_refund->insert;
1818     if ( $error2 ) {
1819       # gah, even with transactions.
1820       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1821               "error inserting refund ($processor): $error2".
1822               " (previously tried insert with paynum #$options{'paynum'}" .
1823               ": $error )";
1824       warn $e;
1825       return $e;
1826     }
1827   }
1828
1829   ''; #no error
1830
1831 }
1832
1833 =item realtime_verify_bop [ OPTION => VALUE ... ]
1834
1835 Runs an authorization-only transaction for $1 against this credit card (if
1836 successful, immediatly reverses the authorization).
1837
1838 Returns the empty string if the authorization was sucessful, or an error
1839 message otherwise.
1840
1841 Option I<cust_payby> should be passed, even if it's not yet been inserted.
1842 Object will be tokenized if possible, but that change will not be
1843 updated in database (must be inserted/replaced afterwards.)
1844
1845 Currently only succeeds for Business::OnlinePayment CC transactions.
1846
1847 =cut
1848
1849 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1850 #it worth merging but some useful small subs should be pulled out
1851 sub realtime_verify_bop {
1852   my $self = shift;
1853
1854   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1855   my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1856
1857   my %options = ();
1858   if (ref($_[0]) eq 'HASH') {
1859     %options = %{$_[0]};
1860   } else {
1861     %options = @_;
1862   }
1863
1864   if ( $DEBUG ) {
1865     warn "$me realtime_verify_bop\n";
1866     warn "  $_ => $options{$_}\n" foreach keys %options;
1867   }
1868
1869   # set fields from passed cust_payby
1870   return "No cust_payby" unless $options{'cust_payby'};
1871   _bop_cust_payby_options(\%options);
1872
1873   # check for banned credit card/ACH
1874   my $ban = FS::banned_pay->ban_search(
1875     'payby'   => $bop_method2payby{'CC'},
1876     'payinfo' => $options{payinfo},
1877   );
1878   return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1879
1880   # possibly run a separate transaction to tokenize card number,
1881   #   so that we never store tokenized card info in cust_pay_pending
1882   if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
1883     my $token_error = $self->realtime_tokenize(\%options);
1884     return $token_error if $token_error;
1885     #important that we not replace cust_payby here,
1886     #because cust_payby->replace uses realtime_verify_bop!
1887   }
1888
1889   ###
1890   # select a gateway
1891   ###
1892
1893   my $payment_gateway =  $self->_payment_gateway( \%options );
1894   my $namespace = $payment_gateway->gateway_namespace;
1895
1896   eval "use $namespace";  
1897   die $@ if $@;
1898
1899   ###
1900   # massage data
1901   ###
1902
1903   my $bop_content = $self->_bop_content(\%options);
1904   return $bop_content unless ref($bop_content);
1905
1906   my @invoicing_list = $self->invoicing_list_emailonly;
1907   if ( $conf->exists('emailinvoiceautoalways')
1908        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1909        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1910     push @invoicing_list, $self->all_emails;
1911   }
1912
1913   my $email = ($conf->exists('business-onlinepayment-email-override'))
1914               ? $conf->config('business-onlinepayment-email-override')
1915               : $invoicing_list[0];
1916
1917   my $paydate = '';
1918   my %content = ();
1919
1920   if ( $namespace eq 'Business::OnlinePayment' ) {
1921
1922     if ( $options{method} eq 'CC' ) {
1923
1924       $content{card_number} = $options{payinfo};
1925       $paydate = $options{'paydate'};
1926       $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1927       $content{expiration} = "$2/$1";
1928
1929       $content{cvv2} = $options{'paycvv'}
1930         if length($options{'paycvv'});
1931
1932       my $paystart_month = $options{'paystart_month'};
1933       my $paystart_year  = $options{'paystart_year'};
1934
1935       $content{card_start} = "$paystart_month/$paystart_year"
1936         if $paystart_month && $paystart_year;
1937
1938       my $payissue       = $options{'payissue'};
1939       $content{issue_number} = $payissue if $payissue;
1940
1941     } elsif ( $options{method} eq 'ECHECK' ){
1942       #cannot verify, move along (though it shouldn't be called...)
1943       return '';
1944     } else {
1945       return "unknown method ". $options{method};
1946     }
1947   } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1948     #cannot verify, move along
1949     return '';
1950   } else {
1951     return "unknown namespace $namespace";
1952   }
1953
1954   ###
1955   # run transaction(s)
1956   ###
1957
1958   my $error;
1959   my $transaction; #need this back so we can do _tokenize_card
1960
1961   # don't mutex the customer here, because they might be uncommitted. and
1962   # this is only verification. it doesn't matter if they have other
1963   # unfinished verifications.
1964
1965   my $cust_pay_pending = new FS::cust_pay_pending {
1966     'custnum_pending'   => 1,
1967     'paid'              => '1.00',
1968     '_date'             => '',
1969     'payby'             => $bop_method2payby{'CC'},
1970     'payinfo'           => $options{payinfo},
1971     'paymask'           => $options{paymask},
1972     'paydate'           => $paydate,
1973     'pkgnum'            => $options{'pkgnum'},
1974     'status'            => 'new',
1975     'gatewaynum'        => $payment_gateway->gatewaynum || '',
1976     'session_id'        => $options{session_id} || '',
1977   };
1978   $cust_pay_pending->payunique( $options{payunique} )
1979     if defined($options{payunique}) && length($options{payunique});
1980
1981   IMMEDIATE: {
1982     # open a separate handle for creating/updating the cust_pay_pending
1983     # record
1984     local $FS::UID::dbh = myconnect();
1985     local $FS::UID::AutoCommit = 1;
1986
1987     # if this is an existing customer (and we can tell now because
1988     # this is a fresh transaction), it's safe to assign their custnum
1989     # to the cust_pay_pending record, and then the verification attempt
1990     # will remain linked to them even if it fails.
1991     if ( FS::cust_main->by_key($self->custnum) ) {
1992       $cust_pay_pending->set('custnum', $self->custnum);
1993     }
1994
1995     warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1996       if $DEBUG > 1;
1997
1998     # if this fails, just return; everything else will still allow the
1999     # cust_pay_pending to have its custnum set later
2000     my $cpp_new_err = $cust_pay_pending->insert;
2001     return $cpp_new_err if $cpp_new_err;
2002
2003     warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
2004       if $DEBUG > 1;
2005     warn Dumper($cust_pay_pending) if $DEBUG > 2;
2006
2007     $transaction = new $namespace( $payment_gateway->gateway_module,
2008                                    _bop_options(\%options),
2009                                     );
2010
2011     $transaction->content(
2012       'type'           => 'CC',
2013       _bop_auth(\%options),          
2014       'action'         => 'Authorization Only',
2015       'description'    => $options{'description'},
2016       'amount'         => '1.00',
2017       'customer_id'    => $self->custnum,
2018       %$bop_content,
2019       'reference'      => $cust_pay_pending->paypendingnum, #for now
2020       'email'          => $email,
2021       %content, #after
2022     );
2023
2024     $cust_pay_pending->status('pending');
2025     my $cpp_pending_err = $cust_pay_pending->replace;
2026     return $cpp_pending_err if $cpp_pending_err;
2027
2028     warn Dumper($transaction) if $DEBUG > 2;
2029
2030     unless ( $BOP_TESTING ) {
2031       $transaction->test_transaction(1)
2032         if $conf->exists('business-onlinepayment-test_transaction');
2033       $transaction->submit();
2034     } else {
2035       if ( $BOP_TESTING_SUCCESS ) {
2036         $transaction->is_success(1);
2037         $transaction->authorization('fake auth');
2038       } else {
2039         $transaction->is_success(0);
2040         $transaction->error_message('fake failure');
2041       }
2042     }
2043
2044     if ( $transaction->is_success() ) {
2045
2046       $cust_pay_pending->status('authorized');
2047       my $cpp_authorized_err = $cust_pay_pending->replace;
2048       return $cpp_authorized_err if $cpp_authorized_err;
2049
2050       my $auth = $transaction->authorization;
2051       my $ordernum = $transaction->can('order_number')
2052                      ? $transaction->order_number
2053                      : '';
2054
2055       my $reverse = new $namespace( $payment_gateway->gateway_module,
2056                                     _bop_options(\%options),
2057                                   );
2058
2059       $reverse->content( 'action'        => 'Reverse Authorization',
2060                          _bop_auth(\%options),          
2061
2062                          # B:OP
2063                          'amount'        => '1.00',
2064                          'authorization' => $transaction->authorization,
2065                          'order_number'  => $ordernum,
2066
2067                          # vsecure
2068                          'result_code'   => $transaction->result_code,
2069                          'txn_date'      => $transaction->txn_date,
2070
2071                          %content,
2072                        );
2073       $reverse->test_transaction(1)
2074         if $conf->exists('business-onlinepayment-test_transaction');
2075       $reverse->submit();
2076
2077       if ( $reverse->is_success ) {
2078
2079         $cust_pay_pending->status('done');
2080         $cust_pay_pending->statustext('reversed');
2081         my $cpp_reversed_err = $cust_pay_pending->replace;
2082         return $cpp_reversed_err if $cpp_reversed_err;
2083
2084       } else {
2085
2086         my $e = "Authorization successful but reversal failed, custnum #".
2087                 $self->custnum. ': '.  $reverse->result_code.
2088                 ": ". $reverse->error_message;
2089         $log->warning($e);
2090         warn $e;
2091         return $e;
2092
2093       }
2094
2095       ### Address Verification ###
2096       #
2097       # Single-letter codes vary by cardtype.
2098       #
2099       # Erring on the side of accepting cards if avs is not available,
2100       # only rejecting if avs occurred and there's been an explicit mismatch
2101       #
2102       # Charts below taken from vSecure documentation,
2103       #    shows codes for Amex/Dscv/MC/Visa
2104       #
2105       # ACCEPTABLE AVS RESPONSES:
2106       # Both Address and 5-digit postal code match Y A Y Y
2107       # Both address and 9-digit postal code match Y A X Y
2108       # United Kingdom – Address and postal code match _ _ _ F
2109       # International transaction – Address and postal code match _ _ _ D/M
2110       #
2111       # ACCEPTABLE, BUT ISSUE A WARNING:
2112       # Ineligible transaction; or message contains a content error _ _ _ E
2113       # System unavailable; retry R U R R
2114       # Information unavailable U W U U
2115       # Issuer does not support AVS S U S S
2116       # AVS is not applicable _ _ _ S
2117       # Incompatible formats – Not verified _ _ _ C
2118       # Incompatible formats – Address not verified; postal code matches _ _ _ P
2119       # International transaction – address not verified _ G _ G/I
2120       #
2121       # UNACCEPTABLE AVS RESPONSES:
2122       # Only Address matches A Y A A
2123       # Only 5-digit postal code matches Z Z Z Z
2124       # Only 9-digit postal code matches Z Z W W
2125       # Neither address nor postal code matches N N N N
2126
2127       if (my $avscode = uc($transaction->avs_code)) {
2128
2129         # map codes to accept/warn/reject
2130         my $avs = {
2131           'American Express card' => {
2132             'A' => 'r',
2133             'N' => 'r',
2134             'R' => 'w',
2135             'S' => 'w',
2136             'U' => 'w',
2137             'Y' => 'a',
2138             'Z' => 'r',
2139           },
2140           'Discover card' => {
2141             'A' => 'a',
2142             'G' => 'w',
2143             'N' => 'r',
2144             'U' => 'w',
2145             'W' => 'w',
2146             'Y' => 'r',
2147             'Z' => 'r',
2148           },
2149           'MasterCard' => {
2150             'A' => 'r',
2151             'N' => 'r',
2152             'R' => 'w',
2153             'S' => 'w',
2154             'U' => 'w',
2155             'W' => 'r',
2156             'X' => 'a',
2157             'Y' => 'a',
2158             'Z' => 'r',
2159           },
2160           'VISA card' => {
2161             'A' => 'r',
2162             'C' => 'w',
2163             'D' => 'a',
2164             'E' => 'w',
2165             'F' => 'a',
2166             'G' => 'w',
2167             'I' => 'w',
2168             'M' => 'a',
2169             'N' => 'r',
2170             'P' => 'w',
2171             'R' => 'w',
2172             'S' => 'w',
2173             'U' => 'w',
2174             'W' => 'r',
2175             'Y' => 'a',
2176             'Z' => 'r',
2177           },
2178         };
2179         my $cardtype = cardtype($content{card_number});
2180         if ($avs->{$cardtype}) {
2181           my $avsact = $avs->{$cardtype}->{$avscode};
2182           my $warning = '';
2183           if ($avsact eq 'r') {
2184             return "AVS code verification failed, cardtype $cardtype, code $avscode";
2185           } elsif ($avsact eq 'w') {
2186             $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2187           } elsif (!$avsact) {
2188             $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2189           } # else $avsact eq 'a'
2190           if ($warning) {
2191             $log->warning($warning);
2192             warn $warning;
2193           }
2194         } # else $cardtype avs handling not implemented
2195       } # else !$transaction->avs_code
2196
2197     } else { # is not success
2198
2199       # status is 'done' not 'declined', as in _realtime_bop_result
2200       $cust_pay_pending->status('done');
2201       $error = $transaction->error_message || 'Unknown error';
2202       $cust_pay_pending->statustext($error);
2203       # could also record failure_status here,
2204       #   but it's not supported by B::OP::vSecureProcessing...
2205       #   need a B::OP module with (reverse) auth only to test it with
2206       my $cpp_declined_err = $cust_pay_pending->replace;
2207       return $cpp_declined_err if $cpp_declined_err;
2208
2209     }
2210
2211   } # end of IMMEDIATE; we now have our $error and $transaction
2212
2213   ###
2214   # Save the custnum (as part of the main transaction, so it can reference
2215   # the cust_main)
2216   ###
2217
2218   if (!$cust_pay_pending->custnum) {
2219     $cust_pay_pending->set('custnum', $self->custnum);
2220     my $set_custnum_err = $cust_pay_pending->replace;
2221     if ($set_custnum_err) {
2222       $log->error($set_custnum_err);
2223       $error ||= $set_custnum_err;
2224       # but if there was a real verification error also, return that one
2225     }
2226   }
2227
2228   ###
2229   # remove paycvv here?  need to find out if a reversed auth
2230   #   counts as an initial transaction for paycvv retention requirements
2231   ###
2232
2233   ###
2234   # Tokenize
2235   ###
2236
2237   # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
2238   #   if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
2239   if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
2240     $cust_pay_pending->payinfo($card_token);
2241     my $cpp_token_err = $cust_pay_pending->replace;
2242     #this leaves real card number in cust_pay_pending, but can't do much else if cpp won't replace
2243     return $cpp_token_err if $cpp_token_err;
2244     #important that we not replace cust_payby here,
2245     #because cust_payby->replace uses realtime_verify_bop!
2246   }
2247
2248   ###
2249   # result handling
2250   ###
2251
2252   # $error contains the transaction error_message, if is_success was false.
2253  
2254   return $error;
2255
2256 }
2257
2258 =item realtime_tokenize [ OPTION => VALUE ... ]
2259
2260 If possible and necessary, runs a tokenize transaction.
2261 In order to be possible, a credit card cust_payby record
2262 must be passed and a Business::OnlinePayment gateway capable
2263 of Tokenize transactions must be configured for this user.
2264 Is only necessary if payinfo is not yet tokenized.
2265
2266 Returns the empty string if the authorization was sucessful
2267 or was not possible/necessary (thus allowing this to be safely called with
2268 non-tokenizable records/gateways, without having to perform separate tests),
2269 or an error message otherwise.
2270
2271 Option I<cust_payby> may be passed, even if it's not yet been inserted.
2272 Object will be tokenized if possible, but that change will not be
2273 updated in database (must be inserted/replaced afterwards.)
2274
2275 Otherwise, options I<method>, I<payinfo> and other cust_payby fields
2276 may be passed.  If options are passed as a hashref, I<payinfo>
2277 will be updated as appropriate in the passed hashref.
2278
2279 Can be run as a class method if option I<payment_gateway> is passed,
2280 but default customer id/name/phone can't be set in that case.  This
2281 is really only intended for tokenizing old records on upgrade.
2282
2283 =cut
2284
2285 # careful--might be run as a class method
2286 sub realtime_tokenize {
2287   my $self = shift;
2288
2289   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2290   my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_tokenize');
2291
2292   my %options = ();
2293   my $outoptions; #for returning cust_payby/payinfo
2294   if (ref($_[0]) eq 'HASH') {
2295     %options = %{$_[0]};
2296     $outoptions = $_[0];
2297   } else {
2298     %options = @_;
2299     $outoptions = \%options;
2300   }
2301
2302   # set fields from passed cust_payby
2303   _bop_cust_payby_options(\%options);
2304   return '' unless $options{method} eq 'CC';
2305   return '' if $self->tokenized($options{payinfo}); #already tokenized
2306
2307   # check for banned credit card/ACH
2308   my $ban = FS::banned_pay->ban_search(
2309     'payby'   => $bop_method2payby{'CC'},
2310     'payinfo' => $options{payinfo},
2311   );
2312   return "Banned credit card" if $ban && $ban->bantype ne 'warn';
2313
2314   ###
2315   # select a gateway
2316   ###
2317
2318   $options{'nofatal'} = 1;
2319   my $payment_gateway =  $self->_payment_gateway( \%options );
2320   return '' unless $payment_gateway;
2321   my $namespace = $payment_gateway->gateway_namespace;
2322   return '' unless $namespace eq 'Business::OnlinePayment';
2323
2324   eval "use $namespace";  
2325   return $@ if $@;
2326
2327   ###
2328   # check for tokenize ability
2329   ###
2330
2331   my $transaction = new $namespace( $payment_gateway->gateway_module,
2332                                     _bop_options(\%options),
2333                                   );
2334
2335   return '' unless $transaction->can('info');
2336
2337   my %supported_actions = $transaction->info('supported_actions');
2338   return '' unless $supported_actions{'CC'}
2339                 && grep /^Tokenize$/, @{$supported_actions{'CC'}};
2340
2341   ###
2342   # massage data
2343   ###
2344
2345   ### Currently, cardfortress only keys in on card number and exp date.
2346   ### We pass everything we'd pass to a normal transaction,
2347   ### for ease of current and future development,
2348   ### but note, when tokenizing old records, we may only have access to payinfo/paydate
2349
2350   my $bop_content = $self->_bop_content(\%options);
2351   return $bop_content unless ref($bop_content);
2352
2353   my $paydate = '';
2354   my %content = ();
2355
2356   $content{card_number} = $options{payinfo};
2357   $paydate = $options{'paydate'};
2358   $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2359   $content{expiration} = "$2/$1";
2360
2361   $content{cvv2} = $options{'paycvv'}
2362     if length($options{'paycvv'});
2363
2364   my $paystart_month = $options{'paystart_month'};
2365   my $paystart_year  = $options{'paystart_year'};
2366
2367   $content{card_start} = "$paystart_month/$paystart_year"
2368     if $paystart_month && $paystart_year;
2369
2370   my $payissue       = $options{'payissue'};
2371   $content{issue_number} = $payissue if $payissue;
2372
2373   $content{customer_id} = $self->custnum
2374     if ref($self);
2375
2376   ###
2377   # run transaction
2378   ###
2379
2380   my $error;
2381
2382   # no cust_pay_pending---this is not a financial transaction
2383
2384   $transaction->content(
2385     'type'           => 'CC',
2386     _bop_auth(\%options),          
2387     'action'         => 'Tokenize',
2388     'description'    => $options{'description'},
2389     %$bop_content,
2390     %content, #after
2391   );
2392
2393   # no $BOP_TESTING handling for this
2394   $transaction->test_transaction(1)
2395     if $conf->exists('business-onlinepayment-test_transaction');
2396   $transaction->submit();
2397
2398   if ( $transaction->card_token() ) { # no is_success flag
2399
2400     # realtime_tokenize should not clear paycvv at this time.  it might be
2401     # needed for the first transaction, and a tokenize isn't actually a
2402     # transaction that hits the gateway.  at some point in the future, card
2403     # fortress should take on the "store paycvv until first transaction"
2404     # functionality and we should fix this in freeside, but i that's a bigger
2405     # project for another time.
2406
2407     #important that we not replace cust_payby here, 
2408     #because cust_payby->replace uses realtime_tokenize!
2409     $self->_tokenize_card($transaction,$outoptions);
2410
2411   } else {
2412
2413     $error = $transaction->error_message || 'Unknown error when tokenizing card';
2414
2415   }
2416
2417   return $error;
2418
2419 }
2420
2421
2422 =item tokenized PAYINFO
2423
2424 Convenience wrapper for L<FS::payinfo_Mixin/tokenized>
2425
2426 PAYINFO is required.
2427
2428 Can be run as class or object method, never loads from object.
2429
2430 =cut
2431
2432 sub tokenized {
2433   my $this = shift;
2434   my $payinfo = shift;
2435   FS::cust_pay->tokenized($payinfo);
2436 }
2437
2438 =item token_check [ quiet => 1, queue => 1, daily => 1 ]
2439
2440 NOT A METHOD.  Acts on all customers.  Placed here because it makes
2441 use of module-internal methods, and to keep everything that uses
2442 Billing::OnlinePayment all in one place.
2443
2444 Tokenizes all tokenizable card numbers from payinfo in cust_payby and 
2445 CARD transactions in cust_pay_pending, cust_pay, cust_pay_void and cust_refund.
2446
2447 If the I<queue> flag is set, newly tokenized records will be immediately
2448 committed, regardless of AutoCommit, so as to release the mutex on the record.
2449
2450 If all configured gateways have the ability to tokenize, detection of an 
2451 untokenizable record will cause a fatal error.  However, if the I<queue> flag 
2452 is set, this will instead cause a critical error to be recorded in the log, 
2453 and any other tokenizable records will still be committed.
2454
2455 If the I<daily> flag is also set, detection of existing untokenized records will 
2456 record an info message in the system log (because they should have never appeared 
2457 in the first place.)  Tokenization will still be attempted.
2458
2459 If any configured gateways do NOT have the ability to tokenize, or if a
2460 default gateway is not configured, then untokenized records are not considered 
2461 a threat, and no critical errors will be generated in the log.
2462
2463 =cut
2464
2465 sub token_check {
2466   #acts on all customers
2467   my %opt = @_;
2468   my $debug = !$opt{'quiet'} || $DEBUG;
2469   my $hascritical = 0;
2470
2471   warn "token_check called with opts\n".Dumper(\%opt) if $debug;
2472
2473   # force some explicitness when invoking this method
2474   die "token_check must run with queue flag if run with daily flag"
2475     if $opt{'daily'} && !$opt{'queue'};
2476
2477   my $conf = FS::Conf->new;
2478
2479   my $log = FS::Log->new('FS::cust_main::Billing_Realtime::token_check');
2480
2481   my $cache = {}; #cache for module info
2482
2483   # look for a gateway that can and can't tokenize
2484   my $require_tokenized = 1;
2485   my $someone_tokenizing = 0;
2486   foreach my $gateway (
2487     FS::payment_gateway->all_gateways(
2488       'method'  => 'CC',
2489       'conf'    => $conf,
2490       'nofatal' => 1,
2491     )
2492   ) {
2493     if (!$gateway) {
2494       # no default gateway, no promise to tokenize
2495       # can just load other gateways as-needeed below
2496       $require_tokenized = 0;
2497       last if $someone_tokenizing;
2498       next;
2499     }
2500     my $info = _token_check_gateway_info($cache,$gateway);
2501     die $info unless ref($info); # means it's an error message
2502     if ($info->{'can_tokenize'}) {
2503       $someone_tokenizing = 1;
2504     } else {
2505       # a configured gateway can't tokenize, that's all we need to know right now
2506       # can just load other gateways as-needeed below
2507       $require_tokenized = 0;
2508       last if $someone_tokenizing;
2509     }
2510   }
2511
2512   unless ($someone_tokenizing) { #no need to check, if no one can tokenize
2513     warn "no gateways tokenize\n" if $debug;
2514     return;
2515   }
2516
2517   warn "REQUIRE TOKENIZED" if $require_tokenized && $debug;
2518
2519   # upgrade does not call this with autocommit turned on,
2520   # and autocommit will be ignored if opt queue is set,
2521   # but might as well be thorough...
2522   my $oldAutoCommit = $FS::UID::AutoCommit;
2523   local $FS::UID::AutoCommit = 0;
2524   my $dbh = dbh;
2525
2526   # for retrieving data in chunks
2527   my $step = 500;
2528   my $offset = 0;
2529
2530   ### Tokenize cust_payby
2531
2532   my @recnums;
2533
2534 CUSTLOOP:
2535   while (my $custnum = _token_check_next_recnum($dbh,'cust_main',$step,\$offset,\@recnums)) {
2536     my $cust_main = FS::cust_main->by_key($custnum);
2537     my $payment_gateway;
2538     foreach my $cust_payby ($cust_main->cust_payby('CARD','DCRD')) {
2539
2540       # see if it's already tokenized
2541       if ($cust_payby->tokenized) {
2542         warn "cust_payby ".$cust_payby->get($cust_payby->primary_key)." already tokenized" if $debug;
2543         next;
2544       }
2545
2546       if ($require_tokenized && $opt{'daily'}) {
2547         $log->info("Untokenized card number detected in cust_payby ".$cust_payby->custpaybynum. '; tokenizing');
2548         $dbh->commit or die $dbh->errstr; # commit log message
2549       }
2550
2551       # only load gateway if we need to, and only need to load it once
2552       $payment_gateway ||= $cust_main->_payment_gateway({
2553         'method'  => 'CC',
2554         'conf'    => $conf,
2555         'nofatal' => 1, # handle lack of gateway smoothly below
2556       });
2557       unless ($payment_gateway) {
2558         # no reason to have untokenized card numbers saved if no gateway,
2559         #   but only a problem if we expected everyone to tokenize card numbers
2560         unless ($require_tokenized) {
2561           warn "Skipping cust_payby for cust_main ".$cust_main->custnum.", no payment gateway" if $debug;
2562           next CUSTLOOP; # can skip rest of customer
2563         }
2564         my $error = "No gateway found for custnum ".$cust_main->custnum;
2565         if ($opt{'queue'}) {
2566           $hascritical = 1;
2567           $log->critical($error);
2568           $dbh->commit or die $dbh->errstr; # commit error message
2569           next; # not next CUSTLOOP, want to record error for every cust_payby
2570         }
2571         $dbh->rollback if $oldAutoCommit;
2572         die $error;
2573       }
2574
2575       my $info = _token_check_gateway_info($cache,$payment_gateway);
2576       unless (ref($info)) {
2577         # only throws error if Business::OnlinePayment won't load,
2578         #   which is just cause to abort this whole process, even if queue
2579         $dbh->rollback if $oldAutoCommit;
2580         die $info; # error message
2581       }
2582       # no fail here--a configured gateway can't tokenize, so be it
2583       unless ($info->{'can_tokenize'}) {
2584         warn "Skipping ".$cust_main->custnum." cannot tokenize" if $debug;
2585         next;
2586       }
2587
2588       # time to tokenize
2589       $cust_payby = $cust_payby->select_for_update;
2590       my %tokenopts = (
2591         'payment_gateway' => $payment_gateway,
2592         'cust_payby'      => $cust_payby,
2593       );
2594       my $error = $cust_main->realtime_tokenize(\%tokenopts);
2595       if ($cust_payby->tokenized) { # implies no error
2596         $error = $cust_payby->replace;
2597       } else {
2598         $error ||= 'Unknown error';
2599       }
2600       if ($error) {
2601         $error = "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error;
2602         if ($opt{'queue'}) {
2603           $hascritical = 1;
2604           $log->critical($error);
2605           $dbh->commit or die $dbh->errstr; # commit log message, release mutex
2606           next; # not next CUSTLOOP, want to record error for every cust_payby
2607         }
2608         $dbh->rollback if $oldAutoCommit;
2609         die $error;
2610       }
2611       $dbh->commit or die $dbh->errstr if $opt{'queue'}; # release mutex
2612       warn "TOKENIZED cust_payby ".$cust_payby->get($cust_payby->primary_key) if $debug;
2613     }
2614     warn "cust_payby upgraded for custnum ".$cust_main->custnum if $debug;
2615
2616   }
2617
2618   ### Tokenize/mask transaction tables
2619
2620   # allow tokenization of closed cust_pay/cust_refund records
2621   local $FS::payinfo_Mixin::allow_closed_replace = 1;
2622
2623   # grep assistance:
2624   #   $cust_pay_pending->replace, $cust_pay->replace, $cust_pay_void->replace, $cust_refund->replace all run here
2625   foreach my $table ( qw(cust_pay_pending cust_pay cust_pay_void cust_refund) ) {
2626     warn "Checking $table" if $debug;
2627
2628     # FS::Cursor does not seem to work over multiple commits (gives cursor not found errors)
2629     # loading only record ids, then loading individual records one at a time
2630     my $tclass = 'FS::'.$table;
2631     $offset = 0;
2632     @recnums = ();
2633
2634     while (my $recnum = _token_check_next_recnum($dbh,$table,$step,\$offset,\@recnums)) {
2635       my $record = $tclass->by_key($recnum);
2636       unless ($record->payby eq 'CARD') {
2637         warn "Skipping non-card record for $table ".$record->get($record->primary_key) if $debug;
2638         next;
2639       }
2640       if (FS::cust_main::Billing_Realtime->tokenized($record->payinfo)) {
2641         warn "Skipping tokenized record for $table ".$record->get($record->primary_key) if $debug;
2642         next;
2643       }
2644       if (!$record->payinfo) { #shouldn't happen, but at least it's not a card number
2645         warn "Skipping blank payinfo for $table ".$record->get($record->primary_key) if $debug;
2646         next;
2647       }
2648       if ($record->payinfo =~ /N\/A/) { # ??? Not sure why we do this, but it's not a card number
2649         warn "Skipping NA payinfo for $table ".$record->get($record->primary_key) if $debug;
2650         next;
2651       }
2652
2653       if ($require_tokenized && $opt{'daily'}) {
2654         $log->info("Untokenized card number detected in $table ".$record->get($record->primary_key). ';tokenizing');
2655         $dbh->commit or die $dbh->errstr; # commit log message
2656       }
2657
2658       my $cust_main = $record->cust_main;
2659       if (!$cust_main) {
2660         # might happen for cust_pay_pending from failed verify records,
2661         #   in which case we attempt tokenization without cust_main
2662         # everything else should absolutely have a cust_main
2663         if ($table eq 'cust_pay_pending' and !$record->custnum ) {
2664           # override the usual safety check and allow the record to be
2665           # updated even without a custnum.
2666           $record->set('custnum_pending', 1);
2667         } else {
2668           my $error = "Could not load cust_main for $table ".$record->get($record->primary_key);
2669           if ($opt{'queue'}) {
2670             $hascritical = 1;
2671             $log->critical($error);
2672             $dbh->commit or die $dbh->errstr; # commit log message
2673             next;
2674           }
2675           $dbh->rollback if $oldAutoCommit;
2676           die $error;
2677         }
2678       }
2679
2680       my $gateway;
2681
2682       # use the gatewaynum specified by the record if possible
2683       $gateway = FS::payment_gateway->by_key_with_namespace(
2684         'gatewaynum' => $record->gatewaynum,
2685       ) if $record->gateway;
2686
2687       # otherwise use the cust agent gateway if possible (which realtime_refund_bop would do)
2688       # otherwise just use default gateway
2689       unless ($gateway) {
2690
2691         $gateway = $cust_main 
2692                  ? $cust_main->agent->payment_gateway
2693                  : FS::payment_gateway->default_gateway;
2694
2695         # check for processor mismatch
2696         unless ($table eq 'cust_pay_pending') { # has no processor table
2697           if (my $processor = $record->processor) {
2698
2699             my $conf_processor = $gateway->gateway_module;
2700             my %bop_options = $gateway->gatewaynum
2701                             ? $gateway->options
2702                             : @{ $gateway->get('options') };
2703
2704             # this is the same standard used by realtime_refund_bop
2705             unless (
2706               ($processor eq $conf_processor) ||
2707               (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'}))
2708             ) {
2709
2710               # processors don't match, so refund already cannot be run on this object,
2711               # regardless of what we do now...
2712               # but unless we gotta tokenize everything, just leave well enough alone
2713               unless ($require_tokenized) {
2714                 warn "Skipping mismatched processor for $table ".$record->get($record->primary_key) if $debug;
2715                 next;
2716               }
2717               ### no error--we'll tokenize using the new gateway, just to remove stored payinfo,
2718               ### because refunds are already impossible for this record, anyway
2719
2720             } # end processor mismatch
2721
2722           } # end record has processor
2723         } # end not cust_pay_pending
2724
2725       }
2726
2727       # means no default gateway, no promise to tokenize, can skip
2728       unless ($gateway) {
2729         warn "Skipping missing gateway for $table ".$record->get($record->primary_key) if $debug;
2730         next;
2731       }
2732
2733       my $info = _token_check_gateway_info($cache,$gateway);
2734       unless (ref($info)) {
2735         # only throws error if Business::OnlinePayment won't load,
2736         #   which is just cause to abort this whole process, even if queue
2737         $dbh->rollback if $oldAutoCommit;
2738         die $info; # error message
2739       }
2740
2741       # a configured gateway can't tokenize, move along
2742       unless ($info->{'can_tokenize'}) {
2743         warn "Skipping, cannot tokenize $table ".$record->get($record->primary_key) if $debug;
2744         next;
2745       }
2746
2747       warn "ATTEMPTING GATEWAY-ONLY TOKENIZE" if $debug && !$cust_main;
2748
2749       # if we got this far, time to mutex
2750       $record->select_for_update;
2751
2752       # no clear record of name/address/etc used for transaction,
2753       # but will load name/phone/id from customer if run as an object method,
2754       # so we try that if we can
2755       my %tokenopts = (
2756         'payment_gateway' => $gateway,
2757         'method'          => 'CC',
2758         'payinfo'         => $record->payinfo,
2759         'paydate'         => $record->paydate,
2760       );
2761       my $error = $cust_main
2762                 ? $cust_main->realtime_tokenize(\%tokenopts)
2763                 : FS::cust_main::Billing_Realtime->realtime_tokenize(\%tokenopts);
2764       if (FS::cust_main::Billing_Realtime->tokenized($tokenopts{'payinfo'})) { # implies no error
2765         $record->payinfo($tokenopts{'payinfo'});
2766         $error = $record->replace;
2767       } else {
2768         $error ||= 'Unknown error';
2769       }
2770       if ($error) {
2771         $error = "Error tokenizing $table ".$record->get($record->primary_key).": ".$error;
2772         if ($opt{'queue'}) {
2773           $hascritical = 1;
2774           $log->critical($error);
2775           $dbh->commit or die $dbh->errstr; # commit log message, release mutex
2776           next;
2777         }
2778         $dbh->rollback if $oldAutoCommit;
2779         die $error;
2780       }
2781       $dbh->commit or die $dbh->errstr if $opt{'queue'}; # release mutex
2782       warn "TOKENIZED $table ".$record->get($record->primary_key) if $debug;
2783
2784     } # end record loop
2785   } # end table loop
2786
2787   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2788
2789   return $hascritical ? 'Critical errors occurred on some records, see system log' : '';
2790 }
2791
2792 # not a method!
2793 sub _token_check_next_recnum {
2794   my ($dbh,$table,$step,$offset,$recnums) = @_;
2795   my $recnum = shift @$recnums;
2796   return $recnum if $recnum;
2797   my $tclass = 'FS::'.$table;
2798   my $sth = $dbh->prepare(
2799     'SELECT '.$tclass->primary_key.
2800     ' FROM '.$table.
2801     " WHERE ( is_tokenized IS NULL OR is_tokenized = '' ) ".
2802     ' ORDER BY '.$tclass->primary_key.
2803     ' LIMIT '.$step.
2804     ' OFFSET '.$$offset
2805   ) or die $dbh->errstr;
2806   $sth->execute() or die $sth->errstr;
2807   my @recnums;
2808   while (my $rec = $sth->fetchrow_hashref) {
2809     push @$recnums, $rec->{$tclass->primary_key};
2810   }
2811   $sth->finish();
2812   $$offset += $step;
2813   return shift @$recnums;
2814 }
2815
2816 # not a method!
2817 sub _token_check_gateway_info {
2818   my ($cache,$payment_gateway) = @_;
2819
2820   return $cache->{$payment_gateway->gateway_module}
2821     if $cache->{$payment_gateway->gateway_module};
2822
2823   my $info = {};
2824   $cache->{$payment_gateway->gateway_module} = $info;
2825
2826   my $namespace = $payment_gateway->gateway_namespace;
2827   return $info unless $namespace eq 'Business::OnlinePayment';
2828   $info->{'is_bop'} = 1;
2829
2830   # only need to load this once,
2831   # don't want to load if nothing is_bop
2832   unless ($cache->{'Business::OnlinePayment'}) {
2833     eval "use $namespace";  
2834     return "Error initializing Business:OnlinePayment: ".$@ if $@;
2835     $cache->{'Business::OnlinePayment'} = 1;
2836   }
2837
2838   my $transaction = new $namespace( $payment_gateway->gateway_module,
2839                                     _bop_options({ 'payment_gateway' => $payment_gateway }),
2840                                   );
2841
2842   return $info unless $transaction->can('info');
2843   $info->{'can_info'} = 1;
2844
2845   my %supported_actions = $transaction->info('supported_actions');
2846   $info->{'can_tokenize'} = 1
2847     if $supported_actions{'CC'}
2848       && grep /^Tokenize$/, @{$supported_actions{'CC'}};
2849
2850   # not using this any more, but for future reference...
2851   $info->{'void_requires_card'} = 1
2852     if $transaction->info('CC_void_requires_card');
2853
2854   return $info;
2855 }
2856
2857 =back
2858
2859 =head1 BUGS
2860
2861 =head1 SEE ALSO
2862
2863 L<FS::cust_main>, L<FS::cust_main::Billing>
2864
2865 =cut
2866
2867 1;