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