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