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