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