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