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