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