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