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