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