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