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