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