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