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