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