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