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