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