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