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