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