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