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