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