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