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