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