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