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