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