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