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