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