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