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