1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.28;
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
17 $realtime_bop_decline_quiet = 0;
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
23 $me = '[FS::cust_main::Billing_Realtime]';
26 our $BOP_TESTING_SUCCESS = 1;
28 install_callback FS::UID sub {
30 #yes, need it for stuff below (prolly should be cached)
35 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
41 These methods are available on FS::cust_main objects.
47 =item realtime_collect [ OPTION => VALUE ... ]
49 Attempt to collect the customer's current balance with a realtime credit
50 card, electronic check, or phone bill transaction (see realtime_bop() below).
52 Returns the result of realtime_bop(): nothing, an error message, or a
53 hashref of state information for a third-party transaction.
55 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
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.
60 If no I<amount> is specified, then the customer balance is used.
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.
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.
70 If an I<invnum> is specified, this payment (if successful) is applied to the
73 I<apply> will automatically apply a resulting payment.
75 I<quiet> can be set true to suppress email decline notices.
77 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
78 resulting paynum, if any.
80 I<payunique> is a unique identifier for this payment.
82 I<session_id> is a session identifier associated with this payment.
84 I<depend_jobnum> allows payment capture to unlock export jobs
88 sub realtime_collect {
89 my( $self, %options ) = @_;
91 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
94 warn "$me realtime_collect:\n";
95 warn " $_ => $options{$_}\n" foreach keys %options;
98 $options{amount} = $self->balance unless exists( $options{amount} );
99 $options{method} = FS::payby->payby2bop($self->payby)
100 unless exists( $options{method} );
102 return $self->realtime_bop({%options});
106 =item realtime_bop { [ ARG => VALUE ... ] }
108 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
109 via a Business::OnlinePayment realtime gateway. See
110 L<http://420.am/business-onlinepayment> for supported gateways.
112 Required arguments in the hashref are I<method>, and I<amount>
114 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
116 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
118 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
119 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
120 if set, will override the value from the customer record.
122 I<description> is a free-text field passed to the gateway. It defaults to
123 the value defined by the business-onlinepayment-description configuration
124 option, or "Internet services" if that is unset.
126 If an I<invnum> is specified, this payment (if successful) is applied to the
127 specified invoice. If the customer has exactly one open invoice, that
128 invoice number will be assumed. If you don't specify an I<invnum> you might
129 want to call the B<apply_payments> method or set the I<apply> option.
131 I<apply> can be set to true to apply a resulting payment.
133 I<quiet> can be set true to surpress email decline notices.
135 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
136 resulting paynum, if any.
138 I<payunique> is a unique identifier for this payment.
140 I<session_id> is a session identifier associated with this payment.
142 I<depend_jobnum> allows payment capture to unlock export jobs
144 I<discount_term> attempts to take a discount by prepaying for discount_term.
145 The payment will fail if I<amount> is incorrect for this discount term.
147 A direct (Business::OnlinePayment) transaction will return nothing on success,
148 or an error message on failure.
150 A third-party transaction will return a hashref containing:
152 - popup_url: the URL to which a browser should be redirected to complete
154 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
155 - reference: a reference ID for the transaction, to show the customer.
157 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
161 # some helper routines
162 sub _bop_recurring_billing {
163 my( $self, %opt ) = @_;
165 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
167 if ( defined($method) && $method eq 'transaction_is_recur' ) {
169 return 1 if $opt{'trans_is_recur'};
173 # return 1 if the payinfo has been used for another payment
174 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
182 sub _payment_gateway {
183 my ($self, $options) = @_;
185 if ( $options->{'selfservice'} ) {
186 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
188 return $options->{payment_gateway} ||=
189 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
193 if ( $options->{'fake_gatewaynum'} ) {
194 $options->{payment_gateway} =
195 qsearchs('payment_gateway',
196 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
200 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
201 unless exists($options->{payment_gateway});
203 $options->{payment_gateway};
207 my ($self, $options) = @_;
210 'login' => $options->{payment_gateway}->gateway_username,
211 'password' => $options->{payment_gateway}->gateway_password,
216 my ($self, $options) = @_;
218 $options->{payment_gateway}->gatewaynum
219 ? $options->{payment_gateway}->options
220 : @{ $options->{payment_gateway}->get('options') };
225 my ($self, $options) = @_;
227 unless ( $options->{'description'} ) {
228 if ( $conf->exists('business-onlinepayment-description') ) {
229 my $dtempl = $conf->config('business-onlinepayment-description');
231 my $agent = $self->agent->agent;
233 $options->{'description'} = eval qq("$dtempl");
235 $options->{'description'} = 'Internet services';
239 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
241 # Default invoice number if the customer has exactly one open invoice.
242 if( ! $options->{'invnum'} ) {
243 $options->{'invnum'} = '';
244 my @open = $self->open_cust_bill;
245 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
248 $options->{payname} = $self->payname unless exists( $options->{payname} );
252 my ($self, $options) = @_;
255 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
256 $content{customer_ip} = $payip if length($payip);
258 $content{invoice_number} = $options->{'invnum'}
259 if exists($options->{'invnum'}) && length($options->{'invnum'});
261 $content{email_customer} =
262 ( $conf->exists('business-onlinepayment-email_customer')
263 || $conf->exists('business-onlinepayment-email-override') );
265 my ($payname, $payfirst, $paylast);
266 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
267 ($payname = $options->{payname}) =~
268 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
269 or return "Illegal payname $payname";
270 ($payfirst, $paylast) = ($1, $2);
272 $payfirst = $self->getfield('first');
273 $paylast = $self->getfield('last');
274 $payname = "$payfirst $paylast";
277 $content{last_name} = $paylast;
278 $content{first_name} = $payfirst;
280 $content{name} = $payname;
282 $content{address} = exists($options->{'address1'})
283 ? $options->{'address1'}
285 my $address2 = exists($options->{'address2'})
286 ? $options->{'address2'}
288 $content{address} .= ", ". $address2 if length($address2);
290 $content{city} = exists($options->{city})
293 $content{state} = exists($options->{state})
296 $content{zip} = exists($options->{zip})
299 $content{country} = exists($options->{country})
300 ? $options->{country}
303 #3.0 is a good a time as any to get rid of this... add a config to pass it
304 # if anyone still needs it
305 #$content{referer} = 'http://cleanwhisker.420.am/';
307 $content{phone} = $self->daytime || $self->night;
309 my $currency = $conf->exists('business-onlinepayment-currency')
310 && $conf->config('business-onlinepayment-currency');
311 $content{currency} = $currency if $currency;
316 my %bop_method2payby = (
326 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
329 if (ref($_[0]) eq 'HASH') {
332 my ( $method, $amount ) = ( shift, shift );
334 $options{method} = $method;
335 $options{amount} = $amount;
340 # optional credit card surcharge
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');
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)?
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 ));
360 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
361 $options{'cc_surcharge'} = $cc_surcharge;
365 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
366 warn " cc_surcharge = $cc_surcharge\n";
369 warn " $_ => $options{$_}\n" foreach keys %options;
372 return $self->fake_bop(\%options) if $options{'fake'};
374 $self->_bop_defaults(\%options);
377 # set trans_is_recur based on invnum if there is one
380 my $trans_is_recur = 0;
381 if ( $options{'invnum'} ) {
383 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
384 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
390 $cust_bill->cust_bill_pkg;
393 if grep { $_->freq ne '0' } @part_pkg;
401 my $payment_gateway = $self->_payment_gateway( \%options );
402 my $namespace = $payment_gateway->gateway_namespace;
404 eval "use $namespace";
408 # check for banned credit card/ACH
411 my $ban = FS::banned_pay->ban_search(
412 'payby' => $bop_method2payby{$options{method}},
413 'payinfo' => $options{payinfo},
415 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
418 # check for term discount validity
421 my $discount_term = $options{discount_term};
422 if ( $discount_term ) {
423 my $bill = ($self->cust_bill)[-1]
424 or return "Can't apply a term discount to an unbilled customer";
425 my $plan = FS::discount_plan->new(
427 months => $discount_term
428 ) or return "No discount available for term '$discount_term'";
430 if ( $plan->discounted_total != $options{amount} ) {
431 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
439 my $bop_content = $self->_bop_content(\%options);
440 return $bop_content unless ref($bop_content);
442 my @invoicing_list = $self->invoicing_list_emailonly;
443 if ( $conf->exists('emailinvoiceautoalways')
444 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
445 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
446 push @invoicing_list, $self->all_emails;
449 my $email = ($conf->exists('business-onlinepayment-email-override'))
450 ? $conf->config('business-onlinepayment-email-override')
451 : $invoicing_list[0];
456 if ( $namespace eq 'Business::OnlinePayment' ) {
458 if ( $options{method} eq 'CC' ) {
460 $content{card_number} = $options{payinfo};
461 $paydate = exists($options{'paydate'})
462 ? $options{'paydate'}
464 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
465 $content{expiration} = "$2/$1";
467 my $paycvv = exists($options{'paycvv'})
470 $content{cvv2} = $paycvv
473 my $paystart_month = exists($options{'paystart_month'})
474 ? $options{'paystart_month'}
475 : $self->paystart_month;
477 my $paystart_year = exists($options{'paystart_year'})
478 ? $options{'paystart_year'}
479 : $self->paystart_year;
481 $content{card_start} = "$paystart_month/$paystart_year"
482 if $paystart_month && $paystart_year;
484 my $payissue = exists($options{'payissue'})
485 ? $options{'payissue'}
487 $content{issue_number} = $payissue if $payissue;
489 if ( $self->_bop_recurring_billing(
490 'payinfo' => $options{'payinfo'},
491 'trans_is_recur' => $trans_is_recur,
495 $content{recurring_billing} = 'YES';
496 $content{acct_code} = 'rebill'
497 if $conf->exists('credit_card-recurring_billing_acct_code');
500 } elsif ( $options{method} eq 'ECHECK' ){
502 ( $content{account_number}, $content{routing_code} ) =
503 split('@', $options{payinfo});
504 $content{bank_name} = $options{payname};
505 $content{bank_state} = exists($options{'paystate'})
506 ? $options{'paystate'}
507 : $self->getfield('paystate');
508 $content{account_type}=
509 (exists($options{'paytype'}) && $options{'paytype'})
510 ? uc($options{'paytype'})
511 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
513 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
514 $content{account_name} = $self->company;
516 $content{account_name} = $self->getfield('first'). ' '.
517 $self->getfield('last');
520 $content{customer_org} = $self->company ? 'B' : 'I';
521 $content{state_id} = exists($options{'stateid'})
522 ? $options{'stateid'}
523 : $self->getfield('stateid');
524 $content{state_id_state} = exists($options{'stateid_state'})
525 ? $options{'stateid_state'}
526 : $self->getfield('stateid_state');
527 $content{customer_ssn} = exists($options{'ss'})
531 } elsif ( $options{method} eq 'LEC' ) {
532 $content{phone} = $options{payinfo};
534 die "unknown method ". $options{method};
537 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
540 die "unknown namespace $namespace";
547 my $balance = exists( $options{'balance'} )
548 ? $options{'balance'}
551 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
552 $self->select_for_update; #mutex ... just until we get our pending record in
553 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
555 #the checks here are intended to catch concurrent payments
556 #double-form-submission prevention is taken care of in cust_pay_pending::check
559 return "The customer's balance has changed; $options{method} transaction aborted."
560 if $self->balance < $balance;
562 #also check and make sure there aren't *other* pending payments for this cust
564 my @pending = qsearch('cust_pay_pending', {
565 'custnum' => $self->custnum,
566 'status' => { op=>'!=', value=>'done' }
569 #for third-party payments only, remove pending payments if they're in the
570 #'thirdparty' (waiting for customer action) state.
571 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
572 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
573 my $error = $_->delete;
574 warn "error deleting unfinished third-party payment ".
575 $_->paypendingnum . ": $error\n"
578 @pending = grep { $_->status ne 'thirdparty' } @pending;
581 return "A payment is already being processed for this customer (".
582 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
583 "); $options{method} transaction aborted."
586 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
588 my $cust_pay_pending = new FS::cust_pay_pending {
589 'custnum' => $self->custnum,
590 'paid' => $options{amount},
592 'payby' => $bop_method2payby{$options{method}},
593 'payinfo' => $options{payinfo},
594 'paydate' => $paydate,
595 'recurring_billing' => $content{recurring_billing},
596 'pkgnum' => $options{'pkgnum'},
598 'gatewaynum' => $payment_gateway->gatewaynum || '',
599 'session_id' => $options{session_id} || '',
600 'jobnum' => $options{depend_jobnum} || '',
602 $cust_pay_pending->payunique( $options{payunique} )
603 if defined($options{payunique}) && length($options{payunique});
605 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
607 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
608 return $cpp_new_err if $cpp_new_err;
610 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
612 warn Dumper($cust_pay_pending) if $DEBUG > 2;
614 my( $action1, $action2 ) =
615 split( /\s*\,\s*/, $payment_gateway->gateway_action );
617 my $transaction = new $namespace( $payment_gateway->gateway_module,
618 $self->_bop_options(\%options),
621 $transaction->content(
622 'type' => $options{method},
623 $self->_bop_auth(\%options),
624 'action' => $action1,
625 'description' => $options{'description'},
626 'amount' => $options{amount},
627 #'invoice_number' => $options{'invnum'},
628 'customer_id' => $self->custnum,
630 'reference' => $cust_pay_pending->paypendingnum, #for now
631 'callback_url' => $payment_gateway->gateway_callback_url,
632 'cancel_url' => $payment_gateway->gateway_cancel_url,
637 $cust_pay_pending->status('pending');
638 my $cpp_pending_err = $cust_pay_pending->replace;
639 return $cpp_pending_err if $cpp_pending_err;
641 warn Dumper($transaction) if $DEBUG > 2;
643 unless ( $BOP_TESTING ) {
644 $transaction->test_transaction(1)
645 if $conf->exists('business-onlinepayment-test_transaction');
646 $transaction->submit();
648 if ( $BOP_TESTING_SUCCESS ) {
649 $transaction->is_success(1);
650 $transaction->authorization('fake auth');
652 $transaction->is_success(0);
653 $transaction->error_message('fake failure');
657 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
659 $cust_pay_pending->status('thirdparty');
660 my $cpp_err = $cust_pay_pending->replace;
661 return { error => $cpp_err } if $cpp_err;
662 return { reference => $cust_pay_pending->paypendingnum,
663 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
665 } elsif ( $transaction->is_success() && $action2 ) {
667 $cust_pay_pending->status('authorized');
668 my $cpp_authorized_err = $cust_pay_pending->replace;
669 return $cpp_authorized_err if $cpp_authorized_err;
671 my $auth = $transaction->authorization;
672 my $ordernum = $transaction->can('order_number')
673 ? $transaction->order_number
677 new Business::OnlinePayment( $payment_gateway->gateway_module,
678 $self->_bop_options(\%options),
683 type => $options{method},
685 $self->_bop_auth(\%options),
686 order_number => $ordernum,
687 amount => $options{amount},
688 authorization => $auth,
689 description => $options{'description'},
692 foreach my $field (qw( authorization_source_code returned_ACI
693 transaction_identifier validation_code
694 transaction_sequence_num local_transaction_date
695 local_transaction_time AVS_result_code )) {
696 $capture{$field} = $transaction->$field() if $transaction->can($field);
699 $capture->content( %capture );
701 $capture->test_transaction(1)
702 if $conf->exists('business-onlinepayment-test_transaction');
705 unless ( $capture->is_success ) {
706 my $e = "Authorization successful but capture failed, custnum #".
707 $self->custnum. ': '. $capture->result_code.
708 ": ". $capture->error_message;
716 # remove paycvv after initial transaction
719 #false laziness w/misc/process/payment.cgi - check both to make sure working
721 if ( length($self->paycvv)
722 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
724 my $error = $self->remove_cvv;
726 warn "WARNING: error removing cvv: $error\n";
735 if ( $transaction->can('card_token') && $transaction->card_token ) {
737 $self->card_token($transaction->card_token);
739 if ( $options{'payinfo'} eq $self->payinfo ) {
740 $self->payinfo($transaction->card_token);
741 my $error = $self->replace;
743 warn "WARNING: error storing token: $error, but proceeding anyway\n";
753 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
765 if (ref($_[0]) eq 'HASH') {
768 my ( $method, $amount ) = ( shift, shift );
770 $options{method} = $method;
771 $options{amount} = $amount;
774 if ( $options{'fake_failure'} ) {
775 return "Error: No error; test failure requested with fake_failure";
778 my $cust_pay = new FS::cust_pay ( {
779 'custnum' => $self->custnum,
780 'invnum' => $options{'invnum'},
781 'paid' => $options{amount},
783 'payby' => $bop_method2payby{$options{method}},
784 #'payinfo' => $payinfo,
785 'payinfo' => '4111111111111111',
786 #'paydate' => $paydate,
787 'paydate' => '2012-05-01',
788 'processor' => 'FakeProcessor',
790 'order_number' => '32',
792 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
795 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
796 warn " $_ => $options{$_}\n" foreach keys %options;
799 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
802 $cust_pay->invnum(''); #try again with no specific invnum
803 my $error2 = $cust_pay->insert( $options{'manual'} ?
804 ( 'manual' => 1 ) : ()
807 # gah, even with transactions.
808 my $e = 'WARNING: Card/ACH debited but database not updated - '.
809 "error inserting (fake!) payment: $error2".
810 " (previously tried insert with invnum #$options{'invnum'}" .
817 if ( $options{'paynum_ref'} ) {
818 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
826 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
828 # Wraps up processing of a realtime credit card, ACH (electronic check) or
829 # phone bill transaction.
831 sub _realtime_bop_result {
832 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
834 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
837 warn "$me _realtime_bop_result: pending transaction ".
838 $cust_pay_pending->paypendingnum. "\n";
839 warn " $_ => $options{$_}\n" foreach keys %options;
842 my $payment_gateway = $options{payment_gateway}
843 or return "no payment gateway in arguments to _realtime_bop_result";
845 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
846 my $cpp_captured_err = $cust_pay_pending->replace;
847 return $cpp_captured_err if $cpp_captured_err;
849 if ( $transaction->is_success() ) {
851 my $order_number = $transaction->order_number
852 if $transaction->can('order_number');
854 my $cust_pay = new FS::cust_pay ( {
855 'custnum' => $self->custnum,
856 'invnum' => $options{'invnum'},
857 'paid' => $cust_pay_pending->paid,
859 'payby' => $cust_pay_pending->payby,
860 'payinfo' => $options{'payinfo'},
861 'paydate' => $cust_pay_pending->paydate,
862 'pkgnum' => $cust_pay_pending->pkgnum,
863 'discount_term' => $options{'discount_term'},
864 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
865 'processor' => $payment_gateway->gateway_module,
866 'auth' => $transaction->authorization,
867 'order_number' => $order_number || '',
870 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
871 $cust_pay->payunique( $options{payunique} )
872 if defined($options{payunique}) && length($options{payunique});
874 my $oldAutoCommit = $FS::UID::AutoCommit;
875 local $FS::UID::AutoCommit = 0;
878 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
880 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
883 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
884 $cust_pay->invnum(''); #try again with no specific invnum
885 $cust_pay->paynum('');
886 my $error2 = $cust_pay->insert( $options{'manual'} ?
887 ( 'manual' => 1 ) : ()
890 # gah. but at least we have a record of the state we had to abort in
891 # from cust_pay_pending now.
892 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
893 my $e = "WARNING: $options{method} captured but payment not recorded -".
894 " error inserting payment (". $payment_gateway->gateway_module.
896 " (previously tried insert with invnum #$options{'invnum'}" .
897 ": $error ) - pending payment saved as paypendingnum ".
898 $cust_pay_pending->paypendingnum. "\n";
904 my $jobnum = $cust_pay_pending->jobnum;
906 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
908 unless ( $placeholder ) {
909 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
910 my $e = "WARNING: $options{method} captured but job $jobnum not ".
911 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
916 $error = $placeholder->delete;
919 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
920 my $e = "WARNING: $options{method} captured but could not delete ".
921 "job $jobnum for paypendingnum ".
922 $cust_pay_pending->paypendingnum. ": $error\n";
929 if ( $options{'paynum_ref'} ) {
930 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
933 $cust_pay_pending->status('done');
934 $cust_pay_pending->statustext('captured');
935 $cust_pay_pending->paynum($cust_pay->paynum);
936 my $cpp_done_err = $cust_pay_pending->replace;
938 if ( $cpp_done_err ) {
940 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
941 my $e = "WARNING: $options{method} captured but payment not recorded - ".
942 "error updating status for paypendingnum ".
943 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
949 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
951 if ( $options{'apply'} ) {
952 my $apply_error = $self->apply_payments_and_credits;
953 if ( $apply_error ) {
954 warn "WARNING: error applying payment: $apply_error\n";
955 #but we still should return no error cause the payment otherwise went
960 # have a CC surcharge portion --> one-time charge
961 if ( $options{'cc_surcharge'} > 0 ) {
962 # XXX: this whole block needs to be in a transaction?
965 $invnum = $options{'invnum'} if $options{'invnum'};
966 unless ( $invnum ) { # probably from a payment screen
967 # do we have any open invoices? pick earliest
968 # uses the fact that cust_main->cust_bill sorts by date ascending
969 my @open = $self->open_cust_bill;
970 $invnum = $open[0]->invnum if scalar(@open);
973 unless ( $invnum ) { # still nothing? pick last closed invoice
974 # again uses fact that cust_main->cust_bill sorts by date ascending
975 my @closed = $self->cust_bill;
976 $invnum = $closed[$#closed]->invnum if scalar(@closed);
980 # XXX: unlikely case - pre-paying before any invoices generated
981 # what it should do is create a new invoice and pick it
982 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
987 my $charge_error = $self->charge({
988 'amount' => $options{'cc_surcharge'},
989 'pkg' => 'Credit Card Surcharge',
991 'cust_pkg_ref' => \$cust_pkg,
994 warn 'Unable to add CC surcharge cust_pkg';
998 $cust_pkg->setup(time);
999 my $cp_error = $cust_pkg->replace;
1001 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1005 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1006 unless ( $cust_bill ) {
1007 warn "race condition + invoice deletion just happened";
1012 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1014 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1018 return ''; #no error
1024 my $perror = $payment_gateway->gateway_module. " error: ".
1025 $transaction->error_message;
1027 my $jobnum = $cust_pay_pending->jobnum;
1029 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1031 if ( $placeholder ) {
1032 my $error = $placeholder->depended_delete;
1033 $error ||= $placeholder->delete;
1034 warn "error removing provisioning jobs after declined paypendingnum ".
1035 $cust_pay_pending->paypendingnum. ": $error\n";
1037 my $e = "error finding job $jobnum for declined paypendingnum ".
1038 $cust_pay_pending->paypendingnum. "\n";
1044 unless ( $transaction->error_message ) {
1047 if ( $transaction->can('response_page') ) {
1049 'page' => ( $transaction->can('response_page')
1050 ? $transaction->response_page
1053 'code' => ( $transaction->can('response_code')
1054 ? $transaction->response_code
1057 'headers' => ( $transaction->can('response_headers')
1058 ? $transaction->response_headers
1064 "No additional debugging information available for ".
1065 $payment_gateway->gateway_module;
1068 $perror .= "No error_message returned from ".
1069 $payment_gateway->gateway_module. " -- ".
1070 ( ref($t_response) ? Dumper($t_response) : $t_response );
1074 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1075 && $conf->exists('emaildecline', $self->agentnum)
1076 && grep { $_ ne 'POST' } $self->invoicing_list
1077 && ! grep { $transaction->error_message =~ /$_/ }
1078 $conf->config('emaildecline-exclude', $self->agentnum)
1081 # Send a decline alert to the customer.
1082 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1085 # include the raw error message in the transaction state
1086 $cust_pay_pending->setfield('error', $transaction->error_message);
1087 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1088 $error = $msg_template->send( 'cust_main' => $self,
1089 'object' => $cust_pay_pending );
1093 my @templ = $conf->config('declinetemplate');
1094 my $template = new Text::Template (
1096 SOURCE => [ map "$_\n", @templ ],
1097 ) or return "($perror) can't create template: $Text::Template::ERROR";
1098 $template->compile()
1099 or return "($perror) can't compile template: $Text::Template::ERROR";
1103 scalar( $conf->config('company_name', $self->agentnum ) ),
1104 'company_address' =>
1105 join("\n", $conf->config('company_address', $self->agentnum ) ),
1106 'error' => $transaction->error_message,
1109 my $error = send_email(
1110 'from' => $conf->config('invoice_from_name', $self->agentnum ) ?
1111 $conf->config('invoice_from_name', $self->agentnum ) . ' <' .
1112 $conf->config('invoice_from', $self->agentnum ) . '>' :
1113 $conf->config('invoice_from', $self->agentnum ),
1114 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1115 'subject' => 'Your payment could not be processed',
1116 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1120 $perror .= " (also received error sending decline notification: $error)"
1125 $cust_pay_pending->status('done');
1126 $cust_pay_pending->statustext("declined: $perror");
1127 my $cpp_done_err = $cust_pay_pending->replace;
1128 if ( $cpp_done_err ) {
1129 my $e = "WARNING: $options{method} declined but pending payment not ".
1130 "resolved - error updating status for paypendingnum ".
1131 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1133 $perror = "$e ($perror)";
1141 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1143 Verifies successful third party processing of a realtime credit card,
1144 ACH (electronic check) or phone bill transaction via a
1145 Business::OnlineThirdPartyPayment realtime gateway. See
1146 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1148 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1150 The additional options I<payname>, I<city>, I<state>,
1151 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1152 if set, will override the value from the customer record.
1154 I<description> is a free-text field passed to the gateway. It defaults to
1155 "Internet services".
1157 If an I<invnum> is specified, this payment (if successful) is applied to the
1158 specified invoice. If you don't specify an I<invnum> you might want to
1159 call the B<apply_payments> method.
1161 I<quiet> can be set true to surpress email decline notices.
1163 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1164 resulting paynum, if any.
1166 I<payunique> is a unique identifier for this payment.
1168 Returns a hashref containing elements bill_error (which will be undefined
1169 upon success) and session_id of any associated session.
1173 sub realtime_botpp_capture {
1174 my( $self, $cust_pay_pending, %options ) = @_;
1176 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1179 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1180 warn " $_ => $options{$_}\n" foreach keys %options;
1183 eval "use Business::OnlineThirdPartyPayment";
1187 # select the gateway
1190 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1192 my $payment_gateway;
1193 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1194 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1195 { gatewaynum => $gatewaynum }
1197 : $self->agent->payment_gateway( 'method' => $method,
1198 # 'invnum' => $cust_pay_pending->invnum,
1199 # 'payinfo' => $cust_pay_pending->payinfo,
1202 $options{payment_gateway} = $payment_gateway; # for the helper subs
1208 my @invoicing_list = $self->invoicing_list_emailonly;
1209 if ( $conf->exists('emailinvoiceautoalways')
1210 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1211 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1212 push @invoicing_list, $self->all_emails;
1215 my $email = ($conf->exists('business-onlinepayment-email-override'))
1216 ? $conf->config('business-onlinepayment-email-override')
1217 : $invoicing_list[0];
1221 $content{email_customer} =
1222 ( $conf->exists('business-onlinepayment-email_customer')
1223 || $conf->exists('business-onlinepayment-email-override') );
1226 # run transaction(s)
1230 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1231 $self->_bop_options(\%options),
1234 $transaction->reference({ %options });
1236 $transaction->content(
1238 $self->_bop_auth(\%options),
1239 'action' => 'Post Authorization',
1240 'description' => $options{'description'},
1241 'amount' => $cust_pay_pending->paid,
1242 #'invoice_number' => $options{'invnum'},
1243 'customer_id' => $self->custnum,
1245 #3.0 is a good a time as any to get rid of this... add a config to pass it
1246 # if anyone still needs it
1247 #'referer' => 'http://cleanwhisker.420.am/',
1249 'reference' => $cust_pay_pending->paypendingnum,
1251 'phone' => $self->daytime || $self->night,
1253 # plus whatever is required for bogus capture avoidance
1256 $transaction->submit();
1259 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1261 if ( $options{'apply'} ) {
1262 my $apply_error = $self->apply_payments_and_credits;
1263 if ( $apply_error ) {
1264 warn "WARNING: error applying payment: $apply_error\n";
1269 bill_error => $error,
1270 session_id => $cust_pay_pending->session_id,
1275 =item default_payment_gateway
1277 DEPRECATED -- use agent->payment_gateway
1281 sub default_payment_gateway {
1282 my( $self, $method ) = @_;
1284 die "Real-time processing not enabled\n"
1285 unless $conf->exists('business-onlinepayment');
1287 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1290 my $bop_config = 'business-onlinepayment';
1291 $bop_config .= '-ach'
1292 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1293 my ( $processor, $login, $password, $action, @bop_options ) =
1294 $conf->config($bop_config);
1295 $action ||= 'normal authorization';
1296 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1297 die "No real-time processor is enabled - ".
1298 "did you set the business-onlinepayment configuration value?\n"
1301 ( $processor, $login, $password, $action, @bop_options )
1304 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1306 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1307 via a Business::OnlinePayment realtime gateway. See
1308 L<http://420.am/business-onlinepayment> for supported gateways.
1310 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1312 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1314 Most gateways require a reference to an original payment transaction to refund,
1315 so you probably need to specify a I<paynum>.
1317 I<amount> defaults to the original amount of the payment if not specified.
1319 I<reason> specifies a reason for the refund.
1321 I<paydate> specifies the expiration date for a credit card overriding the
1322 value from the customer record or the payment record. Specified as yyyy-mm-dd
1324 Implementation note: If I<amount> is unspecified or equal to the amount of the
1325 orignal payment, first an attempt is made to "void" the transaction via
1326 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1327 the normal attempt is made to "refund" ("credit") the transaction via the
1328 gateway is attempted. No attempt to "void" the transaction is made if the
1329 gateway has introspection data and doesn't support void.
1331 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1332 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1333 #if set, will override the value from the customer record.
1335 #If an I<invnum> is specified, this payment (if successful) is applied to the
1336 #specified invoice. If you don't specify an I<invnum> you might want to
1337 #call the B<apply_payments> method.
1341 #some false laziness w/realtime_bop, not enough to make it worth merging
1342 #but some useful small subs should be pulled out
1343 sub realtime_refund_bop {
1346 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1349 if (ref($_[0]) eq 'HASH') {
1350 %options = %{$_[0]};
1354 $options{method} = $method;
1358 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1359 warn " $_ => $options{$_}\n" foreach keys %options;
1363 # look up the original payment and optionally a gateway for that payment
1367 my $amount = $options{'amount'};
1369 my( $processor, $login, $password, @bop_options, $namespace ) ;
1370 my( $auth, $order_number ) = ( '', '', '' );
1371 my $gatewaynum = '';
1373 if ( $options{'paynum'} ) {
1375 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1376 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1377 or return "Unknown paynum $options{'paynum'}";
1378 $amount ||= $cust_pay->paid;
1380 if ( $cust_pay->get('processor') ) {
1381 ($gatewaynum, $processor, $auth, $order_number) =
1383 $cust_pay->gatewaynum,
1384 $cust_pay->processor,
1386 $cust_pay->order_number,
1389 # this payment wasn't upgraded, which probably means this won't work,
1391 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1392 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1393 $cust_pay->paybatch;
1394 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1397 if ( $gatewaynum ) { #gateway for the payment to be refunded
1399 my $payment_gateway =
1400 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1401 die "payment gateway $gatewaynum not found"
1402 unless $payment_gateway;
1404 $processor = $payment_gateway->gateway_module;
1405 $login = $payment_gateway->gateway_username;
1406 $password = $payment_gateway->gateway_password;
1407 $namespace = $payment_gateway->gateway_namespace;
1408 @bop_options = $payment_gateway->options;
1410 } else { #try the default gateway
1413 my $payment_gateway =
1414 $self->agent->payment_gateway('method' => $options{method});
1416 ( $conf_processor, $login, $password, $namespace ) =
1417 map { my $method = "gateway_$_"; $payment_gateway->$method }
1418 qw( module username password namespace );
1420 @bop_options = $payment_gateway->gatewaynum
1421 ? $payment_gateway->options
1422 : @{ $payment_gateway->get('options') };
1424 return "processor of payment $options{'paynum'} $processor does not".
1425 " match default processor $conf_processor"
1426 unless $processor eq $conf_processor;
1431 } else { # didn't specify a paynum, so look for agent gateway overrides
1432 # like a normal transaction
1434 my $payment_gateway =
1435 $self->agent->payment_gateway( 'method' => $options{method},
1436 #'payinfo' => $payinfo,
1438 my( $processor, $login, $password, $namespace ) =
1439 map { my $method = "gateway_$_"; $payment_gateway->$method }
1440 qw( module username password namespace );
1442 my @bop_options = $payment_gateway->gatewaynum
1443 ? $payment_gateway->options
1444 : @{ $payment_gateway->get('options') };
1447 return "neither amount nor paynum specified" unless $amount;
1449 eval "use $namespace";
1453 'type' => $options{method},
1455 'password' => $password,
1456 'order_number' => $order_number,
1457 'amount' => $amount,
1459 #3.0 is a good a time as any to get rid of this... add a config to pass it
1460 # if anyone still needs it
1461 #'referer' => 'http://cleanwhisker.420.am/',
1463 $content{authorization} = $auth
1464 if length($auth); #echeck/ACH transactions have an order # but no auth
1465 #(at least with authorize.net)
1467 my $currency = $conf->exists('business-onlinepayment-currency')
1468 && $conf->config('business-onlinepayment-currency');
1469 $content{currency} = $currency if $currency;
1471 my $disable_void_after;
1472 if ($conf->exists('disable_void_after')
1473 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1474 $disable_void_after = $1;
1477 #first try void if applicable
1478 my $void = new Business::OnlinePayment( $processor, @bop_options );
1481 if ($void->can('info')) {
1483 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1484 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1485 my %supported_actions = $void->info('supported_actions');
1487 if ( %supported_actions && $paytype
1488 && defined($supported_actions{$paytype})
1489 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1492 if ( $cust_pay && $cust_pay->paid == $amount
1494 ( not defined($disable_void_after) )
1495 || ( time < ($cust_pay->_date + $disable_void_after ) )
1499 warn " attempting void\n" if $DEBUG > 1;
1500 if ( $void->can('info') ) {
1501 if ( $cust_pay->payby eq 'CARD'
1502 && $void->info('CC_void_requires_card') )
1504 $content{'card_number'} = $cust_pay->payinfo;
1505 } elsif ( $cust_pay->payby eq 'CHEK'
1506 && $void->info('ECHECK_void_requires_account') )
1508 ( $content{'account_number'}, $content{'routing_code'} ) =
1509 split('@', $cust_pay->payinfo);
1510 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1513 $void->content( 'action' => 'void', %content );
1514 $void->test_transaction(1)
1515 if $conf->exists('business-onlinepayment-test_transaction');
1517 if ( $void->is_success ) {
1518 my $error = $cust_pay->void($options{'reason'});
1520 # gah, even with transactions.
1521 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1522 "error voiding payment: $error";
1526 warn " void successful\n" if $DEBUG > 1;
1531 warn " void unsuccessful, trying refund\n"
1535 my $address = $self->address1;
1536 $address .= ", ". $self->address2 if $self->address2;
1538 my($payname, $payfirst, $paylast);
1539 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1540 $payname = $self->payname;
1541 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1542 or return "Illegal payname $payname";
1543 ($payfirst, $paylast) = ($1, $2);
1545 $payfirst = $self->getfield('first');
1546 $paylast = $self->getfield('last');
1547 $payname = "$payfirst $paylast";
1550 my @invoicing_list = $self->invoicing_list_emailonly;
1551 if ( $conf->exists('emailinvoiceautoalways')
1552 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1553 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1554 push @invoicing_list, $self->all_emails;
1557 my $email = ($conf->exists('business-onlinepayment-email-override'))
1558 ? $conf->config('business-onlinepayment-email-override')
1559 : $invoicing_list[0];
1561 my $payip = exists($options{'payip'})
1564 $content{customer_ip} = $payip
1568 if ( $options{method} eq 'CC' ) {
1571 $content{card_number} = $payinfo = $cust_pay->payinfo;
1572 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1573 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1574 ($content{expiration} = "$2/$1"); # where available
1576 $content{card_number} = $payinfo = $self->payinfo;
1577 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1578 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1579 $content{expiration} = "$2/$1";
1582 } elsif ( $options{method} eq 'ECHECK' ) {
1585 $payinfo = $cust_pay->payinfo;
1587 $payinfo = $self->payinfo;
1589 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1590 $content{bank_name} = $self->payname;
1591 $content{account_type} = 'CHECKING';
1592 $content{account_name} = $payname;
1593 $content{customer_org} = $self->company ? 'B' : 'I';
1594 $content{customer_ssn} = $self->ss;
1595 } elsif ( $options{method} eq 'LEC' ) {
1596 $content{phone} = $payinfo = $self->payinfo;
1600 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1601 my %sub_content = $refund->content(
1602 'action' => 'credit',
1603 'customer_id' => $self->custnum,
1604 'last_name' => $paylast,
1605 'first_name' => $payfirst,
1607 'address' => $address,
1608 'city' => $self->city,
1609 'state' => $self->state,
1610 'zip' => $self->zip,
1611 'country' => $self->country,
1613 'phone' => $self->daytime || $self->night,
1616 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1618 $refund->test_transaction(1)
1619 if $conf->exists('business-onlinepayment-test_transaction');
1622 return "$processor error: ". $refund->error_message
1623 unless $refund->is_success();
1625 $order_number = $refund->order_number if $refund->can('order_number');
1627 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1628 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1629 last unless @cust_bill_pay;
1630 my $cust_bill_pay = pop @cust_bill_pay;
1631 my $error = $cust_bill_pay->delete;
1635 my $cust_refund = new FS::cust_refund ( {
1636 'custnum' => $self->custnum,
1637 'paynum' => $options{'paynum'},
1638 'refund' => $amount,
1640 'payby' => $bop_method2payby{$options{method}},
1641 'payinfo' => $payinfo,
1642 'reason' => $options{'reason'} || 'card or ACH refund',
1643 'gatewaynum' => $gatewaynum, # may be null
1644 'processor' => $processor,
1645 'auth' => $refund->authorization,
1646 'order_number' => $order_number,
1648 my $error = $cust_refund->insert;
1650 $cust_refund->paynum(''); #try again with no specific paynum
1651 my $error2 = $cust_refund->insert;
1653 # gah, even with transactions.
1654 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1655 "error inserting refund ($processor): $error2".
1656 " (previously tried insert with paynum #$options{'paynum'}" .
1675 L<FS::cust_main>, L<FS::cust_main::Billing>