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