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