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