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