Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / cust_main / Billing_Realtime.pm
1 package FS::cust_main::Billing_Realtime;
2
3 use strict;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
6 use 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 $cust_pay = new FS::cust_pay ( {
761      'custnum'  => $self->custnum,
762      'invnum'   => $options{'invnum'},
763      'paid'     => $options{amount},
764      '_date'    => '',
765      'payby'    => $bop_method2payby{$options{method}},
766      #'payinfo'  => $payinfo,
767      'payinfo'  => '4111111111111111',
768      #'paydate'  => $paydate,
769      'paydate'  => '2012-05-01',
770      'processor'      => 'FakeProcessor',
771      'auth'           => '54',
772      'order_number'   => '32',
773   } );
774   $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
775
776   if ( $DEBUG ) {
777       warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
778       warn "  $_ => $options{$_}\n" foreach keys %options;
779   }
780
781   my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
782
783   if ( $error ) {
784     $cust_pay->invnum(''); #try again with no specific invnum
785     my $error2 = $cust_pay->insert( $options{'manual'} ?
786                                     ( 'manual' => 1 ) : ()
787                                   );
788     if ( $error2 ) {
789       # gah, even with transactions.
790       my $e = 'WARNING: Card/ACH debited but database not updated - '.
791               "error inserting (fake!) payment: $error2".
792               " (previously tried insert with invnum #$options{'invnum'}" .
793               ": $error )";
794       warn $e;
795       return $e;
796     }
797   }
798
799   if ( $options{'paynum_ref'} ) {
800     ${ $options{'paynum_ref'} } = $cust_pay->paynum;
801   }
802
803   return ''; #no error
804
805 }
806
807
808 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
809
810 # Wraps up processing of a realtime credit card, ACH (electronic check) or
811 # phone bill transaction.
812
813 sub _realtime_bop_result {
814   my( $self, $cust_pay_pending, $transaction, %options ) = @_;
815
816   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
817
818   if ( $DEBUG ) {
819     warn "$me _realtime_bop_result: pending transaction ".
820       $cust_pay_pending->paypendingnum. "\n";
821     warn "  $_ => $options{$_}\n" foreach keys %options;
822   }
823
824   my $payment_gateway = $options{payment_gateway}
825     or return "no payment gateway in arguments to _realtime_bop_result";
826
827   $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
828   my $cpp_captured_err = $cust_pay_pending->replace;
829   return $cpp_captured_err if $cpp_captured_err;
830
831   if ( $transaction->is_success() ) {
832
833     my $order_number = $transaction->order_number
834       if $transaction->can('order_number');
835
836     my $cust_pay = new FS::cust_pay ( {
837        'custnum'  => $self->custnum,
838        'invnum'   => $options{'invnum'},
839        'paid'     => $cust_pay_pending->paid,
840        '_date'    => '',
841        'payby'    => $cust_pay_pending->payby,
842        'payinfo'  => $options{'payinfo'},
843        'paydate'  => $cust_pay_pending->paydate,
844        'pkgnum'   => $cust_pay_pending->pkgnum,
845        'discount_term'  => $options{'discount_term'},
846        'gatewaynum'     => ($payment_gateway->gatewaynum || ''),
847        'processor'      => $payment_gateway->gateway_module,
848        'auth'           => $transaction->authorization,
849        'order_number'   => $order_number || '',
850
851     } );
852     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
853     $cust_pay->payunique( $options{payunique} )
854       if defined($options{payunique}) && length($options{payunique});
855
856     my $oldAutoCommit = $FS::UID::AutoCommit;
857     local $FS::UID::AutoCommit = 0;
858     my $dbh = dbh;
859
860     #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
861
862     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
863
864     if ( $error ) {
865       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
866       $cust_pay->invnum(''); #try again with no specific invnum
867       $cust_pay->paynum('');
868       my $error2 = $cust_pay->insert( $options{'manual'} ?
869                                       ( 'manual' => 1 ) : ()
870                                     );
871       if ( $error2 ) {
872         # gah.  but at least we have a record of the state we had to abort in
873         # from cust_pay_pending now.
874         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
875         my $e = "WARNING: $options{method} captured but payment not recorded -".
876                 " error inserting payment (". $payment_gateway->gateway_module.
877                 "): $error2".
878                 " (previously tried insert with invnum #$options{'invnum'}" .
879                 ": $error ) - pending payment saved as paypendingnum ".
880                 $cust_pay_pending->paypendingnum. "\n";
881         warn $e;
882         return $e;
883       }
884     }
885
886     my $jobnum = $cust_pay_pending->jobnum;
887     if ( $jobnum ) {
888        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
889       
890        unless ( $placeholder ) {
891          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
892          my $e = "WARNING: $options{method} captured but job $jobnum not ".
893              "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
894          warn $e;
895          return $e;
896        }
897
898        $error = $placeholder->delete;
899
900        if ( $error ) {
901          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
902          my $e = "WARNING: $options{method} captured but could not delete ".
903               "job $jobnum for paypendingnum ".
904               $cust_pay_pending->paypendingnum. ": $error\n";
905          warn $e;
906          return $e;
907        }
908
909     }
910     
911     if ( $options{'paynum_ref'} ) {
912       ${ $options{'paynum_ref'} } = $cust_pay->paynum;
913     }
914
915     $cust_pay_pending->status('done');
916     $cust_pay_pending->statustext('captured');
917     $cust_pay_pending->paynum($cust_pay->paynum);
918     my $cpp_done_err = $cust_pay_pending->replace;
919
920     if ( $cpp_done_err ) {
921
922       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
923       my $e = "WARNING: $options{method} captured but payment not recorded - ".
924               "error updating status for paypendingnum ".
925               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
926       warn $e;
927       return $e;
928
929     } else {
930
931       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
932
933       if ( $options{'apply'} ) {
934         my $apply_error = $self->apply_payments_and_credits;
935         if ( $apply_error ) {
936           warn "WARNING: error applying payment: $apply_error\n";
937           #but we still should return no error cause the payment otherwise went
938           #through...
939         }
940       }
941
942       # have a CC surcharge portion --> one-time charge
943       if ( $options{'cc_surcharge'} > 0 ) { 
944             # XXX: this whole block needs to be in a transaction?
945
946           my $invnum;
947           $invnum = $options{'invnum'} if $options{'invnum'};
948           unless ( $invnum ) { # probably from a payment screen
949              # do we have any open invoices? pick earliest
950              # uses the fact that cust_main->cust_bill sorts by date ascending
951              my @open = $self->open_cust_bill;
952              $invnum = $open[0]->invnum if scalar(@open);
953           }
954             
955           unless ( $invnum ) {  # still nothing? pick last closed invoice
956              # again uses fact that cust_main->cust_bill sorts by date ascending
957              my @closed = $self->cust_bill;
958              $invnum = $closed[$#closed]->invnum if scalar(@closed);
959           }
960
961           unless ( $invnum ) {
962             # XXX: unlikely case - pre-paying before any invoices generated
963             # what it should do is create a new invoice and pick it
964                 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
965                 return '';
966           }
967
968           my $cust_pkg;
969           my $charge_error = $self->charge({
970                                     'amount'    => $options{'cc_surcharge'},
971                                     'pkg'       => 'Credit Card Surcharge',
972                                     'setuptax'  => 'Y',
973                                     'cust_pkg_ref' => \$cust_pkg,
974                                 });
975           if($charge_error) {
976                 warn 'Unable to add CC surcharge cust_pkg';
977                 return '';
978           }
979
980           $cust_pkg->setup(time);
981           my $cp_error = $cust_pkg->replace;
982           if($cp_error) {
983               warn 'Unable to set setup time on cust_pkg for cc surcharge';
984             # but keep going...
985           }
986                                     
987           my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
988           unless ( $cust_bill ) {
989               warn "race condition + invoice deletion just happened";
990               return '';
991           }
992
993           my $grand_error = 
994             $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
995
996           warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
997             if $grand_error;
998       }
999
1000       return ''; #no error
1001
1002     }
1003
1004   } else {
1005
1006     my $perror = $payment_gateway->gateway_module. " error: ".
1007       $transaction->error_message;
1008
1009     my $jobnum = $cust_pay_pending->jobnum;
1010     if ( $jobnum ) {
1011        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1012       
1013        if ( $placeholder ) {
1014          my $error = $placeholder->depended_delete;
1015          $error ||= $placeholder->delete;
1016          warn "error removing provisioning jobs after declined paypendingnum ".
1017            $cust_pay_pending->paypendingnum. ": $error\n";
1018        } else {
1019          my $e = "error finding job $jobnum for declined paypendingnum ".
1020               $cust_pay_pending->paypendingnum. "\n";
1021          warn $e;
1022        }
1023
1024     }
1025     
1026     unless ( $transaction->error_message ) {
1027
1028       my $t_response;
1029       if ( $transaction->can('response_page') ) {
1030         $t_response = {
1031                         'page'    => ( $transaction->can('response_page')
1032                                          ? $transaction->response_page
1033                                          : ''
1034                                      ),
1035                         'code'    => ( $transaction->can('response_code')
1036                                          ? $transaction->response_code
1037                                          : ''
1038                                      ),
1039                         'headers' => ( $transaction->can('response_headers')
1040                                          ? $transaction->response_headers
1041                                          : ''
1042                                      ),
1043                       };
1044       } else {
1045         $t_response .=
1046           "No additional debugging information available for ".
1047             $payment_gateway->gateway_module;
1048       }
1049
1050       $perror .= "No error_message returned from ".
1051                    $payment_gateway->gateway_module. " -- ".
1052                  ( ref($t_response) ? Dumper($t_response) : $t_response );
1053
1054     }
1055
1056     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1057          && $conf->exists('emaildecline', $self->agentnum)
1058          && grep { $_ ne 'POST' } $self->invoicing_list
1059          && ! grep { $transaction->error_message =~ /$_/ }
1060                    $conf->config('emaildecline-exclude', $self->agentnum)
1061     ) {
1062
1063       # Send a decline alert to the customer.
1064       my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1065       my $error = '';
1066       if ( $msgnum ) {
1067         # include the raw error message in the transaction state
1068         $cust_pay_pending->setfield('error', $transaction->error_message);
1069         my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1070         $error = $msg_template->send( 'cust_main' => $self,
1071                                       'object'    => $cust_pay_pending );
1072       }
1073       else { #!$msgnum
1074
1075         my @templ = $conf->config('declinetemplate');
1076         my $template = new Text::Template (
1077           TYPE   => 'ARRAY',
1078           SOURCE => [ map "$_\n", @templ ],
1079         ) or return "($perror) can't create template: $Text::Template::ERROR";
1080         $template->compile()
1081           or return "($perror) can't compile template: $Text::Template::ERROR";
1082
1083         my $templ_hash = {
1084           'company_name'    =>
1085             scalar( $conf->config('company_name', $self->agentnum ) ),
1086           'company_address' =>
1087             join("\n", $conf->config('company_address', $self->agentnum ) ),
1088           'error'           => $transaction->error_message,
1089         };
1090
1091         my $error = send_email(
1092           'from'    => $conf->config('invoice_from', $self->agentnum ),
1093           'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1094           'subject' => 'Your payment could not be processed',
1095           'body'    => [ $template->fill_in(HASH => $templ_hash) ],
1096         );
1097       }
1098
1099       $perror .= " (also received error sending decline notification: $error)"
1100         if $error;
1101
1102     }
1103
1104     $cust_pay_pending->status('done');
1105     $cust_pay_pending->statustext("declined: $perror");
1106     my $cpp_done_err = $cust_pay_pending->replace;
1107     if ( $cpp_done_err ) {
1108       my $e = "WARNING: $options{method} declined but pending payment not ".
1109               "resolved - error updating status for paypendingnum ".
1110               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1111       warn $e;
1112       $perror = "$e ($perror)";
1113     }
1114
1115     return $perror;
1116   }
1117
1118 }
1119
1120 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1121
1122 Verifies successful third party processing of a realtime credit card,
1123 ACH (electronic check) or phone bill transaction via a
1124 Business::OnlineThirdPartyPayment realtime gateway.  See
1125 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1126
1127 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1128
1129 The additional options I<payname>, I<city>, I<state>,
1130 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1131 if set, will override the value from the customer record.
1132
1133 I<description> is a free-text field passed to the gateway.  It defaults to
1134 "Internet services".
1135
1136 If an I<invnum> is specified, this payment (if successful) is applied to the
1137 specified invoice.  If you don't specify an I<invnum> you might want to
1138 call the B<apply_payments> method.
1139
1140 I<quiet> can be set true to surpress email decline notices.
1141
1142 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
1143 resulting paynum, if any.
1144
1145 I<payunique> is a unique identifier for this payment.
1146
1147 Returns a hashref containing elements bill_error (which will be undefined
1148 upon success) and session_id of any associated session.
1149
1150 =cut
1151
1152 sub realtime_botpp_capture {
1153   my( $self, $cust_pay_pending, %options ) = @_;
1154
1155   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1156
1157   if ( $DEBUG ) {
1158     warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1159     warn "  $_ => $options{$_}\n" foreach keys %options;
1160   }
1161
1162   eval "use Business::OnlineThirdPartyPayment";  
1163   die $@ if $@;
1164
1165   ###
1166   # select the gateway
1167   ###
1168
1169   my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1170
1171   my $payment_gateway;
1172   my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1173   $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1174                 { gatewaynum => $gatewaynum }
1175               )
1176     : $self->agent->payment_gateway( 'method' => $method,
1177                                      # 'invnum'  => $cust_pay_pending->invnum,
1178                                      # 'payinfo' => $cust_pay_pending->payinfo,
1179                                    );
1180
1181   $options{payment_gateway} = $payment_gateway; # for the helper subs
1182
1183   ###
1184   # massage data
1185   ###
1186
1187   my @invoicing_list = $self->invoicing_list_emailonly;
1188   if ( $conf->exists('emailinvoiceautoalways')
1189        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1190        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1191     push @invoicing_list, $self->all_emails;
1192   }
1193
1194   my $email = ($conf->exists('business-onlinepayment-email-override'))
1195               ? $conf->config('business-onlinepayment-email-override')
1196               : $invoicing_list[0];
1197
1198   my %content = ();
1199
1200   $content{email_customer} = 
1201     (    $conf->exists('business-onlinepayment-email_customer')
1202       || $conf->exists('business-onlinepayment-email-override') );
1203       
1204   ###
1205   # run transaction(s)
1206   ###
1207
1208   my $transaction =
1209     new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1210                                            $self->_bop_options(\%options),
1211                                          );
1212
1213   $transaction->reference({ %options }); 
1214
1215   $transaction->content(
1216     'type'           => $method,
1217     $self->_bop_auth(\%options),
1218     'action'         => 'Post Authorization',
1219     'description'    => $options{'description'},
1220     'amount'         => $cust_pay_pending->paid,
1221     #'invoice_number' => $options{'invnum'},
1222     'customer_id'    => $self->custnum,
1223
1224     #3.0 is a good a time as any to get rid of this... add a config to pass it
1225     # if anyone still needs it
1226     #'referer'        => 'http://cleanwhisker.420.am/',
1227
1228     'reference'      => $cust_pay_pending->paypendingnum,
1229     'email'          => $email,
1230     'phone'          => $self->daytime || $self->night,
1231     %content, #after
1232     # plus whatever is required for bogus capture avoidance
1233   );
1234
1235   $transaction->submit();
1236
1237   my $error =
1238     $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1239
1240   if ( $options{'apply'} ) {
1241     my $apply_error = $self->apply_payments_and_credits;
1242     if ( $apply_error ) {
1243       warn "WARNING: error applying payment: $apply_error\n";
1244     }
1245   }
1246
1247   return {
1248     bill_error => $error,
1249     session_id => $cust_pay_pending->session_id,
1250   }
1251
1252 }
1253
1254 =item default_payment_gateway
1255
1256 DEPRECATED -- use agent->payment_gateway
1257
1258 =cut
1259
1260 sub default_payment_gateway {
1261   my( $self, $method ) = @_;
1262
1263   die "Real-time processing not enabled\n"
1264     unless $conf->exists('business-onlinepayment');
1265
1266   #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1267
1268   #load up config
1269   my $bop_config = 'business-onlinepayment';
1270   $bop_config .= '-ach'
1271     if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1272   my ( $processor, $login, $password, $action, @bop_options ) =
1273     $conf->config($bop_config);
1274   $action ||= 'normal authorization';
1275   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1276   die "No real-time processor is enabled - ".
1277       "did you set the business-onlinepayment configuration value?\n"
1278     unless $processor;
1279
1280   ( $processor, $login, $password, $action, @bop_options )
1281 }
1282
1283 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1284
1285 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1286 via a Business::OnlinePayment realtime gateway.  See
1287 L<http://420.am/business-onlinepayment> for supported gateways.
1288
1289 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1290
1291 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1292
1293 Most gateways require a reference to an original payment transaction to refund,
1294 so you probably need to specify a I<paynum>.
1295
1296 I<amount> defaults to the original amount of the payment if not specified.
1297
1298 I<reason> specifies a reason for the refund.
1299
1300 I<paydate> specifies the expiration date for a credit card overriding the
1301 value from the customer record or the payment record. Specified as yyyy-mm-dd
1302
1303 Implementation note: If I<amount> is unspecified or equal to the amount of the
1304 orignal payment, first an attempt is made to "void" the transaction via
1305 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1306 the normal attempt is made to "refund" ("credit") the transaction via the
1307 gateway is attempted. No attempt to "void" the transaction is made if the 
1308 gateway has introspection data and doesn't support void.
1309
1310 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1311 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1312 #if set, will override the value from the customer record.
1313
1314 #If an I<invnum> is specified, this payment (if successful) is applied to the
1315 #specified invoice.  If you don't specify an I<invnum> you might want to
1316 #call the B<apply_payments> method.
1317
1318 =cut
1319
1320 #some false laziness w/realtime_bop, not enough to make it worth merging
1321 #but some useful small subs should be pulled out
1322 sub realtime_refund_bop {
1323   my $self = shift;
1324
1325   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1326
1327   my %options = ();
1328   if (ref($_[0]) eq 'HASH') {
1329     %options = %{$_[0]};
1330   } else {
1331     my $method = shift;
1332     %options = @_;
1333     $options{method} = $method;
1334   }
1335
1336   if ( $DEBUG ) {
1337     warn "$me realtime_refund_bop (new): $options{method} refund\n";
1338     warn "  $_ => $options{$_}\n" foreach keys %options;
1339   }
1340
1341   ###
1342   # look up the original payment and optionally a gateway for that payment
1343   ###
1344
1345   my $cust_pay = '';
1346   my $amount = $options{'amount'};
1347
1348   my( $processor, $login, $password, @bop_options, $namespace ) ;
1349   my( $auth, $order_number ) = ( '', '', '' );
1350   my $gatewaynum = '';
1351
1352   if ( $options{'paynum'} ) {
1353
1354     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
1355     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1356       or return "Unknown paynum $options{'paynum'}";
1357     $amount ||= $cust_pay->paid;
1358
1359     if ( $cust_pay->get('processor') ) {
1360       ($gatewaynum, $processor, $auth, $order_number) =
1361       (
1362         $cust_pay->gatewaynum,
1363         $cust_pay->processor,
1364         $cust_pay->auth,
1365         $cust_pay->order_number,
1366       );
1367     } else {
1368       # this payment wasn't upgraded, which probably means this won't work,
1369       # but try it anyway
1370       $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1371         or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1372                   $cust_pay->paybatch;
1373       ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1374     }
1375
1376     if ( $gatewaynum ) { #gateway for the payment to be refunded
1377
1378       my $payment_gateway =
1379         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1380       die "payment gateway $gatewaynum not found"
1381         unless $payment_gateway;
1382
1383       $processor   = $payment_gateway->gateway_module;
1384       $login       = $payment_gateway->gateway_username;
1385       $password    = $payment_gateway->gateway_password;
1386       $namespace   = $payment_gateway->gateway_namespace;
1387       @bop_options = $payment_gateway->options;
1388
1389     } else { #try the default gateway
1390
1391       my $conf_processor;
1392       my $payment_gateway =
1393         $self->agent->payment_gateway('method' => $options{method});
1394
1395       ( $conf_processor, $login, $password, $namespace ) =
1396         map { my $method = "gateway_$_"; $payment_gateway->$method }
1397           qw( module username password namespace );
1398
1399       @bop_options = $payment_gateway->gatewaynum
1400                        ? $payment_gateway->options
1401                        : @{ $payment_gateway->get('options') };
1402
1403       return "processor of payment $options{'paynum'} $processor does not".
1404              " match default processor $conf_processor"
1405         unless $processor eq $conf_processor;
1406
1407     }
1408
1409
1410   } else { # didn't specify a paynum, so look for agent gateway overrides
1411            # like a normal transaction 
1412  
1413     my $payment_gateway =
1414       $self->agent->payment_gateway( 'method'  => $options{method},
1415                                      #'payinfo' => $payinfo,
1416                                    );
1417     my( $processor, $login, $password, $namespace ) =
1418       map { my $method = "gateway_$_"; $payment_gateway->$method }
1419         qw( module username password namespace );
1420
1421     my @bop_options = $payment_gateway->gatewaynum
1422                         ? $payment_gateway->options
1423                         : @{ $payment_gateway->get('options') };
1424
1425   }
1426   return "neither amount nor paynum specified" unless $amount;
1427
1428   eval "use $namespace";  
1429   die $@ if $@;
1430
1431   my %content = (
1432     'type'           => $options{method},
1433     'login'          => $login,
1434     'password'       => $password,
1435     'order_number'   => $order_number,
1436     'amount'         => $amount,
1437
1438     #3.0 is a good a time as any to get rid of this... add a config to pass it
1439     # if anyone still needs it
1440     #'referer'        => 'http://cleanwhisker.420.am/',
1441   );
1442   $content{authorization} = $auth
1443     if length($auth); #echeck/ACH transactions have an order # but no auth
1444                       #(at least with authorize.net)
1445
1446   my $currency =    $conf->exists('business-onlinepayment-currency')
1447                  && $conf->config('business-onlinepayment-currency');
1448   $content{currency} = $currency if $currency;
1449
1450   my $disable_void_after;
1451   if ($conf->exists('disable_void_after')
1452       && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1453     $disable_void_after = $1;
1454   }
1455
1456   #first try void if applicable
1457   my $void = new Business::OnlinePayment( $processor, @bop_options );
1458
1459   my $tryvoid = 1;
1460   if ($void->can('info')) {
1461       my $paytype = '';
1462       $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1463       $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1464       my %supported_actions = $void->info('supported_actions');
1465       $tryvoid = 0 
1466         if ( %supported_actions && $paytype 
1467                 && defined($supported_actions{$paytype}) 
1468                 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1469   }
1470
1471   if ( $cust_pay && $cust_pay->paid == $amount
1472     && (
1473       ( not defined($disable_void_after) )
1474       || ( time < ($cust_pay->_date + $disable_void_after ) )
1475     )
1476     && $tryvoid
1477   ) {
1478     warn "  attempting void\n" if $DEBUG > 1;
1479     if ( $void->can('info') ) {
1480       if ( $cust_pay->payby eq 'CARD'
1481            && $void->info('CC_void_requires_card') )
1482       {
1483         $content{'card_number'} = $cust_pay->payinfo;
1484       } elsif ( $cust_pay->payby eq 'CHEK'
1485                 && $void->info('ECHECK_void_requires_account') )
1486       {
1487         ( $content{'account_number'}, $content{'routing_code'} ) =
1488           split('@', $cust_pay->payinfo);
1489         $content{'name'} = $self->get('first'). ' '. $self->get('last');
1490       }
1491     }
1492     $void->content( 'action' => 'void', %content );
1493     $void->test_transaction(1)
1494       if $conf->exists('business-onlinepayment-test_transaction');
1495     $void->submit();
1496     if ( $void->is_success ) {
1497       my $error = $cust_pay->void($options{'reason'});
1498       if ( $error ) {
1499         # gah, even with transactions.
1500         my $e = 'WARNING: Card/ACH voided but database not updated - '.
1501                 "error voiding payment: $error";
1502         warn $e;
1503         return $e;
1504       }
1505       warn "  void successful\n" if $DEBUG > 1;
1506       return '';
1507     }
1508   }
1509
1510   warn "  void unsuccessful, trying refund\n"
1511     if $DEBUG > 1;
1512
1513   #massage data
1514   my $address = $self->address1;
1515   $address .= ", ". $self->address2 if $self->address2;
1516
1517   my($payname, $payfirst, $paylast);
1518   if ( $self->payname && $options{method} ne 'ECHECK' ) {
1519     $payname = $self->payname;
1520     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1521       or return "Illegal payname $payname";
1522     ($payfirst, $paylast) = ($1, $2);
1523   } else {
1524     $payfirst = $self->getfield('first');
1525     $paylast = $self->getfield('last');
1526     $payname =  "$payfirst $paylast";
1527   }
1528
1529   my @invoicing_list = $self->invoicing_list_emailonly;
1530   if ( $conf->exists('emailinvoiceautoalways')
1531        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1532        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1533     push @invoicing_list, $self->all_emails;
1534   }
1535
1536   my $email = ($conf->exists('business-onlinepayment-email-override'))
1537               ? $conf->config('business-onlinepayment-email-override')
1538               : $invoicing_list[0];
1539
1540   my $payip = exists($options{'payip'})
1541                 ? $options{'payip'}
1542                 : $self->payip;
1543   $content{customer_ip} = $payip
1544     if length($payip);
1545
1546   my $payinfo = '';
1547   if ( $options{method} eq 'CC' ) {
1548
1549     if ( $cust_pay ) {
1550       $content{card_number} = $payinfo = $cust_pay->payinfo;
1551       (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1552         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1553         ($content{expiration} = "$2/$1");  # where available
1554     } else {
1555       $content{card_number} = $payinfo = $self->payinfo;
1556       (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1557         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1558       $content{expiration} = "$2/$1";
1559     }
1560
1561   } elsif ( $options{method} eq 'ECHECK' ) {
1562
1563     if ( $cust_pay ) {
1564       $payinfo = $cust_pay->payinfo;
1565     } else {
1566       $payinfo = $self->payinfo;
1567     } 
1568     ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1569     $content{bank_name} = $self->payname;
1570     $content{account_type} = 'CHECKING';
1571     $content{account_name} = $payname;
1572     $content{customer_org} = $self->company ? 'B' : 'I';
1573     $content{customer_ssn} = $self->ss;
1574   } elsif ( $options{method} eq 'LEC' ) {
1575     $content{phone} = $payinfo = $self->payinfo;
1576   }
1577
1578   #then try refund
1579   my $refund = new Business::OnlinePayment( $processor, @bop_options );
1580   my %sub_content = $refund->content(
1581     'action'         => 'credit',
1582     'customer_id'    => $self->custnum,
1583     'last_name'      => $paylast,
1584     'first_name'     => $payfirst,
1585     'name'           => $payname,
1586     'address'        => $address,
1587     'city'           => $self->city,
1588     'state'          => $self->state,
1589     'zip'            => $self->zip,
1590     'country'        => $self->country,
1591     'email'          => $email,
1592     'phone'          => $self->daytime || $self->night,
1593     %content, #after
1594   );
1595   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
1596     if $DEBUG > 1;
1597   $refund->test_transaction(1)
1598     if $conf->exists('business-onlinepayment-test_transaction');
1599   $refund->submit();
1600
1601   return "$processor error: ". $refund->error_message
1602     unless $refund->is_success();
1603
1604   $order_number = $refund->order_number if $refund->can('order_number');
1605
1606   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1607     my @cust_bill_pay = $cust_pay->cust_bill_pay;
1608     last unless @cust_bill_pay;
1609     my $cust_bill_pay = pop @cust_bill_pay;
1610     my $error = $cust_bill_pay->delete;
1611     last if $error;
1612   }
1613
1614   my $cust_refund = new FS::cust_refund ( {
1615     'custnum'  => $self->custnum,
1616     'paynum'   => $options{'paynum'},
1617     'refund'   => $amount,
1618     '_date'    => '',
1619     'payby'    => $bop_method2payby{$options{method}},
1620     'payinfo'  => $payinfo,
1621     'reason'   => $options{'reason'} || 'card or ACH refund',
1622     'gatewaynum'    => $gatewaynum, # may be null
1623     'processor'     => $processor,
1624     'auth'          => $refund->authorization,
1625     'order_number'  => $order_number,
1626   } );
1627   my $error = $cust_refund->insert;
1628   if ( $error ) {
1629     $cust_refund->paynum(''); #try again with no specific paynum
1630     my $error2 = $cust_refund->insert;
1631     if ( $error2 ) {
1632       # gah, even with transactions.
1633       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1634               "error inserting refund ($processor): $error2".
1635               " (previously tried insert with paynum #$options{'paynum'}" .
1636               ": $error )";
1637       warn $e;
1638       return $e;
1639     }
1640   }
1641
1642   ''; #no error
1643
1644 }
1645
1646 =back
1647
1648 =head1 BUGS
1649
1650 Not autoloaded.
1651
1652 =head1 SEE ALSO
1653
1654 L<FS::cust_main>, L<FS::cust_main::Billing>
1655
1656 =cut
1657
1658 1;