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