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