1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
8 use Business::CreditCard 0.28;
10 use FS::Record qw( qsearch qsearchs );
13 use FS::cust_pay_pending;
14 use FS::cust_bill_pay;
18 $realtime_bop_decline_quiet = 0;
20 # 1 is mostly method/subroutine entry and options
21 # 2 traces progress of some operations
22 # 3 is even more information including possibly sensitive data
24 $me = '[FS::cust_main::Billing_Realtime]';
27 our $BOP_TESTING_SUCCESS = 1;
29 install_callback FS::UID sub {
31 #yes, need it for stuff below (prolly should be cached)
36 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
42 These methods are available on FS::cust_main objects.
48 =item realtime_cust_payby
52 sub realtime_cust_payby {
53 my( $self, %options ) = @_;
55 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
57 $options{amount} = $self->balance unless exists( $options{amount} );
59 my @cust_payby = qsearch({
60 'table' => 'cust_payby',
61 'hashref' => { 'custnum' => $self->custnum, },
62 'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
63 'order_by' => 'ORDER BY weight ASC',
67 foreach my $cust_payby (@cust_payby) {
68 $error = $cust_payby->realtime_bop( %options, );
72 #XXX what about the earlier errors?
78 =item realtime_collect [ OPTION => VALUE ... ]
80 Attempt to collect the customer's current balance with a realtime credit
81 card, electronic check, or phone bill transaction (see realtime_bop() below).
83 Returns the result of realtime_bop(): nothing, an error message, or a
84 hashref of state information for a third-party transaction.
86 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
88 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
89 then it is deduced from the customer record.
91 If no I<amount> is specified, then the customer balance is used.
93 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
94 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
95 if set, will override the value from the customer record.
97 I<description> is a free-text field passed to the gateway. It defaults to
98 the value defined by the business-onlinepayment-description configuration
99 option, or "Internet services" if that is unset.
101 If an I<invnum> is specified, this payment (if successful) is applied to the
104 I<apply> will automatically apply a resulting payment.
106 I<quiet> can be set true to suppress email decline notices.
108 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
109 resulting paynum, if any.
111 I<payunique> is a unique identifier for this payment.
113 I<session_id> is a session identifier associated with this payment.
115 I<depend_jobnum> allows payment capture to unlock export jobs
119 sub realtime_collect {
120 my( $self, %options ) = @_;
122 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
125 warn "$me realtime_collect:\n";
126 warn " $_ => $options{$_}\n" foreach keys %options;
129 $options{amount} = $self->balance unless exists( $options{amount} );
130 $options{method} = FS::payby->payby2bop($self->payby)
131 unless exists( $options{method} );
133 return $self->realtime_bop({%options});
137 =item realtime_bop { [ ARG => VALUE ... ] }
139 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
140 via a Business::OnlinePayment realtime gateway. See
141 L<http://420.am/business-onlinepayment> for supported gateways.
143 Required arguments in the hashref are I<method>, and I<amount>
145 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
147 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
149 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
150 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
151 if set, will override the value from the customer record.
153 I<description> is a free-text field passed to the gateway. It defaults to
154 the value defined by the business-onlinepayment-description configuration
155 option, or "Internet services" if that is unset.
157 If an I<invnum> is specified, this payment (if successful) is applied to the
158 specified invoice. If the customer has exactly one open invoice, that
159 invoice number will be assumed. If you don't specify an I<invnum> you might
160 want to call the B<apply_payments> method or set the I<apply> option.
162 I<apply> can be set to true to apply a resulting payment.
164 I<quiet> can be set true to surpress email decline notices.
166 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
167 resulting paynum, if any.
169 I<payunique> is a unique identifier for this payment.
171 I<session_id> is a session identifier associated with this payment.
173 I<depend_jobnum> allows payment capture to unlock export jobs
175 I<discount_term> attempts to take a discount by prepaying for discount_term.
176 The payment will fail if I<amount> is incorrect for this discount term.
178 A direct (Business::OnlinePayment) transaction will return nothing on success,
179 or an error message on failure.
181 A third-party transaction will return a hashref containing:
183 - popup_url: the URL to which a browser should be redirected to complete
185 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
186 - reference: a reference ID for the transaction, to show the customer.
188 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
192 # some helper routines
193 sub _bop_recurring_billing {
194 my( $self, %opt ) = @_;
196 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
198 if ( defined($method) && $method eq 'transaction_is_recur' ) {
200 return 1 if $opt{'trans_is_recur'};
204 # return 1 if the payinfo has been used for another payment
205 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
213 sub _payment_gateway {
214 my ($self, $options) = @_;
216 if ( $options->{'selfservice'} ) {
217 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
219 return $options->{payment_gateway} ||=
220 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
224 if ( $options->{'fake_gatewaynum'} ) {
225 $options->{payment_gateway} =
226 qsearchs('payment_gateway',
227 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
231 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
232 unless exists($options->{payment_gateway});
234 $options->{payment_gateway};
238 my ($self, $options) = @_;
241 'login' => $options->{payment_gateway}->gateway_username,
242 'password' => $options->{payment_gateway}->gateway_password,
247 my ($self, $options) = @_;
249 $options->{payment_gateway}->gatewaynum
250 ? $options->{payment_gateway}->options
251 : @{ $options->{payment_gateway}->get('options') };
256 my ($self, $options) = @_;
258 unless ( $options->{'description'} ) {
259 if ( $conf->exists('business-onlinepayment-description') ) {
260 my $dtempl = $conf->config('business-onlinepayment-description');
262 my $agent = $self->agent->agent;
264 $options->{'description'} = eval qq("$dtempl");
266 $options->{'description'} = 'Internet services';
270 unless ( exists( $options->{'payinfo'} ) ) {
271 $options->{'payinfo'} = $self->payinfo;
272 $options->{'paymask'} = $self->paymask;
275 # Default invoice number if the customer has exactly one open invoice.
276 if( ! $options->{'invnum'} ) {
277 $options->{'invnum'} = '';
278 my @open = $self->open_cust_bill;
279 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
282 $options->{payname} = $self->payname unless exists( $options->{payname} );
286 my ($self, $options) = @_;
289 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
290 $content{customer_ip} = $payip if length($payip);
292 $content{invoice_number} = $options->{'invnum'}
293 if exists($options->{'invnum'}) && length($options->{'invnum'});
295 $content{email_customer} =
296 ( $conf->exists('business-onlinepayment-email_customer')
297 || $conf->exists('business-onlinepayment-email-override') );
299 my ($payname, $payfirst, $paylast);
300 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
301 ($payname = $options->{payname}) =~
302 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
303 or return "Illegal payname $payname";
304 ($payfirst, $paylast) = ($1, $2);
306 $payfirst = $self->getfield('first');
307 $paylast = $self->getfield('last');
308 $payname = "$payfirst $paylast";
311 $content{last_name} = $paylast;
312 $content{first_name} = $payfirst;
314 $content{name} = $payname;
316 $content{address} = exists($options->{'address1'})
317 ? $options->{'address1'}
319 my $address2 = exists($options->{'address2'})
320 ? $options->{'address2'}
322 $content{address} .= ", ". $address2 if length($address2);
324 $content{city} = exists($options->{city})
327 $content{state} = exists($options->{state})
330 $content{zip} = exists($options->{zip})
333 $content{country} = exists($options->{country})
334 ? $options->{country}
337 $content{phone} = $self->daytime || $self->night;
339 my $currency = $conf->exists('business-onlinepayment-currency')
340 && $conf->config('business-onlinepayment-currency');
341 $content{currency} = $currency if $currency;
346 my %bop_method2payby = (
356 confess "Can't call realtime_bop within another transaction ".
357 '($FS::UID::AutoCommit is false)'
358 unless $FS::UID::AutoCommit;
360 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
363 if (ref($_[0]) eq 'HASH') {
366 my ( $method, $amount ) = ( shift, shift );
368 $options{method} = $method;
369 $options{amount} = $amount;
374 # optional credit card surcharge
377 my $cc_surcharge = 0;
378 my $cc_surcharge_pct = 0;
379 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
380 if $conf->config('credit-card-surcharge-percentage')
381 && $options{method} eq 'CC';
383 # always add cc surcharge if called from event
384 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
385 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
386 $options{'amount'} += $cc_surcharge;
387 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
389 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
390 # payment screen), so consider the given
391 # amount as post-surcharge
392 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
395 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
396 $options{'cc_surcharge'} = $cc_surcharge;
400 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
401 warn " cc_surcharge = $cc_surcharge\n";
404 warn " $_ => $options{$_}\n" foreach keys %options;
407 return $self->fake_bop(\%options) if $options{'fake'};
409 $self->_bop_defaults(\%options);
412 # set trans_is_recur based on invnum if there is one
415 my $trans_is_recur = 0;
416 if ( $options{'invnum'} ) {
418 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
419 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
425 $cust_bill->cust_bill_pkg;
428 if grep { $_->freq ne '0' } @part_pkg;
436 my $payment_gateway = $self->_payment_gateway( \%options );
437 my $namespace = $payment_gateway->gateway_namespace;
439 eval "use $namespace";
443 # check for banned credit card/ACH
446 my $ban = FS::banned_pay->ban_search(
447 'payby' => $bop_method2payby{$options{method}},
448 'payinfo' => $options{payinfo},
450 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
453 # check for term discount validity
456 my $discount_term = $options{discount_term};
457 if ( $discount_term ) {
458 my $bill = ($self->cust_bill)[-1]
459 or return "Can't apply a term discount to an unbilled customer";
460 my $plan = FS::discount_plan->new(
462 months => $discount_term
463 ) or return "No discount available for term '$discount_term'";
465 if ( $plan->discounted_total != $options{amount} ) {
466 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
474 my $bop_content = $self->_bop_content(\%options);
475 return $bop_content unless ref($bop_content);
477 my @invoicing_list = $self->invoicing_list_emailonly;
478 if ( $conf->exists('emailinvoiceautoalways')
479 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
480 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
481 push @invoicing_list, $self->all_emails;
484 my $email = ($conf->exists('business-onlinepayment-email-override'))
485 ? $conf->config('business-onlinepayment-email-override')
486 : $invoicing_list[0];
491 if ( $namespace eq 'Business::OnlinePayment' ) {
493 if ( $options{method} eq 'CC' ) {
495 $content{card_number} = $options{payinfo};
496 $paydate = exists($options{'paydate'})
497 ? $options{'paydate'}
499 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
500 $content{expiration} = "$2/$1";
502 my $paycvv = exists($options{'paycvv'})
505 $content{cvv2} = $paycvv
508 my $paystart_month = exists($options{'paystart_month'})
509 ? $options{'paystart_month'}
510 : $self->paystart_month;
512 my $paystart_year = exists($options{'paystart_year'})
513 ? $options{'paystart_year'}
514 : $self->paystart_year;
516 $content{card_start} = "$paystart_month/$paystart_year"
517 if $paystart_month && $paystart_year;
519 my $payissue = exists($options{'payissue'})
520 ? $options{'payissue'}
522 $content{issue_number} = $payissue if $payissue;
524 if ( $self->_bop_recurring_billing(
525 'payinfo' => $options{'payinfo'},
526 'trans_is_recur' => $trans_is_recur,
530 $content{recurring_billing} = 'YES';
531 $content{acct_code} = 'rebill'
532 if $conf->exists('credit_card-recurring_billing_acct_code');
535 } elsif ( $options{method} eq 'ECHECK' ){
537 ( $content{account_number}, $content{routing_code} ) =
538 split('@', $options{payinfo});
539 $content{bank_name} = $options{payname};
540 $content{bank_state} = exists($options{'paystate'})
541 ? $options{'paystate'}
542 : $self->getfield('paystate');
543 $content{account_type}=
544 (exists($options{'paytype'}) && $options{'paytype'})
545 ? uc($options{'paytype'})
546 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
548 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
549 $content{account_name} = $self->company;
551 $content{account_name} = $self->getfield('first'). ' '.
552 $self->getfield('last');
555 $content{customer_org} = $self->company ? 'B' : 'I';
556 $content{state_id} = exists($options{'stateid'})
557 ? $options{'stateid'}
558 : $self->getfield('stateid');
559 $content{state_id_state} = exists($options{'stateid_state'})
560 ? $options{'stateid_state'}
561 : $self->getfield('stateid_state');
562 $content{customer_ssn} = exists($options{'ss'})
566 } elsif ( $options{method} eq 'LEC' ) {
567 $content{phone} = $options{payinfo};
569 die "unknown method ". $options{method};
572 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
575 die "unknown namespace $namespace";
582 my $balance = exists( $options{'balance'} )
583 ? $options{'balance'}
586 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
587 $self->select_for_update; #mutex ... just until we get our pending record in
588 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
590 #the checks here are intended to catch concurrent payments
591 #double-form-submission prevention is taken care of in cust_pay_pending::check
594 return "The customer's balance has changed; $options{method} transaction aborted."
595 if $self->balance < $balance;
597 #also check and make sure there aren't *other* pending payments for this cust
599 my @pending = qsearch('cust_pay_pending', {
600 'custnum' => $self->custnum,
601 'status' => { op=>'!=', value=>'done' }
604 #for third-party payments only, remove pending payments if they're in the
605 #'thirdparty' (waiting for customer action) state.
606 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
607 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
608 my $error = $_->delete;
609 warn "error deleting unfinished third-party payment ".
610 $_->paypendingnum . ": $error\n"
613 @pending = grep { $_->status ne 'thirdparty' } @pending;
616 return "A payment is already being processed for this customer (".
617 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
618 "); $options{method} transaction aborted."
621 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
623 my $cust_pay_pending = new FS::cust_pay_pending {
624 'custnum' => $self->custnum,
625 'paid' => $options{amount},
627 'payby' => $bop_method2payby{$options{method}},
628 'payinfo' => $options{payinfo},
629 'paymask' => $options{paymask},
630 'paydate' => $paydate,
631 'recurring_billing' => $content{recurring_billing},
632 'pkgnum' => $options{'pkgnum'},
634 'gatewaynum' => $payment_gateway->gatewaynum || '',
635 'session_id' => $options{session_id} || '',
636 'jobnum' => $options{depend_jobnum} || '',
638 $cust_pay_pending->payunique( $options{payunique} )
639 if defined($options{payunique}) && length($options{payunique});
641 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
643 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
644 return $cpp_new_err if $cpp_new_err;
646 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
648 warn Dumper($cust_pay_pending) if $DEBUG > 2;
650 my( $action1, $action2 ) =
651 split( /\s*\,\s*/, $payment_gateway->gateway_action );
653 my $transaction = new $namespace( $payment_gateway->gateway_module,
654 $self->_bop_options(\%options),
657 $transaction->content(
658 'type' => $options{method},
659 $self->_bop_auth(\%options),
660 'action' => $action1,
661 'description' => $options{'description'},
662 'amount' => $options{amount},
663 #'invoice_number' => $options{'invnum'},
664 'customer_id' => $self->custnum,
666 'reference' => $cust_pay_pending->paypendingnum, #for now
667 'callback_url' => $payment_gateway->gateway_callback_url,
668 'cancel_url' => $payment_gateway->gateway_cancel_url,
673 $cust_pay_pending->status('pending');
674 my $cpp_pending_err = $cust_pay_pending->replace;
675 return $cpp_pending_err if $cpp_pending_err;
677 warn Dumper($transaction) if $DEBUG > 2;
679 unless ( $BOP_TESTING ) {
680 $transaction->test_transaction(1)
681 if $conf->exists('business-onlinepayment-test_transaction');
682 $transaction->submit();
684 if ( $BOP_TESTING_SUCCESS ) {
685 $transaction->is_success(1);
686 $transaction->authorization('fake auth');
688 $transaction->is_success(0);
689 $transaction->error_message('fake failure');
693 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
695 $cust_pay_pending->status('thirdparty');
696 my $cpp_err = $cust_pay_pending->replace;
697 return { error => $cpp_err } if $cpp_err;
698 return { reference => $cust_pay_pending->paypendingnum,
699 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
701 } elsif ( $transaction->is_success() && $action2 ) {
703 $cust_pay_pending->status('authorized');
704 my $cpp_authorized_err = $cust_pay_pending->replace;
705 return $cpp_authorized_err if $cpp_authorized_err;
707 my $auth = $transaction->authorization;
708 my $ordernum = $transaction->can('order_number')
709 ? $transaction->order_number
713 new Business::OnlinePayment( $payment_gateway->gateway_module,
714 $self->_bop_options(\%options),
719 type => $options{method},
721 $self->_bop_auth(\%options),
722 order_number => $ordernum,
723 amount => $options{amount},
724 authorization => $auth,
725 description => $options{'description'},
728 foreach my $field (qw( authorization_source_code returned_ACI
729 transaction_identifier validation_code
730 transaction_sequence_num local_transaction_date
731 local_transaction_time AVS_result_code )) {
732 $capture{$field} = $transaction->$field() if $transaction->can($field);
735 $capture->content( %capture );
737 $capture->test_transaction(1)
738 if $conf->exists('business-onlinepayment-test_transaction');
741 unless ( $capture->is_success ) {
742 my $e = "Authorization successful but capture failed, custnum #".
743 $self->custnum. ': '. $capture->result_code.
744 ": ". $capture->error_message;
752 # remove paycvv after initial transaction
755 #false laziness w/misc/process/payment.cgi - check both to make sure working
757 if ( length($self->paycvv)
758 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
760 my $error = $self->remove_cvv;
762 warn "WARNING: error removing cvv: $error\n";
771 if ( $transaction->can('card_token') && $transaction->card_token ) {
773 if ( $options{'payinfo'} eq $self->payinfo ) {
774 $self->payinfo($transaction->card_token);
775 my $error = $self->replace;
777 warn "WARNING: error storing token: $error, but proceeding anyway\n";
787 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
799 if (ref($_[0]) eq 'HASH') {
802 my ( $method, $amount ) = ( shift, shift );
804 $options{method} = $method;
805 $options{amount} = $amount;
808 if ( $options{'fake_failure'} ) {
809 return "Error: No error; test failure requested with fake_failure";
812 my $cust_pay = new FS::cust_pay ( {
813 'custnum' => $self->custnum,
814 'invnum' => $options{'invnum'},
815 'paid' => $options{amount},
817 'payby' => $bop_method2payby{$options{method}},
818 #'payinfo' => $payinfo,
819 'payinfo' => '4111111111111111',
820 #'paydate' => $paydate,
821 'paydate' => '2012-05-01',
822 'processor' => 'FakeProcessor',
824 'order_number' => '32',
826 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
829 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
830 warn " $_ => $options{$_}\n" foreach keys %options;
833 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
836 $cust_pay->invnum(''); #try again with no specific invnum
837 my $error2 = $cust_pay->insert( $options{'manual'} ?
838 ( 'manual' => 1 ) : ()
841 # gah, even with transactions.
842 my $e = 'WARNING: Card/ACH debited but database not updated - '.
843 "error inserting (fake!) payment: $error2".
844 " (previously tried insert with invnum #$options{'invnum'}" .
851 if ( $options{'paynum_ref'} ) {
852 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
860 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
862 # Wraps up processing of a realtime credit card, ACH (electronic check) or
863 # phone bill transaction.
865 sub _realtime_bop_result {
866 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
868 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
871 warn "$me _realtime_bop_result: pending transaction ".
872 $cust_pay_pending->paypendingnum. "\n";
873 warn " $_ => $options{$_}\n" foreach keys %options;
876 my $payment_gateway = $options{payment_gateway}
877 or return "no payment gateway in arguments to _realtime_bop_result";
879 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
880 my $cpp_captured_err = $cust_pay_pending->replace;
881 return $cpp_captured_err if $cpp_captured_err;
883 if ( $transaction->is_success() ) {
885 my $order_number = $transaction->order_number
886 if $transaction->can('order_number');
888 my $cust_pay = new FS::cust_pay ( {
889 'custnum' => $self->custnum,
890 'invnum' => $options{'invnum'},
891 'paid' => $cust_pay_pending->paid,
893 'payby' => $cust_pay_pending->payby,
894 'payinfo' => $options{'payinfo'},
895 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
896 'paydate' => $cust_pay_pending->paydate,
897 'pkgnum' => $cust_pay_pending->pkgnum,
898 'discount_term' => $options{'discount_term'},
899 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
900 'processor' => $payment_gateway->gateway_module,
901 'auth' => $transaction->authorization,
902 'order_number' => $order_number || '',
905 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
906 $cust_pay->payunique( $options{payunique} )
907 if defined($options{payunique}) && length($options{payunique});
909 my $oldAutoCommit = $FS::UID::AutoCommit;
910 local $FS::UID::AutoCommit = 0;
913 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
915 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
918 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
919 $cust_pay->invnum(''); #try again with no specific invnum
920 $cust_pay->paynum('');
921 my $error2 = $cust_pay->insert( $options{'manual'} ?
922 ( 'manual' => 1 ) : ()
925 # gah. but at least we have a record of the state we had to abort in
926 # from cust_pay_pending now.
927 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
928 my $e = "WARNING: $options{method} captured but payment not recorded -".
929 " error inserting payment (". $payment_gateway->gateway_module.
931 " (previously tried insert with invnum #$options{'invnum'}" .
932 ": $error ) - pending payment saved as paypendingnum ".
933 $cust_pay_pending->paypendingnum. "\n";
939 my $jobnum = $cust_pay_pending->jobnum;
941 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
943 unless ( $placeholder ) {
944 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
945 my $e = "WARNING: $options{method} captured but job $jobnum not ".
946 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
951 $error = $placeholder->delete;
954 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
955 my $e = "WARNING: $options{method} captured but could not delete ".
956 "job $jobnum for paypendingnum ".
957 $cust_pay_pending->paypendingnum. ": $error\n";
962 $cust_pay_pending->set('jobnum','');
966 if ( $options{'paynum_ref'} ) {
967 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
970 $cust_pay_pending->status('done');
971 $cust_pay_pending->statustext('captured');
972 $cust_pay_pending->paynum($cust_pay->paynum);
973 my $cpp_done_err = $cust_pay_pending->replace;
975 if ( $cpp_done_err ) {
977 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
978 my $e = "WARNING: $options{method} captured but payment not recorded - ".
979 "error updating status for paypendingnum ".
980 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
986 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
988 if ( $options{'apply'} ) {
989 my $apply_error = $self->apply_payments_and_credits;
990 if ( $apply_error ) {
991 warn "WARNING: error applying payment: $apply_error\n";
992 #but we still should return no error cause the payment otherwise went
997 # have a CC surcharge portion --> one-time charge
998 if ( $options{'cc_surcharge'} > 0 ) {
999 # XXX: this whole block needs to be in a transaction?
1002 $invnum = $options{'invnum'} if $options{'invnum'};
1003 unless ( $invnum ) { # probably from a payment screen
1004 # do we have any open invoices? pick earliest
1005 # uses the fact that cust_main->cust_bill sorts by date ascending
1006 my @open = $self->open_cust_bill;
1007 $invnum = $open[0]->invnum if scalar(@open);
1010 unless ( $invnum ) { # still nothing? pick last closed invoice
1011 # again uses fact that cust_main->cust_bill sorts by date ascending
1012 my @closed = $self->cust_bill;
1013 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1016 unless ( $invnum ) {
1017 # XXX: unlikely case - pre-paying before any invoices generated
1018 # what it should do is create a new invoice and pick it
1019 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1024 my $charge_error = $self->charge({
1025 'amount' => $options{'cc_surcharge'},
1026 'pkg' => 'Credit Card Surcharge',
1028 'cust_pkg_ref' => \$cust_pkg,
1031 warn 'Unable to add CC surcharge cust_pkg';
1035 $cust_pkg->setup(time);
1036 my $cp_error = $cust_pkg->replace;
1038 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1042 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1043 unless ( $cust_bill ) {
1044 warn "race condition + invoice deletion just happened";
1049 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1051 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1055 return ''; #no error
1061 my $perror = $transaction->error_message;
1062 #$payment_gateway->gateway_module. " error: ".
1063 # removed for conciseness
1065 my $jobnum = $cust_pay_pending->jobnum;
1067 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1069 if ( $placeholder ) {
1070 my $error = $placeholder->depended_delete;
1071 $error ||= $placeholder->delete;
1072 $cust_pay_pending->set('jobnum','');
1073 warn "error removing provisioning jobs after declined paypendingnum ".
1074 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1076 my $e = "error finding job $jobnum for declined paypendingnum ".
1077 $cust_pay_pending->paypendingnum. "\n";
1083 unless ( $transaction->error_message ) {
1086 if ( $transaction->can('response_page') ) {
1088 'page' => ( $transaction->can('response_page')
1089 ? $transaction->response_page
1092 'code' => ( $transaction->can('response_code')
1093 ? $transaction->response_code
1096 'headers' => ( $transaction->can('response_headers')
1097 ? $transaction->response_headers
1103 "No additional debugging information available for ".
1104 $payment_gateway->gateway_module;
1107 $perror .= "No error_message returned from ".
1108 $payment_gateway->gateway_module. " -- ".
1109 ( ref($t_response) ? Dumper($t_response) : $t_response );
1113 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1114 && $conf->exists('emaildecline', $self->agentnum)
1115 && grep { $_ ne 'POST' } $self->invoicing_list
1116 && ! grep { $transaction->error_message =~ /$_/ }
1117 $conf->config('emaildecline-exclude', $self->agentnum)
1120 # Send a decline alert to the customer.
1121 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1124 # include the raw error message in the transaction state
1125 $cust_pay_pending->setfield('error', $transaction->error_message);
1126 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1127 $error = $msg_template->send( 'cust_main' => $self,
1128 'object' => $cust_pay_pending );
1132 $perror .= " (also received error sending decline notification: $error)"
1137 $cust_pay_pending->status('done');
1138 $cust_pay_pending->statustext($perror);
1139 #'declined:': no, that's failure_status
1140 if ( $transaction->can('failure_status') ) {
1141 $cust_pay_pending->failure_status( $transaction->failure_status );
1143 my $cpp_done_err = $cust_pay_pending->replace;
1144 if ( $cpp_done_err ) {
1145 my $e = "WARNING: $options{method} declined but pending payment not ".
1146 "resolved - error updating status for paypendingnum ".
1147 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1149 $perror = "$e ($perror)";
1157 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1159 Verifies successful third party processing of a realtime credit card,
1160 ACH (electronic check) or phone bill transaction via a
1161 Business::OnlineThirdPartyPayment realtime gateway. See
1162 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1164 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1166 The additional options I<payname>, I<city>, I<state>,
1167 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1168 if set, will override the value from the customer record.
1170 I<description> is a free-text field passed to the gateway. It defaults to
1171 "Internet services".
1173 If an I<invnum> is specified, this payment (if successful) is applied to the
1174 specified invoice. If you don't specify an I<invnum> you might want to
1175 call the B<apply_payments> method.
1177 I<quiet> can be set true to surpress email decline notices.
1179 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1180 resulting paynum, if any.
1182 I<payunique> is a unique identifier for this payment.
1184 Returns a hashref containing elements bill_error (which will be undefined
1185 upon success) and session_id of any associated session.
1189 sub realtime_botpp_capture {
1190 my( $self, $cust_pay_pending, %options ) = @_;
1192 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1195 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1196 warn " $_ => $options{$_}\n" foreach keys %options;
1199 eval "use Business::OnlineThirdPartyPayment";
1203 # select the gateway
1206 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1208 my $payment_gateway;
1209 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1210 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1211 { gatewaynum => $gatewaynum }
1213 : $self->agent->payment_gateway( 'method' => $method,
1214 # 'invnum' => $cust_pay_pending->invnum,
1215 # 'payinfo' => $cust_pay_pending->payinfo,
1218 $options{payment_gateway} = $payment_gateway; # for the helper subs
1224 my @invoicing_list = $self->invoicing_list_emailonly;
1225 if ( $conf->exists('emailinvoiceautoalways')
1226 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1227 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1228 push @invoicing_list, $self->all_emails;
1231 my $email = ($conf->exists('business-onlinepayment-email-override'))
1232 ? $conf->config('business-onlinepayment-email-override')
1233 : $invoicing_list[0];
1237 $content{email_customer} =
1238 ( $conf->exists('business-onlinepayment-email_customer')
1239 || $conf->exists('business-onlinepayment-email-override') );
1242 # run transaction(s)
1246 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1247 $self->_bop_options(\%options),
1250 $transaction->reference({ %options });
1252 $transaction->content(
1254 $self->_bop_auth(\%options),
1255 'action' => 'Post Authorization',
1256 'description' => $options{'description'},
1257 'amount' => $cust_pay_pending->paid,
1258 #'invoice_number' => $options{'invnum'},
1259 'customer_id' => $self->custnum,
1260 'reference' => $cust_pay_pending->paypendingnum,
1262 'phone' => $self->daytime || $self->night,
1264 # plus whatever is required for bogus capture avoidance
1267 $transaction->submit();
1270 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1272 if ( $options{'apply'} ) {
1273 my $apply_error = $self->apply_payments_and_credits;
1274 if ( $apply_error ) {
1275 warn "WARNING: error applying payment: $apply_error\n";
1280 bill_error => $error,
1281 session_id => $cust_pay_pending->session_id,
1286 =item default_payment_gateway
1288 DEPRECATED -- use agent->payment_gateway
1292 sub default_payment_gateway {
1293 my( $self, $method ) = @_;
1295 die "Real-time processing not enabled\n"
1296 unless $conf->exists('business-onlinepayment');
1298 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1301 my $bop_config = 'business-onlinepayment';
1302 $bop_config .= '-ach'
1303 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1304 my ( $processor, $login, $password, $action, @bop_options ) =
1305 $conf->config($bop_config);
1306 $action ||= 'normal authorization';
1307 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1308 die "No real-time processor is enabled - ".
1309 "did you set the business-onlinepayment configuration value?\n"
1312 ( $processor, $login, $password, $action, @bop_options )
1315 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1317 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1318 via a Business::OnlinePayment realtime gateway. See
1319 L<http://420.am/business-onlinepayment> for supported gateways.
1321 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1323 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1325 Most gateways require a reference to an original payment transaction to refund,
1326 so you probably need to specify a I<paynum>.
1328 I<amount> defaults to the original amount of the payment if not specified.
1330 I<reason> specifies a reason for the refund.
1332 I<paydate> specifies the expiration date for a credit card overriding the
1333 value from the customer record or the payment record. Specified as yyyy-mm-dd
1335 Implementation note: If I<amount> is unspecified or equal to the amount of the
1336 orignal payment, first an attempt is made to "void" the transaction via
1337 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1338 the normal attempt is made to "refund" ("credit") the transaction via the
1339 gateway is attempted. No attempt to "void" the transaction is made if the
1340 gateway has introspection data and doesn't support void.
1342 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1343 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1344 #if set, will override the value from the customer record.
1346 #If an I<invnum> is specified, this payment (if successful) is applied to the
1347 #specified invoice. If you don't specify an I<invnum> you might want to
1348 #call the B<apply_payments> method.
1352 #some false laziness w/realtime_bop, not enough to make it worth merging
1353 #but some useful small subs should be pulled out
1354 sub realtime_refund_bop {
1357 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1360 if (ref($_[0]) eq 'HASH') {
1361 %options = %{$_[0]};
1365 $options{method} = $method;
1369 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1370 warn " $_ => $options{$_}\n" foreach keys %options;
1376 # look up the original payment and optionally a gateway for that payment
1380 my $amount = $options{'amount'};
1382 my( $processor, $login, $password, @bop_options, $namespace ) ;
1383 my( $auth, $order_number ) = ( '', '', '' );
1384 my $gatewaynum = '';
1386 if ( $options{'paynum'} ) {
1388 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1389 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1390 or return "Unknown paynum $options{'paynum'}";
1391 $amount ||= $cust_pay->paid;
1393 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1394 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1396 if ( $cust_pay->get('processor') ) {
1397 ($gatewaynum, $processor, $auth, $order_number) =
1399 $cust_pay->gatewaynum,
1400 $cust_pay->processor,
1402 $cust_pay->order_number,
1405 # this payment wasn't upgraded, which probably means this won't work,
1407 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1408 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1409 $cust_pay->paybatch;
1410 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1413 if ( $gatewaynum ) { #gateway for the payment to be refunded
1415 my $payment_gateway =
1416 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1417 die "payment gateway $gatewaynum not found"
1418 unless $payment_gateway;
1420 $processor = $payment_gateway->gateway_module;
1421 $login = $payment_gateway->gateway_username;
1422 $password = $payment_gateway->gateway_password;
1423 $namespace = $payment_gateway->gateway_namespace;
1424 @bop_options = $payment_gateway->options;
1426 } else { #try the default gateway
1429 my $payment_gateway =
1430 $self->agent->payment_gateway('method' => $options{method});
1432 ( $conf_processor, $login, $password, $namespace ) =
1433 map { my $method = "gateway_$_"; $payment_gateway->$method }
1434 qw( module username password namespace );
1436 @bop_options = $payment_gateway->gatewaynum
1437 ? $payment_gateway->options
1438 : @{ $payment_gateway->get('options') };
1440 return "processor of payment $options{'paynum'} $processor does not".
1441 " match default processor $conf_processor"
1442 unless $processor eq $conf_processor;
1447 } else { # didn't specify a paynum, so look for agent gateway overrides
1448 # like a normal transaction
1450 my $payment_gateway =
1451 $self->agent->payment_gateway( 'method' => $options{method},
1452 #'payinfo' => $payinfo,
1454 my( $processor, $login, $password, $namespace ) =
1455 map { my $method = "gateway_$_"; $payment_gateway->$method }
1456 qw( module username password namespace );
1458 my @bop_options = $payment_gateway->gatewaynum
1459 ? $payment_gateway->options
1460 : @{ $payment_gateway->get('options') };
1463 return "neither amount nor paynum specified" unless $amount;
1465 eval "use $namespace";
1470 'type' => $options{method},
1472 'password' => $password,
1473 'order_number' => $order_number,
1474 'amount' => $amount,
1476 $content{authorization} = $auth
1477 if length($auth); #echeck/ACH transactions have an order # but no auth
1478 #(at least with authorize.net)
1480 my $currency = $conf->exists('business-onlinepayment-currency')
1481 && $conf->config('business-onlinepayment-currency');
1482 $content{currency} = $currency if $currency;
1484 my $disable_void_after;
1485 if ($conf->exists('disable_void_after')
1486 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1487 $disable_void_after = $1;
1490 #first try void if applicable
1491 my $void = new Business::OnlinePayment( $processor, @bop_options );
1494 if ($void->can('info')) {
1496 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1497 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1498 my %supported_actions = $void->info('supported_actions');
1500 if ( %supported_actions && $paytype
1501 && defined($supported_actions{$paytype})
1502 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1505 if ( $cust_pay && $cust_pay->paid == $amount
1507 ( not defined($disable_void_after) )
1508 || ( time < ($cust_pay->_date + $disable_void_after ) )
1512 warn " attempting void\n" if $DEBUG > 1;
1513 if ( $void->can('info') ) {
1514 if ( $cust_pay->payby eq 'CARD'
1515 && $void->info('CC_void_requires_card') )
1517 $content{'card_number'} = $cust_pay->payinfo;
1518 } elsif ( $cust_pay->payby eq 'CHEK'
1519 && $void->info('ECHECK_void_requires_account') )
1521 ( $content{'account_number'}, $content{'routing_code'} ) =
1522 split('@', $cust_pay->payinfo);
1523 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1526 $void->content( 'action' => 'void', %content );
1527 $void->test_transaction(1)
1528 if $conf->exists('business-onlinepayment-test_transaction');
1530 if ( $void->is_success ) {
1531 my $error = $cust_pay->void($options{'reason'});
1533 # gah, even with transactions.
1534 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1535 "error voiding payment: $error";
1539 warn " void successful\n" if $DEBUG > 1;
1544 warn " void unsuccessful, trying refund\n"
1548 my $address = $self->address1;
1549 $address .= ", ". $self->address2 if $self->address2;
1551 my($payname, $payfirst, $paylast);
1552 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1553 $payname = $self->payname;
1554 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1555 or return "Illegal payname $payname";
1556 ($payfirst, $paylast) = ($1, $2);
1558 $payfirst = $self->getfield('first');
1559 $paylast = $self->getfield('last');
1560 $payname = "$payfirst $paylast";
1563 my @invoicing_list = $self->invoicing_list_emailonly;
1564 if ( $conf->exists('emailinvoiceautoalways')
1565 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1566 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1567 push @invoicing_list, $self->all_emails;
1570 my $email = ($conf->exists('business-onlinepayment-email-override'))
1571 ? $conf->config('business-onlinepayment-email-override')
1572 : $invoicing_list[0];
1574 my $payip = exists($options{'payip'})
1577 $content{customer_ip} = $payip
1581 if ( $options{method} eq 'CC' ) {
1584 $content{card_number} = $payinfo = $cust_pay->payinfo;
1585 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1586 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1587 ($content{expiration} = "$2/$1"); # where available
1589 $content{card_number} = $payinfo = $self->payinfo;
1590 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1591 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1592 $content{expiration} = "$2/$1";
1595 } elsif ( $options{method} eq 'ECHECK' ) {
1598 $payinfo = $cust_pay->payinfo;
1600 $payinfo = $self->payinfo;
1602 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1603 $content{bank_name} = $self->payname;
1604 $content{account_type} = 'CHECKING';
1605 $content{account_name} = $payname;
1606 $content{customer_org} = $self->company ? 'B' : 'I';
1607 $content{customer_ssn} = $self->ss;
1608 } elsif ( $options{method} eq 'LEC' ) {
1609 $content{phone} = $payinfo = $self->payinfo;
1613 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1614 my %sub_content = $refund->content(
1615 'action' => 'credit',
1616 'customer_id' => $self->custnum,
1617 'last_name' => $paylast,
1618 'first_name' => $payfirst,
1620 'address' => $address,
1621 'city' => $self->city,
1622 'state' => $self->state,
1623 'zip' => $self->zip,
1624 'country' => $self->country,
1626 'phone' => $self->daytime || $self->night,
1629 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1631 $refund->test_transaction(1)
1632 if $conf->exists('business-onlinepayment-test_transaction');
1635 return "$processor error: ". $refund->error_message
1636 unless $refund->is_success();
1638 $order_number = $refund->order_number if $refund->can('order_number');
1640 # change this to just use $cust_pay->delete_cust_bill_pay?
1641 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1642 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1643 last unless @cust_bill_pay;
1644 my $cust_bill_pay = pop @cust_bill_pay;
1645 my $error = $cust_bill_pay->delete;
1649 my $cust_refund = new FS::cust_refund ( {
1650 'custnum' => $self->custnum,
1651 'paynum' => $options{'paynum'},
1652 'refund' => $amount,
1654 'payby' => $bop_method2payby{$options{method}},
1655 'payinfo' => $payinfo,
1656 'reason' => $options{'reason'} || 'card or ACH refund',
1657 'gatewaynum' => $gatewaynum, # may be null
1658 'processor' => $processor,
1659 'auth' => $refund->authorization,
1660 'order_number' => $order_number,
1662 my $error = $cust_refund->insert;
1664 $cust_refund->paynum(''); #try again with no specific paynum
1665 my $error2 = $cust_refund->insert;
1667 # gah, even with transactions.
1668 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1669 "error inserting refund ($processor): $error2".
1670 " (previously tried insert with paynum #$options{'paynum'}" .
1689 L<FS::cust_main>, L<FS::cust_main::Billing>