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