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