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