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