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