ae41c70c4c26b97e39fbd9673876b548f689c4e4
[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
378     $cc_surcharge = $options{'amount'} - (($options{'amount'} - $cc_surcharge_flat) / ( 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
901     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
902
903     if ( $error ) {
904       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
905       $cust_pay->invnum(''); #try again with no specific invnum
906       $cust_pay->paynum('');
907       my $error2 = $cust_pay->insert( $options{'manual'} ?
908                                       ( 'manual' => 1 ) : ()
909                                     );
910       if ( $error2 ) {
911         # gah.  but at least we have a record of the state we had to abort in
912         # from cust_pay_pending now.
913         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
914         my $e = "WARNING: $options{method} captured but payment not recorded -".
915                 " error inserting payment (". $payment_gateway->gateway_module.
916                 "): $error2".
917                 " (previously tried insert with invnum #$options{'invnum'}" .
918                 ": $error ) - pending payment saved as paypendingnum ".
919                 $cust_pay_pending->paypendingnum. "\n";
920         warn $e;
921         return $e;
922       }
923     }
924
925     my $jobnum = $cust_pay_pending->jobnum;
926     if ( $jobnum ) {
927        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
928       
929        unless ( $placeholder ) {
930          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
931          my $e = "WARNING: $options{method} captured but job $jobnum not ".
932              "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
933          warn $e;
934          return $e;
935        }
936
937        $error = $placeholder->delete;
938
939        if ( $error ) {
940          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
941          my $e = "WARNING: $options{method} captured but could not delete ".
942               "job $jobnum for paypendingnum ".
943               $cust_pay_pending->paypendingnum. ": $error\n";
944          warn $e;
945          return $e;
946        }
947
948     }
949     
950     if ( $options{'paynum_ref'} ) {
951       ${ $options{'paynum_ref'} } = $cust_pay->paynum;
952     }
953
954     $cust_pay_pending->status('done');
955     $cust_pay_pending->statustext('captured');
956     $cust_pay_pending->paynum($cust_pay->paynum);
957     my $cpp_done_err = $cust_pay_pending->replace;
958
959     if ( $cpp_done_err ) {
960
961       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
962       my $e = "WARNING: $options{method} captured but payment not recorded - ".
963               "error updating status for paypendingnum ".
964               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
965       warn $e;
966       return $e;
967
968     } else {
969
970       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
971
972       if ( $options{'apply'} ) {
973         my $apply_error = $self->apply_payments_and_credits;
974         if ( $apply_error ) {
975           warn "WARNING: error applying payment: $apply_error\n";
976           #but we still should return no error cause the payment otherwise went
977           #through...
978         }
979       }
980
981       # have a CC surcharge portion --> one-time charge
982       if ( $options{'cc_surcharge'} > 0 ) { 
983             # XXX: this whole block needs to be in a transaction?
984
985           my $invnum;
986           $invnum = $options{'invnum'} if $options{'invnum'};
987           unless ( $invnum ) { # probably from a payment screen
988              # do we have any open invoices? pick earliest
989              # uses the fact that cust_main->cust_bill sorts by date ascending
990              my @open = $self->open_cust_bill;
991              $invnum = $open[0]->invnum if scalar(@open);
992           }
993             
994           unless ( $invnum ) {  # still nothing? pick last closed invoice
995              # again uses fact that cust_main->cust_bill sorts by date ascending
996              my @closed = $self->cust_bill;
997              $invnum = $closed[$#closed]->invnum if scalar(@closed);
998           }
999
1000           unless ( $invnum ) {
1001             # XXX: unlikely case - pre-paying before any invoices generated
1002             # what it should do is create a new invoice and pick it
1003                 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1004                 return '';
1005           }
1006
1007           my $cust_pkg;
1008           my $charge_error = $self->charge({
1009                                     'amount'    => $options{'cc_surcharge'},
1010                                     'pkg'       => 'Credit Card Surcharge',
1011                                     'setuptax'  => 'Y',
1012                                     'cust_pkg_ref' => \$cust_pkg,
1013                                 });
1014           if($charge_error) {
1015                 warn 'Unable to add CC surcharge cust_pkg';
1016                 return '';
1017           }
1018
1019           $cust_pkg->setup(time);
1020           my $cp_error = $cust_pkg->replace;
1021           if($cp_error) {
1022               warn 'Unable to set setup time on cust_pkg for cc surcharge';
1023             # but keep going...
1024           }
1025                                     
1026           my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1027           unless ( $cust_bill ) {
1028               warn "race condition + invoice deletion just happened";
1029               return '';
1030           }
1031
1032           my $grand_error = 
1033             $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1034
1035           warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1036             if $grand_error;
1037       }
1038
1039       return ''; #no error
1040
1041     }
1042
1043   } else {
1044
1045     my $perror = $payment_gateway->gateway_module. " error: ".
1046       $transaction->error_message;
1047
1048     my $jobnum = $cust_pay_pending->jobnum;
1049     if ( $jobnum ) {
1050        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1051       
1052        if ( $placeholder ) {
1053          my $error = $placeholder->depended_delete;
1054          $error ||= $placeholder->delete;
1055          warn "error removing provisioning jobs after declined paypendingnum ".
1056            $cust_pay_pending->paypendingnum. ": $error\n";
1057        } else {
1058          my $e = "error finding job $jobnum for declined paypendingnum ".
1059               $cust_pay_pending->paypendingnum. "\n";
1060          warn $e;
1061        }
1062
1063     }
1064     
1065     unless ( $transaction->error_message ) {
1066
1067       my $t_response;
1068       if ( $transaction->can('response_page') ) {
1069         $t_response = {
1070                         'page'    => ( $transaction->can('response_page')
1071                                          ? $transaction->response_page
1072                                          : ''
1073                                      ),
1074                         'code'    => ( $transaction->can('response_code')
1075                                          ? $transaction->response_code
1076                                          : ''
1077                                      ),
1078                         'headers' => ( $transaction->can('response_headers')
1079                                          ? $transaction->response_headers
1080                                          : ''
1081                                      ),
1082                       };
1083       } else {
1084         $t_response .=
1085           "No additional debugging information available for ".
1086             $payment_gateway->gateway_module;
1087       }
1088
1089       $perror .= "No error_message returned from ".
1090                    $payment_gateway->gateway_module. " -- ".
1091                  ( ref($t_response) ? Dumper($t_response) : $t_response );
1092
1093     }
1094
1095     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1096          && $conf->exists('emaildecline', $self->agentnum)
1097          && grep { $_ ne 'POST' } $self->invoicing_list
1098          && ! grep { $transaction->error_message =~ /$_/ }
1099                    $conf->config('emaildecline-exclude', $self->agentnum)
1100     ) {
1101
1102       # Send a decline alert to the customer.
1103       my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1104       my $error = '';
1105       if ( $msgnum ) {
1106         # include the raw error message in the transaction state
1107         $cust_pay_pending->setfield('error', $transaction->error_message);
1108         my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1109         $error = $msg_template->send( 'cust_main' => $self,
1110                                       'object'    => $cust_pay_pending );
1111       }
1112       else { #!$msgnum
1113
1114         my @templ = $conf->config('declinetemplate');
1115         my $template = new Text::Template (
1116           TYPE   => 'ARRAY',
1117           SOURCE => [ map "$_\n", @templ ],
1118         ) or return "($perror) can't create template: $Text::Template::ERROR";
1119         $template->compile()
1120           or return "($perror) can't compile template: $Text::Template::ERROR";
1121
1122         my $templ_hash = {
1123           'company_name'    =>
1124             scalar( $conf->config('company_name', $self->agentnum ) ),
1125           'company_address' =>
1126             join("\n", $conf->config('company_address', $self->agentnum ) ),
1127           'error'           => $transaction->error_message,
1128         };
1129
1130         my $error = send_email(
1131           'from'    => $conf->invoice_from_full( $self->agentnum ),
1132           'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1133           'subject' => 'Your payment could not be processed',
1134           'body'    => [ $template->fill_in(HASH => $templ_hash) ],
1135         );
1136       }
1137
1138       $perror .= " (also received error sending decline notification: $error)"
1139         if $error;
1140
1141     }
1142
1143     $cust_pay_pending->status('done');
1144     $cust_pay_pending->statustext("declined: $perror");
1145     my $cpp_done_err = $cust_pay_pending->replace;
1146     if ( $cpp_done_err ) {
1147       my $e = "WARNING: $options{method} declined but pending payment not ".
1148               "resolved - error updating status for paypendingnum ".
1149               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1150       warn $e;
1151       $perror = "$e ($perror)";
1152     }
1153
1154     return $perror;
1155   }
1156
1157 }
1158
1159 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1160
1161 Verifies successful third party processing of a realtime credit card or
1162 ACH (electronic check) transaction via a
1163 Business::OnlineThirdPartyPayment realtime gateway.  See
1164 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1165
1166 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1167
1168 The additional options I<payname>, I<city>, I<state>,
1169 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1170 if set, will override the value from the customer record.
1171
1172 I<description> is a free-text field passed to the gateway.  It defaults to
1173 "Internet services".
1174
1175 If an I<invnum> is specified, this payment (if successful) is applied to the
1176 specified invoice.  If you don't specify an I<invnum> you might want to
1177 call the B<apply_payments> method.
1178
1179 I<quiet> can be set true to surpress email decline notices.
1180
1181 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
1182 resulting paynum, if any.
1183
1184 I<payunique> is a unique identifier for this payment.
1185
1186 Returns a hashref containing elements bill_error (which will be undefined
1187 upon success) and session_id of any associated session.
1188
1189 =cut
1190
1191 sub realtime_botpp_capture {
1192   my( $self, $cust_pay_pending, %options ) = @_;
1193
1194   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1195
1196   if ( $DEBUG ) {
1197     warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1198     warn "  $_ => $options{$_}\n" foreach keys %options;
1199   }
1200
1201   eval "use Business::OnlineThirdPartyPayment";  
1202   die $@ if $@;
1203
1204   ###
1205   # select the gateway
1206   ###
1207
1208   my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1209
1210   my $payment_gateway;
1211   my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1212   $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1213                 { gatewaynum => $gatewaynum }
1214               )
1215     : $self->agent->payment_gateway( 'method' => $method,
1216                                      # 'invnum'  => $cust_pay_pending->invnum,
1217                                      # 'payinfo' => $cust_pay_pending->payinfo,
1218                                    );
1219
1220   $options{payment_gateway} = $payment_gateway; # for the helper subs
1221
1222   ###
1223   # massage data
1224   ###
1225
1226   my @invoicing_list = $self->invoicing_list_emailonly;
1227   if ( $conf->exists('emailinvoiceautoalways')
1228        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1229        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1230     push @invoicing_list, $self->all_emails;
1231   }
1232
1233   my $email = ($conf->exists('business-onlinepayment-email-override'))
1234               ? $conf->config('business-onlinepayment-email-override')
1235               : $invoicing_list[0];
1236
1237   my %content = ();
1238
1239   $content{email_customer} = 
1240     (    $conf->exists('business-onlinepayment-email_customer')
1241       || $conf->exists('business-onlinepayment-email-override') );
1242       
1243   ###
1244   # run transaction(s)
1245   ###
1246
1247   my $transaction =
1248     new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1249                                            $self->_bop_options(\%options),
1250                                          );
1251
1252   $transaction->reference({ %options }); 
1253
1254   $transaction->content(
1255     'type'           => $method,
1256     $self->_bop_auth(\%options),
1257     'action'         => 'Post Authorization',
1258     'description'    => $options{'description'},
1259     'amount'         => $cust_pay_pending->paid,
1260     #'invoice_number' => $options{'invnum'},
1261     'customer_id'    => $self->custnum,
1262
1263     #3.0 is a good a time as any to get rid of this... add a config to pass it
1264     # if anyone still needs it
1265     #'referer'        => 'http://cleanwhisker.420.am/',
1266
1267     'reference'      => $cust_pay_pending->paypendingnum,
1268     'email'          => $email,
1269     'phone'          => $self->daytime || $self->night,
1270     %content, #after
1271     # plus whatever is required for bogus capture avoidance
1272   );
1273
1274   $transaction->submit();
1275
1276   my $error =
1277     $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1278
1279   if ( $options{'apply'} ) {
1280     my $apply_error = $self->apply_payments_and_credits;
1281     if ( $apply_error ) {
1282       warn "WARNING: error applying payment: $apply_error\n";
1283     }
1284   }
1285
1286   return {
1287     bill_error => $error,
1288     session_id => $cust_pay_pending->session_id,
1289   }
1290
1291 }
1292
1293 =item default_payment_gateway
1294
1295 DEPRECATED -- use agent->payment_gateway
1296
1297 =cut
1298
1299 sub default_payment_gateway {
1300   my( $self, $method ) = @_;
1301
1302   die "Real-time processing not enabled\n"
1303     unless $conf->exists('business-onlinepayment');
1304
1305   #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1306
1307   #load up config
1308   my $bop_config = 'business-onlinepayment';
1309   $bop_config .= '-ach'
1310     if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1311   my ( $processor, $login, $password, $action, @bop_options ) =
1312     $conf->config($bop_config);
1313   $action ||= 'normal authorization';
1314   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1315   die "No real-time processor is enabled - ".
1316       "did you set the business-onlinepayment configuration value?\n"
1317     unless $processor;
1318
1319   ( $processor, $login, $password, $action, @bop_options )
1320 }
1321
1322 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1323
1324 Refunds a realtime credit card or ACH (electronic check) transaction
1325 via a Business::OnlinePayment realtime gateway.  See
1326 L<http://420.am/business-onlinepayment> for supported gateways.
1327
1328 Available methods are: I<CC> or I<ECHECK>
1329
1330 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1331
1332 Most gateways require a reference to an original payment transaction to refund,
1333 so you probably need to specify a I<paynum>.
1334
1335 I<amount> defaults to the original amount of the payment if not specified.
1336
1337 I<reasonnum> specifies a reason for the refund.
1338
1339 I<paydate> specifies the expiration date for a credit card overriding the
1340 value from the customer record or the payment record. Specified as yyyy-mm-dd
1341
1342 Implementation note: If I<amount> is unspecified or equal to the amount of the
1343 orignal payment, first an attempt is made to "void" the transaction via
1344 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1345 the normal attempt is made to "refund" ("credit") the transaction via the
1346 gateway is attempted. No attempt to "void" the transaction is made if the 
1347 gateway has introspection data and doesn't support void.
1348
1349 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1350 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1351 #if set, will override the value from the customer record.
1352
1353 #If an I<invnum> is specified, this payment (if successful) is applied to the
1354 #specified invoice.  If you don't specify an I<invnum> you might want to
1355 #call the B<apply_payments> method.
1356
1357 =cut
1358
1359 #some false laziness w/realtime_bop, not enough to make it worth merging
1360 #but some useful small subs should be pulled out
1361 sub realtime_refund_bop {
1362   my $self = shift;
1363
1364   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1365
1366   my %options = ();
1367   if (ref($_[0]) eq 'HASH') {
1368     %options = %{$_[0]};
1369   } else {
1370     my $method = shift;
1371     %options = @_;
1372     $options{method} = $method;
1373   }
1374
1375   my ($reason, $reason_text);
1376   if ( $options{'reasonnum'} ) {
1377     # do this here, because we need the plain text reason string in case we
1378     # void the payment
1379     $reason = FS::reason->by_key($options{'reasonnum'});
1380     $reason_text = $reason->reason;
1381   } else {
1382     # support old 'reason' string parameter in case it's still used,
1383     # or else set a default
1384     $reason_text = $options{'reason'} || 'card or ACH refund';
1385     local $@;
1386     $reason = FS::reason->new_or_existing(
1387       reason  => $reason_text,
1388       type    => 'Refund reason',
1389       class   => 'F',
1390     );
1391     if ($@) {
1392       return "failed to add refund reason: $@";
1393     }
1394   }
1395
1396   if ( $DEBUG ) {
1397     warn "$me realtime_refund_bop (new): $options{method} refund\n";
1398     warn "  $_ => $options{$_}\n" foreach keys %options;
1399   }
1400
1401   my %content = ();
1402
1403   ###
1404   # look up the original payment and optionally a gateway for that payment
1405   ###
1406
1407   my $cust_pay = '';
1408   my $amount = $options{'amount'};
1409
1410   my( $processor, $login, $password, @bop_options, $namespace ) ;
1411   my( $auth, $order_number ) = ( '', '', '' );
1412   my $gatewaynum = '';
1413
1414   if ( $options{'paynum'} ) {
1415
1416     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
1417     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1418       or return "Unknown paynum $options{'paynum'}";
1419     $amount ||= $cust_pay->paid;
1420
1421     my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1422     $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1423
1424     if ( $cust_pay->get('processor') ) {
1425       ($gatewaynum, $processor, $auth, $order_number) =
1426       (
1427         $cust_pay->gatewaynum,
1428         $cust_pay->processor,
1429         $cust_pay->auth,
1430         $cust_pay->order_number,
1431       );
1432     } else {
1433       # this payment wasn't upgraded, which probably means this won't work,
1434       # but try it anyway
1435       $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1436         or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1437                   $cust_pay->paybatch;
1438       ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1439     }
1440
1441     if ( $gatewaynum ) { #gateway for the payment to be refunded
1442
1443       my $payment_gateway =
1444         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1445       die "payment gateway $gatewaynum not found"
1446         unless $payment_gateway;
1447
1448       $processor   = $payment_gateway->gateway_module;
1449       $login       = $payment_gateway->gateway_username;
1450       $password    = $payment_gateway->gateway_password;
1451       $namespace   = $payment_gateway->gateway_namespace;
1452       @bop_options = $payment_gateway->options;
1453
1454     } else { #try the default gateway
1455
1456       my $conf_processor;
1457       my $payment_gateway =
1458         $self->agent->payment_gateway('method' => $options{method});
1459
1460       ( $conf_processor, $login, $password, $namespace ) =
1461         map { my $method = "gateway_$_"; $payment_gateway->$method }
1462           qw( module username password namespace );
1463
1464       @bop_options = $payment_gateway->gatewaynum
1465                        ? $payment_gateway->options
1466                        : @{ $payment_gateway->get('options') };
1467
1468       return "processor of payment $options{'paynum'} $processor does not".
1469              " match default processor $conf_processor"
1470         unless $processor eq $conf_processor;
1471
1472     }
1473
1474
1475   } else { # didn't specify a paynum, so look for agent gateway overrides
1476            # like a normal transaction 
1477  
1478     my $payment_gateway =
1479       $self->agent->payment_gateway( 'method'  => $options{method},
1480                                      #'payinfo' => $payinfo,
1481                                    );
1482     my( $processor, $login, $password, $namespace ) =
1483       map { my $method = "gateway_$_"; $payment_gateway->$method }
1484         qw( module username password namespace );
1485
1486     my @bop_options = $payment_gateway->gatewaynum
1487                         ? $payment_gateway->options
1488                         : @{ $payment_gateway->get('options') };
1489
1490   }
1491   return "neither amount nor paynum specified" unless $amount;
1492
1493   eval "use $namespace";  
1494   die $@ if $@;
1495
1496   %content = (
1497     %content,
1498     'type'           => $options{method},
1499     'login'          => $login,
1500     'password'       => $password,
1501     'order_number'   => $order_number,
1502     'amount'         => $amount,
1503
1504     #3.0 is a good a time as any to get rid of this... add a config to pass it
1505     # if anyone still needs it
1506     #'referer'        => 'http://cleanwhisker.420.am/',
1507   );
1508   $content{authorization} = $auth
1509     if length($auth); #echeck/ACH transactions have an order # but no auth
1510                       #(at least with authorize.net)
1511
1512   my $currency =    $conf->exists('business-onlinepayment-currency')
1513                  && $conf->config('business-onlinepayment-currency');
1514   $content{currency} = $currency if $currency;
1515
1516   my $disable_void_after;
1517   if ($conf->exists('disable_void_after')
1518       && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1519     $disable_void_after = $1;
1520   }
1521
1522   #first try void if applicable
1523   my $void = new Business::OnlinePayment( $processor, @bop_options );
1524
1525   my $tryvoid = 1;
1526   if ($void->can('info')) {
1527       my $paytype = '';
1528       $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1529       $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1530       my %supported_actions = $void->info('supported_actions');
1531       $tryvoid = 0 
1532         if ( %supported_actions && $paytype 
1533                 && defined($supported_actions{$paytype}) 
1534                 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1535   }
1536
1537   if ( $cust_pay && $cust_pay->paid == $amount
1538     && (
1539       ( not defined($disable_void_after) )
1540       || ( time < ($cust_pay->_date + $disable_void_after ) )
1541     )
1542     && $tryvoid
1543   ) {
1544     warn "  attempting void\n" if $DEBUG > 1;
1545     if ( $void->can('info') ) {
1546       if ( $cust_pay->payby eq 'CARD'
1547            && $void->info('CC_void_requires_card') )
1548       {
1549         $content{'card_number'} = $cust_pay->payinfo;
1550       } elsif ( $cust_pay->payby eq 'CHEK'
1551                 && $void->info('ECHECK_void_requires_account') )
1552       {
1553         ( $content{'account_number'}, $content{'routing_code'} ) =
1554           split('@', $cust_pay->payinfo);
1555         $content{'name'} = $self->get('first'). ' '. $self->get('last');
1556       }
1557     }
1558     $void->content( 'action' => 'void', %content );
1559     $void->test_transaction(1)
1560       if $conf->exists('business-onlinepayment-test_transaction');
1561     $void->submit();
1562     if ( $void->is_success ) {
1563       my $error = $cust_pay->void($reason_text);
1564       if ( $error ) {
1565         # gah, even with transactions.
1566         my $e = 'WARNING: Card/ACH voided but database not updated - '.
1567                 "error voiding payment: $error";
1568         warn $e;
1569         return $e;
1570       }
1571       warn "  void successful\n" if $DEBUG > 1;
1572       return '';
1573     }
1574   }
1575
1576   warn "  void unsuccessful, trying refund\n"
1577     if $DEBUG > 1;
1578
1579   #massage data
1580   my $address = $self->address1;
1581   $address .= ", ". $self->address2 if $self->address2;
1582
1583   my($payname, $payfirst, $paylast);
1584   if ( $self->payname && $options{method} ne 'ECHECK' ) {
1585     $payname = $self->payname;
1586     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1587       or return "Illegal payname $payname";
1588     ($payfirst, $paylast) = ($1, $2);
1589   } else {
1590     $payfirst = $self->getfield('first');
1591     $paylast = $self->getfield('last');
1592     $payname =  "$payfirst $paylast";
1593   }
1594
1595   my @invoicing_list = $self->invoicing_list_emailonly;
1596   if ( $conf->exists('emailinvoiceautoalways')
1597        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1598        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1599     push @invoicing_list, $self->all_emails;
1600   }
1601
1602   my $email = ($conf->exists('business-onlinepayment-email-override'))
1603               ? $conf->config('business-onlinepayment-email-override')
1604               : $invoicing_list[0];
1605
1606   my $payip = exists($options{'payip'})
1607                 ? $options{'payip'}
1608                 : $self->payip;
1609   $content{customer_ip} = $payip
1610     if length($payip);
1611
1612   my $payinfo = '';
1613   if ( $options{method} eq 'CC' ) {
1614
1615     if ( $cust_pay ) {
1616       $content{card_number} = $payinfo = $cust_pay->payinfo;
1617       (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1618         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1619         ($content{expiration} = "$2/$1");  # where available
1620     } else {
1621       $content{card_number} = $payinfo = $self->payinfo;
1622       (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1623         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1624       $content{expiration} = "$2/$1";
1625     }
1626
1627   } elsif ( $options{method} eq 'ECHECK' ) {
1628
1629     if ( $cust_pay ) {
1630       $payinfo = $cust_pay->payinfo;
1631     } else {
1632       $payinfo = $self->payinfo;
1633     } 
1634     ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1635     $content{bank_name} = $self->payname;
1636     $content{account_type} = 'CHECKING';
1637     $content{account_name} = $payname;
1638     $content{customer_org} = $self->company ? 'B' : 'I';
1639     $content{customer_ssn} = $self->ss;
1640
1641   }
1642
1643   #then try refund
1644   my $refund = new Business::OnlinePayment( $processor, @bop_options );
1645   my %sub_content = $refund->content(
1646     'action'         => 'credit',
1647     'customer_id'    => $self->custnum,
1648     'last_name'      => $paylast,
1649     'first_name'     => $payfirst,
1650     'name'           => $payname,
1651     'address'        => $address,
1652     'city'           => $self->city,
1653     'state'          => $self->state,
1654     'zip'            => $self->zip,
1655     'country'        => $self->country,
1656     'email'          => $email,
1657     'phone'          => $self->daytime || $self->night,
1658     %content, #after
1659   );
1660   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
1661     if $DEBUG > 1;
1662   $refund->test_transaction(1)
1663     if $conf->exists('business-onlinepayment-test_transaction');
1664   $refund->submit();
1665
1666   return "$processor error: ". $refund->error_message
1667     unless $refund->is_success();
1668
1669   $order_number = $refund->order_number if $refund->can('order_number');
1670
1671   # change this to just use $cust_pay->delete_cust_bill_pay?
1672   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1673     my @cust_bill_pay = $cust_pay->cust_bill_pay;
1674     last unless @cust_bill_pay;
1675     my $cust_bill_pay = pop @cust_bill_pay;
1676     my $error = $cust_bill_pay->delete;
1677     last if $error;
1678   }
1679
1680   my $cust_refund = new FS::cust_refund ( {
1681     'custnum'  => $self->custnum,
1682     'paynum'   => $options{'paynum'},
1683     'source_paynum' => $options{'paynum'},
1684     'refund'   => $amount,
1685     '_date'    => '',
1686     'payby'    => $bop_method2payby{$options{method}},
1687     'payinfo'  => $payinfo,
1688     'reasonnum'   => $reason->reasonnum,
1689     'gatewaynum'    => $gatewaynum, # may be null
1690     'processor'     => $processor,
1691     'auth'          => $refund->authorization,
1692     'order_number'  => $order_number,
1693   } );
1694   my $error = $cust_refund->insert;
1695   if ( $error ) {
1696     $cust_refund->paynum(''); #try again with no specific paynum
1697     $cust_refund->source_paynum('');
1698     my $error2 = $cust_refund->insert;
1699     if ( $error2 ) {
1700       # gah, even with transactions.
1701       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1702               "error inserting refund ($processor): $error2".
1703               " (previously tried insert with paynum #$options{'paynum'}" .
1704               ": $error )";
1705       warn $e;
1706       return $e;
1707     }
1708   }
1709
1710   ''; #no error
1711
1712 }
1713
1714 =item realtime_verify_bop [ OPTION => VALUE ... ]
1715
1716 Runs an authorization-only transaction for $1 against this credit card (if
1717 successful, immediatly reverses the authorization).
1718
1719 Returns the empty string if the authorization was sucessful, or an error
1720 message otherwise.
1721
1722 I<payinfo>
1723
1724 I<payname>
1725
1726 I<paydate> specifies the expiration date for a credit card overriding the
1727 value from the customer record or the payment record. Specified as yyyy-mm-dd
1728
1729 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1730 #I<zip> are also available.  Any of these options,
1731 #if set, will override the value from the customer record.
1732
1733 =cut
1734
1735 #Available methods are: I<CC> or I<ECHECK>
1736
1737 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1738 #it worth merging but some useful small subs should be pulled out
1739 sub realtime_verify_bop {
1740   my $self = shift;
1741
1742   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1743   my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1744
1745   my %options = ();
1746   if (ref($_[0]) eq 'HASH') {
1747     %options = %{$_[0]};
1748   } else {
1749     %options = @_;
1750   }
1751
1752   if ( $DEBUG ) {
1753     warn "$me realtime_verify_bop\n";
1754     warn "  $_ => $options{$_}\n" foreach keys %options;
1755   }
1756
1757   ###
1758   # select a gateway
1759   ###
1760
1761   my $payment_gateway =  $self->_payment_gateway( \%options );
1762   my $namespace = $payment_gateway->gateway_namespace;
1763
1764   eval "use $namespace";  
1765   die $@ if $@;
1766
1767   ###
1768   # check for banned credit card/ACH
1769   ###
1770
1771   my $ban = FS::banned_pay->ban_search(
1772     'payby'   => $bop_method2payby{'CC'},
1773     'payinfo' => $options{payinfo} || $self->payinfo,
1774   );
1775   return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1776
1777   ###
1778   # massage data
1779   ###
1780
1781   my $bop_content = $self->_bop_content(\%options);
1782   return $bop_content unless ref($bop_content);
1783
1784   my @invoicing_list = $self->invoicing_list_emailonly;
1785   if ( $conf->exists('emailinvoiceautoalways')
1786        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1787        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1788     push @invoicing_list, $self->all_emails;
1789   }
1790
1791   my $email = ($conf->exists('business-onlinepayment-email-override'))
1792               ? $conf->config('business-onlinepayment-email-override')
1793               : $invoicing_list[0];
1794
1795   my $paydate = '';
1796   my %content = ();
1797
1798   if ( $namespace eq 'Business::OnlinePayment' ) {
1799
1800     if ( $options{method} eq 'CC' ) {
1801
1802       $content{card_number} = $options{payinfo} || $self->payinfo;
1803       $paydate = exists($options{'paydate'})
1804                       ? $options{'paydate'}
1805                       : $self->paydate;
1806       $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1807       $content{expiration} = "$2/$1";
1808
1809       my $paycvv = exists($options{'paycvv'})
1810                      ? $options{'paycvv'}
1811                      : $self->paycvv;
1812       $content{cvv2} = $paycvv
1813         if length($paycvv);
1814
1815       my $paystart_month = exists($options{'paystart_month'})
1816                              ? $options{'paystart_month'}
1817                              : $self->paystart_month;
1818
1819       my $paystart_year  = exists($options{'paystart_year'})
1820                              ? $options{'paystart_year'}
1821                              : $self->paystart_year;
1822
1823       $content{card_start} = "$paystart_month/$paystart_year"
1824         if $paystart_month && $paystart_year;
1825
1826       my $payissue       = exists($options{'payissue'})
1827                              ? $options{'payissue'}
1828                              : $self->payissue;
1829       $content{issue_number} = $payissue if $payissue;
1830
1831     } elsif ( $options{method} eq 'ECHECK' ){
1832
1833       #nop for checks (though it shouldn't be called...)
1834
1835     } else {
1836       die "unknown method ". $options{method};
1837     }
1838
1839   } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1840     #move along
1841   } else {
1842     die "unknown namespace $namespace";
1843   }
1844
1845   ###
1846   # run transaction(s)
1847   ###
1848
1849   my $error;
1850   my $transaction; #need this back so we can do _tokenize_card
1851   # don't mutex the customer here, because they might be uncommitted. and
1852   # this is only verification. it doesn't matter if they have other
1853   # unfinished verifications.
1854
1855   my $cust_pay_pending = new FS::cust_pay_pending {
1856     'custnum_pending'   => 1,
1857     'paid'              => '1.00',
1858     '_date'             => '',
1859     'payby'             => $bop_method2payby{'CC'},
1860     'payinfo'           => $options{payinfo} || $self->payinfo,
1861     'paymask'           => $options{paymask} || $self->paymask,
1862     'paydate'           => $paydate,
1863     #'recurring_billing' => $content{recurring_billing},
1864     'pkgnum'            => $options{'pkgnum'},
1865     'status'            => 'new',
1866     'gatewaynum'        => $payment_gateway->gatewaynum || '',
1867     'session_id'        => $options{session_id} || '',
1868     #'jobnum'            => $options{depend_jobnum} || '',
1869   };
1870   $cust_pay_pending->payunique( $options{payunique} )
1871     if defined($options{payunique}) && length($options{payunique});
1872
1873   IMMEDIATE: {
1874     # open a separate handle for creating/updating the cust_pay_pending
1875     # record
1876     local $FS::UID::dbh = myconnect();
1877     local $FS::UID::AutoCommit = 1;
1878
1879     # if this is an existing customer (and we can tell now because
1880     # this is a fresh transaction), it's safe to assign their custnum
1881     # to the cust_pay_pending record, and then the verification attempt
1882     # will remain linked to them even if it fails.
1883     if ( FS::cust_main->by_key($self->custnum) ) {
1884       $cust_pay_pending->set('custnum', $self->custnum);
1885     }
1886
1887     warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1888       if $DEBUG > 1;
1889
1890     # if this fails, just return; everything else will still allow the
1891     # cust_pay_pending to have its custnum set later
1892     my $cpp_new_err = $cust_pay_pending->insert;
1893     return $cpp_new_err if $cpp_new_err;
1894
1895     warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1896       if $DEBUG > 1;
1897     warn Dumper($cust_pay_pending) if $DEBUG > 2;
1898
1899     $transaction = new $namespace( $payment_gateway->gateway_module,
1900                                    $self->_bop_options(\%options),
1901                                     );
1902
1903     $transaction->content(
1904       'type'           => 'CC',
1905       $self->_bop_auth(\%options),          
1906       'action'         => 'Authorization Only',
1907       'description'    => $options{'description'},
1908       'amount'         => '1.00',
1909       #'invoice_number' => $options{'invnum'},
1910       'customer_id'    => $self->custnum,
1911       %$bop_content,
1912       'reference'      => $cust_pay_pending->paypendingnum, #for now
1913       'callback_url'   => $payment_gateway->gateway_callback_url,
1914       'cancel_url'     => $payment_gateway->gateway_cancel_url,
1915       'email'          => $email,
1916       %content, #after
1917     );
1918
1919     $cust_pay_pending->status('pending');
1920     my $cpp_pending_err = $cust_pay_pending->replace;
1921     return $cpp_pending_err if $cpp_pending_err;
1922
1923     warn Dumper($transaction) if $DEBUG > 2;
1924
1925     unless ( $BOP_TESTING ) {
1926       $transaction->test_transaction(1)
1927         if $conf->exists('business-onlinepayment-test_transaction');
1928       $transaction->submit();
1929     } else {
1930       if ( $BOP_TESTING_SUCCESS ) {
1931         $transaction->is_success(1);
1932         $transaction->authorization('fake auth');
1933       } else {
1934         $transaction->is_success(0);
1935         $transaction->error_message('fake failure');
1936       }
1937     }
1938
1939     if ( $transaction->is_success() ) {
1940
1941       $cust_pay_pending->status('authorized');
1942       my $cpp_authorized_err = $cust_pay_pending->replace;
1943       return $cpp_authorized_err if $cpp_authorized_err;
1944
1945       my $auth = $transaction->authorization;
1946       my $ordernum = $transaction->can('order_number')
1947                      ? $transaction->order_number
1948                      : '';
1949
1950       my $reverse = new $namespace( $payment_gateway->gateway_module,
1951                                     $self->_bop_options(\%options),
1952                                   );
1953
1954       $reverse->content( 'action'        => 'Reverse Authorization',
1955                          $self->_bop_auth(\%options),          
1956
1957                          # B:OP
1958                          'amount'        => '1.00',
1959                          'authorization' => $transaction->authorization,
1960                          'order_number'  => $ordernum,
1961
1962                          # vsecure
1963                          'result_code'   => $transaction->result_code,
1964                          'txn_date'      => $transaction->txn_date,
1965
1966                          %content,
1967                        );
1968       $reverse->test_transaction(1)
1969         if $conf->exists('business-onlinepayment-test_transaction');
1970       $reverse->submit();
1971
1972       if ( $reverse->is_success ) {
1973
1974         $cust_pay_pending->status('done');
1975         $cust_pay_pending->statustext('reversed');
1976         my $cpp_reversed_err = $cust_pay_pending->replace;
1977         return $cpp_reversed_err if $cpp_reversed_err;
1978
1979       } else {
1980
1981         my $e = "Authorization successful but reversal failed, custnum #".
1982                 $self->custnum. ': '.  $reverse->result_code.
1983                 ": ". $reverse->error_message;
1984         $log->warning($e);
1985         warn $e;
1986         return $e;
1987
1988       }
1989
1990       ### Address Verification ###
1991       #
1992       # Single-letter codes vary by cardtype.
1993       #
1994       # Erring on the side of accepting cards if avs is not available,
1995       # only rejecting if avs occurred and there's been an explicit mismatch
1996       #
1997       # Charts below taken from vSecure documentation,
1998       #    shows codes for Amex/Dscv/MC/Visa
1999       #
2000       # ACCEPTABLE AVS RESPONSES:
2001       # Both Address and 5-digit postal code match Y A Y Y
2002       # Both address and 9-digit postal code match Y A X Y
2003       # United Kingdom – Address and postal code match _ _ _ F
2004       # International transaction – Address and postal code match _ _ _ D/M
2005       #
2006       # ACCEPTABLE, BUT ISSUE A WARNING:
2007       # Ineligible transaction; or message contains a content error _ _ _ E
2008       # System unavailable; retry R U R R
2009       # Information unavailable U W U U
2010       # Issuer does not support AVS S U S S
2011       # AVS is not applicable _ _ _ S
2012       # Incompatible formats – Not verified _ _ _ C
2013       # Incompatible formats – Address not verified; postal code matches _ _ _ P
2014       # International transaction – address not verified _ G _ G/I
2015       #
2016       # UNACCEPTABLE AVS RESPONSES:
2017       # Only Address matches A Y A A
2018       # Only 5-digit postal code matches Z Z Z Z
2019       # Only 9-digit postal code matches Z Z W W
2020       # Neither address nor postal code matches N N N N
2021
2022       if (my $avscode = uc($transaction->avs_code)) {
2023
2024         # map codes to accept/warn/reject
2025         my $avs = {
2026           'American Express card' => {
2027             'A' => 'r',
2028             'N' => 'r',
2029             'R' => 'w',
2030             'S' => 'w',
2031             'U' => 'w',
2032             'Y' => 'a',
2033             'Z' => 'r',
2034           },
2035           'Discover card' => {
2036             'A' => 'a',
2037             'G' => 'w',
2038             'N' => 'r',
2039             'U' => 'w',
2040             'W' => 'w',
2041             'Y' => 'r',
2042             'Z' => 'r',
2043           },
2044           'MasterCard' => {
2045             'A' => 'r',
2046             'N' => 'r',
2047             'R' => 'w',
2048             'S' => 'w',
2049             'U' => 'w',
2050             'W' => 'r',
2051             'X' => 'a',
2052             'Y' => 'a',
2053             'Z' => 'r',
2054           },
2055           'VISA card' => {
2056             'A' => 'r',
2057             'C' => 'w',
2058             'D' => 'a',
2059             'E' => 'w',
2060             'F' => 'a',
2061             'G' => 'w',
2062             'I' => 'w',
2063             'M' => 'a',
2064             'N' => 'r',
2065             'P' => 'w',
2066             'R' => 'w',
2067             'S' => 'w',
2068             'U' => 'w',
2069             'W' => 'r',
2070             'Y' => 'a',
2071             'Z' => 'r',
2072           },
2073         };
2074         my $cardtype = cardtype($content{card_number});
2075         if ($avs->{$cardtype}) {
2076           my $avsact = $avs->{$cardtype}->{$avscode};
2077           my $warning = '';
2078           if ($avsact eq 'r') {
2079             return "AVS code verification failed, cardtype $cardtype, code $avscode";
2080           } elsif ($avsact eq 'w') {
2081             $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2082           } elsif (!$avsact) {
2083             $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2084           } # else $avsact eq 'a'
2085           if ($warning) {
2086             $log->warning($warning);
2087             warn $warning;
2088           }
2089         } # else $cardtype avs handling not implemented
2090       } # else !$transaction->avs_code
2091
2092     } else { # is not success
2093
2094       # status is 'done' not 'declined', as in _realtime_bop_result
2095       $cust_pay_pending->status('done');
2096       $error = $transaction->error_message || 'Unknown error';
2097       $cust_pay_pending->statustext($error);
2098       # could also record failure_status here,
2099       #   but it's not supported by B::OP::vSecureProcessing...
2100       #   need a B::OP module with (reverse) auth only to test it with
2101       my $cpp_declined_err = $cust_pay_pending->replace;
2102       return $cpp_declined_err if $cpp_declined_err;
2103
2104     }
2105
2106   } # end of IMMEDIATE; we now have our $error and $transaction
2107
2108   ###
2109   # Save the custnum (as part of the main transaction, so it can reference
2110   # the cust_main)
2111   ###
2112
2113   if (!$cust_pay_pending->custnum) {
2114     $cust_pay_pending->set('custnum', $self->custnum);
2115     my $set_custnum_err = $cust_pay_pending->replace;
2116     if ($set_custnum_err) {
2117       $log->error($set_custnum_err);
2118       $error ||= $set_custnum_err;
2119       # but if there was a real verification error also, return that one
2120     }
2121   }
2122
2123   ###
2124   # Tokenize
2125   ###
2126
2127   if ( $transaction->can('card_token') && $transaction->card_token ) {
2128
2129     if ( $options{'payinfo'} eq $self->payinfo ) {
2130       $self->payinfo($transaction->card_token);
2131       my $error = $self->replace;
2132       if ( $error ) {
2133         my $warning = "WARNING: error storing token: $error, but proceeding anyway\n";
2134         $log->warning($warning);
2135         warn $warning;
2136       }
2137     }
2138
2139   }
2140
2141   ###
2142   # result handling
2143   ###
2144
2145   # $error contains the transaction error_message, if is_success was false.
2146  
2147   return $error;
2148
2149 }
2150
2151 =back
2152
2153 =head1 BUGS
2154
2155 Not autoloaded.
2156
2157 =head1 SEE ALSO
2158
2159 L<FS::cust_main>, L<FS::cust_main::Billing>
2160
2161 =cut
2162
2163 1;