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