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