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