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