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