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