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