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