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