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