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 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
272 # Default invoice number if the customer has exactly one open invoice.
273 if( ! $options->{'invnum'} ) {
274 $options->{'invnum'} = '';
275 my @open = $self->open_cust_bill;
276 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
279 $options->{payname} = $self->payname unless exists( $options->{payname} );
283 my ($self, $options) = @_;
286 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
287 $content{customer_ip} = $payip if length($payip);
289 $content{invoice_number} = $options->{'invnum'}
290 if exists($options->{'invnum'}) && length($options->{'invnum'});
292 $content{email_customer} =
293 ( $conf->exists('business-onlinepayment-email_customer')
294 || $conf->exists('business-onlinepayment-email-override') );
296 my ($payname, $payfirst, $paylast);
297 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
298 ($payname = $options->{payname}) =~
299 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
300 or return "Illegal payname $payname";
301 ($payfirst, $paylast) = ($1, $2);
303 $payfirst = $self->getfield('first');
304 $paylast = $self->getfield('last');
305 $payname = "$payfirst $paylast";
308 $content{last_name} = $paylast;
309 $content{first_name} = $payfirst;
311 $content{name} = $payname;
313 $content{address} = exists($options->{'address1'})
314 ? $options->{'address1'}
316 my $address2 = exists($options->{'address2'})
317 ? $options->{'address2'}
319 $content{address} .= ", ". $address2 if length($address2);
321 $content{city} = exists($options->{city})
324 $content{state} = exists($options->{state})
327 $content{zip} = exists($options->{zip})
330 $content{country} = exists($options->{country})
331 ? $options->{country}
334 $content{phone} = $self->daytime || $self->night;
336 my $currency = $conf->exists('business-onlinepayment-currency')
337 && $conf->config('business-onlinepayment-currency');
338 $content{currency} = $currency if $currency;
343 my %bop_method2payby = (
353 confess "Can't call realtime_bop within another transaction ".
354 '($FS::UID::AutoCommit is false)'
355 unless $FS::UID::AutoCommit;
357 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
360 if (ref($_[0]) eq 'HASH') {
363 my ( $method, $amount ) = ( shift, shift );
365 $options{method} = $method;
366 $options{amount} = $amount;
371 # optional credit card surcharge
374 my $cc_surcharge = 0;
375 my $cc_surcharge_pct = 0;
376 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
377 if $conf->config('credit-card-surcharge-percentage')
378 && $options{method} eq 'CC';
380 # always add cc surcharge if called from event
381 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
382 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
383 $options{'amount'} += $cc_surcharge;
384 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
386 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
387 # payment screen), so consider the given
388 # amount as post-surcharge
389 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
392 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
393 $options{'cc_surcharge'} = $cc_surcharge;
397 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
398 warn " cc_surcharge = $cc_surcharge\n";
401 warn " $_ => $options{$_}\n" foreach keys %options;
404 return $self->fake_bop(\%options) if $options{'fake'};
406 $self->_bop_defaults(\%options);
409 # set trans_is_recur based on invnum if there is one
412 my $trans_is_recur = 0;
413 if ( $options{'invnum'} ) {
415 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
416 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
422 $cust_bill->cust_bill_pkg;
425 if grep { $_->freq ne '0' } @part_pkg;
433 my $payment_gateway = $self->_payment_gateway( \%options );
434 my $namespace = $payment_gateway->gateway_namespace;
436 eval "use $namespace";
440 # check for banned credit card/ACH
443 my $ban = FS::banned_pay->ban_search(
444 'payby' => $bop_method2payby{$options{method}},
445 'payinfo' => $options{payinfo},
447 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
450 # check for term discount validity
453 my $discount_term = $options{discount_term};
454 if ( $discount_term ) {
455 my $bill = ($self->cust_bill)[-1]
456 or return "Can't apply a term discount to an unbilled customer";
457 my $plan = FS::discount_plan->new(
459 months => $discount_term
460 ) or return "No discount available for term '$discount_term'";
462 if ( $plan->discounted_total != $options{amount} ) {
463 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
471 my $bop_content = $self->_bop_content(\%options);
472 return $bop_content unless ref($bop_content);
474 my @invoicing_list = $self->invoicing_list_emailonly;
475 if ( $conf->exists('emailinvoiceautoalways')
476 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
477 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
478 push @invoicing_list, $self->all_emails;
481 my $email = ($conf->exists('business-onlinepayment-email-override'))
482 ? $conf->config('business-onlinepayment-email-override')
483 : $invoicing_list[0];
488 if ( $namespace eq 'Business::OnlinePayment' ) {
490 if ( $options{method} eq 'CC' ) {
492 $content{card_number} = $options{payinfo};
493 $paydate = exists($options{'paydate'})
494 ? $options{'paydate'}
496 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
497 $content{expiration} = "$2/$1";
499 my $paycvv = exists($options{'paycvv'})
502 $content{cvv2} = $paycvv
505 my $paystart_month = exists($options{'paystart_month'})
506 ? $options{'paystart_month'}
507 : $self->paystart_month;
509 my $paystart_year = exists($options{'paystart_year'})
510 ? $options{'paystart_year'}
511 : $self->paystart_year;
513 $content{card_start} = "$paystart_month/$paystart_year"
514 if $paystart_month && $paystart_year;
516 my $payissue = exists($options{'payissue'})
517 ? $options{'payissue'}
519 $content{issue_number} = $payissue if $payissue;
521 if ( $self->_bop_recurring_billing(
522 'payinfo' => $options{'payinfo'},
523 'trans_is_recur' => $trans_is_recur,
527 $content{recurring_billing} = 'YES';
528 $content{acct_code} = 'rebill'
529 if $conf->exists('credit_card-recurring_billing_acct_code');
532 } elsif ( $options{method} eq 'ECHECK' ){
534 ( $content{account_number}, $content{routing_code} ) =
535 split('@', $options{payinfo});
536 $content{bank_name} = $options{payname};
537 $content{bank_state} = exists($options{'paystate'})
538 ? $options{'paystate'}
539 : $self->getfield('paystate');
540 $content{account_type}=
541 (exists($options{'paytype'}) && $options{'paytype'})
542 ? uc($options{'paytype'})
543 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
545 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
546 $content{account_name} = $self->company;
548 $content{account_name} = $self->getfield('first'). ' '.
549 $self->getfield('last');
552 $content{customer_org} = $self->company ? 'B' : 'I';
553 $content{state_id} = exists($options{'stateid'})
554 ? $options{'stateid'}
555 : $self->getfield('stateid');
556 $content{state_id_state} = exists($options{'stateid_state'})
557 ? $options{'stateid_state'}
558 : $self->getfield('stateid_state');
559 $content{customer_ssn} = exists($options{'ss'})
563 } elsif ( $options{method} eq 'LEC' ) {
564 $content{phone} = $options{payinfo};
566 die "unknown method ". $options{method};
569 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
572 die "unknown namespace $namespace";
579 my $balance = exists( $options{'balance'} )
580 ? $options{'balance'}
583 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
584 $self->select_for_update; #mutex ... just until we get our pending record in
585 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
587 #the checks here are intended to catch concurrent payments
588 #double-form-submission prevention is taken care of in cust_pay_pending::check
591 return "The customer's balance has changed; $options{method} transaction aborted."
592 if $self->balance < $balance;
594 #also check and make sure there aren't *other* pending payments for this cust
596 my @pending = qsearch('cust_pay_pending', {
597 'custnum' => $self->custnum,
598 'status' => { op=>'!=', value=>'done' }
601 #for third-party payments only, remove pending payments if they're in the
602 #'thirdparty' (waiting for customer action) state.
603 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
604 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
605 my $error = $_->delete;
606 warn "error deleting unfinished third-party payment ".
607 $_->paypendingnum . ": $error\n"
610 @pending = grep { $_->status ne 'thirdparty' } @pending;
613 return "A payment is already being processed for this customer (".
614 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
615 "); $options{method} transaction aborted."
618 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
620 my $cust_pay_pending = new FS::cust_pay_pending {
621 'custnum' => $self->custnum,
622 'paid' => $options{amount},
624 'payby' => $bop_method2payby{$options{method}},
625 'payinfo' => $options{payinfo},
626 'paymask' => $options{paymask},
627 'paydate' => $paydate,
628 'recurring_billing' => $content{recurring_billing},
629 'pkgnum' => $options{'pkgnum'},
631 'gatewaynum' => $payment_gateway->gatewaynum || '',
632 'session_id' => $options{session_id} || '',
633 'jobnum' => $options{depend_jobnum} || '',
635 $cust_pay_pending->payunique( $options{payunique} )
636 if defined($options{payunique}) && length($options{payunique});
638 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
640 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
641 return $cpp_new_err if $cpp_new_err;
643 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
645 warn Dumper($cust_pay_pending) if $DEBUG > 2;
647 my( $action1, $action2 ) =
648 split( /\s*\,\s*/, $payment_gateway->gateway_action );
650 my $transaction = new $namespace( $payment_gateway->gateway_module,
651 $self->_bop_options(\%options),
654 $transaction->content(
655 'type' => $options{method},
656 $self->_bop_auth(\%options),
657 'action' => $action1,
658 'description' => $options{'description'},
659 'amount' => $options{amount},
660 #'invoice_number' => $options{'invnum'},
661 'customer_id' => $self->custnum,
663 'reference' => $cust_pay_pending->paypendingnum, #for now
664 'callback_url' => $payment_gateway->gateway_callback_url,
665 'cancel_url' => $payment_gateway->gateway_cancel_url,
670 $cust_pay_pending->status('pending');
671 my $cpp_pending_err = $cust_pay_pending->replace;
672 return $cpp_pending_err if $cpp_pending_err;
674 warn Dumper($transaction) if $DEBUG > 2;
676 unless ( $BOP_TESTING ) {
677 $transaction->test_transaction(1)
678 if $conf->exists('business-onlinepayment-test_transaction');
679 $transaction->submit();
681 if ( $BOP_TESTING_SUCCESS ) {
682 $transaction->is_success(1);
683 $transaction->authorization('fake auth');
685 $transaction->is_success(0);
686 $transaction->error_message('fake failure');
690 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
692 $cust_pay_pending->status('thirdparty');
693 my $cpp_err = $cust_pay_pending->replace;
694 return { error => $cpp_err } if $cpp_err;
695 return { reference => $cust_pay_pending->paypendingnum,
696 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
698 } elsif ( $transaction->is_success() && $action2 ) {
700 $cust_pay_pending->status('authorized');
701 my $cpp_authorized_err = $cust_pay_pending->replace;
702 return $cpp_authorized_err if $cpp_authorized_err;
704 my $auth = $transaction->authorization;
705 my $ordernum = $transaction->can('order_number')
706 ? $transaction->order_number
710 new Business::OnlinePayment( $payment_gateway->gateway_module,
711 $self->_bop_options(\%options),
716 type => $options{method},
718 $self->_bop_auth(\%options),
719 order_number => $ordernum,
720 amount => $options{amount},
721 authorization => $auth,
722 description => $options{'description'},
725 foreach my $field (qw( authorization_source_code returned_ACI
726 transaction_identifier validation_code
727 transaction_sequence_num local_transaction_date
728 local_transaction_time AVS_result_code )) {
729 $capture{$field} = $transaction->$field() if $transaction->can($field);
732 $capture->content( %capture );
734 $capture->test_transaction(1)
735 if $conf->exists('business-onlinepayment-test_transaction');
738 unless ( $capture->is_success ) {
739 my $e = "Authorization successful but capture failed, custnum #".
740 $self->custnum. ': '. $capture->result_code.
741 ": ". $capture->error_message;
749 # remove paycvv after initial transaction
752 #false laziness w/misc/process/payment.cgi - check both to make sure working
754 if ( length($self->paycvv)
755 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
757 my $error = $self->remove_cvv;
759 warn "WARNING: error removing cvv: $error\n";
768 if ( $transaction->can('card_token') && $transaction->card_token ) {
770 if ( $options{'payinfo'} eq $self->payinfo ) {
771 $self->payinfo($transaction->card_token);
772 my $error = $self->replace;
774 warn "WARNING: error storing token: $error, but proceeding anyway\n";
784 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
796 if (ref($_[0]) eq 'HASH') {
799 my ( $method, $amount ) = ( shift, shift );
801 $options{method} = $method;
802 $options{amount} = $amount;
805 if ( $options{'fake_failure'} ) {
806 return "Error: No error; test failure requested with fake_failure";
809 my $cust_pay = new FS::cust_pay ( {
810 'custnum' => $self->custnum,
811 'invnum' => $options{'invnum'},
812 'paid' => $options{amount},
814 'payby' => $bop_method2payby{$options{method}},
815 #'payinfo' => $payinfo,
816 'payinfo' => '4111111111111111',
817 #'paydate' => $paydate,
818 'paydate' => '2012-05-01',
819 'processor' => 'FakeProcessor',
821 'order_number' => '32',
823 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
826 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
827 warn " $_ => $options{$_}\n" foreach keys %options;
830 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
833 $cust_pay->invnum(''); #try again with no specific invnum
834 my $error2 = $cust_pay->insert( $options{'manual'} ?
835 ( 'manual' => 1 ) : ()
838 # gah, even with transactions.
839 my $e = 'WARNING: Card/ACH debited but database not updated - '.
840 "error inserting (fake!) payment: $error2".
841 " (previously tried insert with invnum #$options{'invnum'}" .
848 if ( $options{'paynum_ref'} ) {
849 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
857 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
859 # Wraps up processing of a realtime credit card, ACH (electronic check) or
860 # phone bill transaction.
862 sub _realtime_bop_result {
863 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
865 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
868 warn "$me _realtime_bop_result: pending transaction ".
869 $cust_pay_pending->paypendingnum. "\n";
870 warn " $_ => $options{$_}\n" foreach keys %options;
873 my $payment_gateway = $options{payment_gateway}
874 or return "no payment gateway in arguments to _realtime_bop_result";
876 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
877 my $cpp_captured_err = $cust_pay_pending->replace;
878 return $cpp_captured_err if $cpp_captured_err;
880 if ( $transaction->is_success() ) {
882 my $order_number = $transaction->order_number
883 if $transaction->can('order_number');
885 my $cust_pay = new FS::cust_pay ( {
886 'custnum' => $self->custnum,
887 'invnum' => $options{'invnum'},
888 'paid' => $cust_pay_pending->paid,
890 'payby' => $cust_pay_pending->payby,
891 'payinfo' => $options{'payinfo'},
892 'paymask' => $options{'paymask'},
893 'paydate' => $cust_pay_pending->paydate,
894 'pkgnum' => $cust_pay_pending->pkgnum,
895 'discount_term' => $options{'discount_term'},
896 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
897 'processor' => $payment_gateway->gateway_module,
898 'auth' => $transaction->authorization,
899 'order_number' => $order_number || '',
902 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
903 $cust_pay->payunique( $options{payunique} )
904 if defined($options{payunique}) && length($options{payunique});
906 my $oldAutoCommit = $FS::UID::AutoCommit;
907 local $FS::UID::AutoCommit = 0;
910 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
912 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
915 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
916 $cust_pay->invnum(''); #try again with no specific invnum
917 $cust_pay->paynum('');
918 my $error2 = $cust_pay->insert( $options{'manual'} ?
919 ( 'manual' => 1 ) : ()
922 # gah. but at least we have a record of the state we had to abort in
923 # from cust_pay_pending now.
924 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
925 my $e = "WARNING: $options{method} captured but payment not recorded -".
926 " error inserting payment (". $payment_gateway->gateway_module.
928 " (previously tried insert with invnum #$options{'invnum'}" .
929 ": $error ) - pending payment saved as paypendingnum ".
930 $cust_pay_pending->paypendingnum. "\n";
936 my $jobnum = $cust_pay_pending->jobnum;
938 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
940 unless ( $placeholder ) {
941 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
942 my $e = "WARNING: $options{method} captured but job $jobnum not ".
943 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
948 $error = $placeholder->delete;
951 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
952 my $e = "WARNING: $options{method} captured but could not delete ".
953 "job $jobnum for paypendingnum ".
954 $cust_pay_pending->paypendingnum. ": $error\n";
959 $cust_pay_pending->set('jobnum','');
963 if ( $options{'paynum_ref'} ) {
964 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
967 $cust_pay_pending->status('done');
968 $cust_pay_pending->statustext('captured');
969 $cust_pay_pending->paynum($cust_pay->paynum);
970 my $cpp_done_err = $cust_pay_pending->replace;
972 if ( $cpp_done_err ) {
974 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
975 my $e = "WARNING: $options{method} captured but payment not recorded - ".
976 "error updating status for paypendingnum ".
977 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
983 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
985 if ( $options{'apply'} ) {
986 my $apply_error = $self->apply_payments_and_credits;
987 if ( $apply_error ) {
988 warn "WARNING: error applying payment: $apply_error\n";
989 #but we still should return no error cause the payment otherwise went
994 # have a CC surcharge portion --> one-time charge
995 if ( $options{'cc_surcharge'} > 0 ) {
996 # XXX: this whole block needs to be in a transaction?
999 $invnum = $options{'invnum'} if $options{'invnum'};
1000 unless ( $invnum ) { # probably from a payment screen
1001 # do we have any open invoices? pick earliest
1002 # uses the fact that cust_main->cust_bill sorts by date ascending
1003 my @open = $self->open_cust_bill;
1004 $invnum = $open[0]->invnum if scalar(@open);
1007 unless ( $invnum ) { # still nothing? pick last closed invoice
1008 # again uses fact that cust_main->cust_bill sorts by date ascending
1009 my @closed = $self->cust_bill;
1010 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1013 unless ( $invnum ) {
1014 # XXX: unlikely case - pre-paying before any invoices generated
1015 # what it should do is create a new invoice and pick it
1016 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1021 my $charge_error = $self->charge({
1022 'amount' => $options{'cc_surcharge'},
1023 'pkg' => 'Credit Card Surcharge',
1025 'cust_pkg_ref' => \$cust_pkg,
1028 warn 'Unable to add CC surcharge cust_pkg';
1032 $cust_pkg->setup(time);
1033 my $cp_error = $cust_pkg->replace;
1035 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1039 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1040 unless ( $cust_bill ) {
1041 warn "race condition + invoice deletion just happened";
1046 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1048 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1052 return ''; #no error
1058 my $perror = $transaction->error_message;
1059 #$payment_gateway->gateway_module. " error: ".
1060 # removed for conciseness
1062 my $jobnum = $cust_pay_pending->jobnum;
1064 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1066 if ( $placeholder ) {
1067 my $error = $placeholder->depended_delete;
1068 $error ||= $placeholder->delete;
1069 $cust_pay_pending->set('jobnum','');
1070 warn "error removing provisioning jobs after declined paypendingnum ".
1071 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1073 my $e = "error finding job $jobnum for declined paypendingnum ".
1074 $cust_pay_pending->paypendingnum. "\n";
1080 unless ( $transaction->error_message ) {
1083 if ( $transaction->can('response_page') ) {
1085 'page' => ( $transaction->can('response_page')
1086 ? $transaction->response_page
1089 'code' => ( $transaction->can('response_code')
1090 ? $transaction->response_code
1093 'headers' => ( $transaction->can('response_headers')
1094 ? $transaction->response_headers
1100 "No additional debugging information available for ".
1101 $payment_gateway->gateway_module;
1104 $perror .= "No error_message returned from ".
1105 $payment_gateway->gateway_module. " -- ".
1106 ( ref($t_response) ? Dumper($t_response) : $t_response );
1110 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1111 && $conf->exists('emaildecline', $self->agentnum)
1112 && grep { $_ ne 'POST' } $self->invoicing_list
1113 && ! grep { $transaction->error_message =~ /$_/ }
1114 $conf->config('emaildecline-exclude', $self->agentnum)
1117 # Send a decline alert to the customer.
1118 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1121 # include the raw error message in the transaction state
1122 $cust_pay_pending->setfield('error', $transaction->error_message);
1123 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1124 $error = $msg_template->send( 'cust_main' => $self,
1125 'object' => $cust_pay_pending );
1129 $perror .= " (also received error sending decline notification: $error)"
1134 $cust_pay_pending->status('done');
1135 $cust_pay_pending->statustext($perror);
1136 #'declined:': no, that's failure_status
1137 if ( $transaction->can('failure_status') ) {
1138 $cust_pay_pending->failure_status( $transaction->failure_status );
1140 my $cpp_done_err = $cust_pay_pending->replace;
1141 if ( $cpp_done_err ) {
1142 my $e = "WARNING: $options{method} declined but pending payment not ".
1143 "resolved - error updating status for paypendingnum ".
1144 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1146 $perror = "$e ($perror)";
1154 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1156 Verifies successful third party processing of a realtime credit card,
1157 ACH (electronic check) or phone bill transaction via a
1158 Business::OnlineThirdPartyPayment realtime gateway. See
1159 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1161 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1163 The additional options I<payname>, I<city>, I<state>,
1164 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1165 if set, will override the value from the customer record.
1167 I<description> is a free-text field passed to the gateway. It defaults to
1168 "Internet services".
1170 If an I<invnum> is specified, this payment (if successful) is applied to the
1171 specified invoice. If you don't specify an I<invnum> you might want to
1172 call the B<apply_payments> method.
1174 I<quiet> can be set true to surpress email decline notices.
1176 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1177 resulting paynum, if any.
1179 I<payunique> is a unique identifier for this payment.
1181 Returns a hashref containing elements bill_error (which will be undefined
1182 upon success) and session_id of any associated session.
1186 sub realtime_botpp_capture {
1187 my( $self, $cust_pay_pending, %options ) = @_;
1189 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1192 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1193 warn " $_ => $options{$_}\n" foreach keys %options;
1196 eval "use Business::OnlineThirdPartyPayment";
1200 # select the gateway
1203 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1205 my $payment_gateway;
1206 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1207 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1208 { gatewaynum => $gatewaynum }
1210 : $self->agent->payment_gateway( 'method' => $method,
1211 # 'invnum' => $cust_pay_pending->invnum,
1212 # 'payinfo' => $cust_pay_pending->payinfo,
1215 $options{payment_gateway} = $payment_gateway; # for the helper subs
1221 my @invoicing_list = $self->invoicing_list_emailonly;
1222 if ( $conf->exists('emailinvoiceautoalways')
1223 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1224 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1225 push @invoicing_list, $self->all_emails;
1228 my $email = ($conf->exists('business-onlinepayment-email-override'))
1229 ? $conf->config('business-onlinepayment-email-override')
1230 : $invoicing_list[0];
1234 $content{email_customer} =
1235 ( $conf->exists('business-onlinepayment-email_customer')
1236 || $conf->exists('business-onlinepayment-email-override') );
1239 # run transaction(s)
1243 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1244 $self->_bop_options(\%options),
1247 $transaction->reference({ %options });
1249 $transaction->content(
1251 $self->_bop_auth(\%options),
1252 'action' => 'Post Authorization',
1253 'description' => $options{'description'},
1254 'amount' => $cust_pay_pending->paid,
1255 #'invoice_number' => $options{'invnum'},
1256 'customer_id' => $self->custnum,
1257 'reference' => $cust_pay_pending->paypendingnum,
1259 'phone' => $self->daytime || $self->night,
1261 # plus whatever is required for bogus capture avoidance
1264 $transaction->submit();
1267 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1269 if ( $options{'apply'} ) {
1270 my $apply_error = $self->apply_payments_and_credits;
1271 if ( $apply_error ) {
1272 warn "WARNING: error applying payment: $apply_error\n";
1277 bill_error => $error,
1278 session_id => $cust_pay_pending->session_id,
1283 =item default_payment_gateway
1285 DEPRECATED -- use agent->payment_gateway
1289 sub default_payment_gateway {
1290 my( $self, $method ) = @_;
1292 die "Real-time processing not enabled\n"
1293 unless $conf->exists('business-onlinepayment');
1295 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1298 my $bop_config = 'business-onlinepayment';
1299 $bop_config .= '-ach'
1300 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1301 my ( $processor, $login, $password, $action, @bop_options ) =
1302 $conf->config($bop_config);
1303 $action ||= 'normal authorization';
1304 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1305 die "No real-time processor is enabled - ".
1306 "did you set the business-onlinepayment configuration value?\n"
1309 ( $processor, $login, $password, $action, @bop_options )
1312 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1314 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1315 via a Business::OnlinePayment realtime gateway. See
1316 L<http://420.am/business-onlinepayment> for supported gateways.
1318 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1320 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1322 Most gateways require a reference to an original payment transaction to refund,
1323 so you probably need to specify a I<paynum>.
1325 I<amount> defaults to the original amount of the payment if not specified.
1327 I<reason> specifies a reason for the refund.
1329 I<paydate> specifies the expiration date for a credit card overriding the
1330 value from the customer record or the payment record. Specified as yyyy-mm-dd
1332 Implementation note: If I<amount> is unspecified or equal to the amount of the
1333 orignal payment, first an attempt is made to "void" the transaction via
1334 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1335 the normal attempt is made to "refund" ("credit") the transaction via the
1336 gateway is attempted. No attempt to "void" the transaction is made if the
1337 gateway has introspection data and doesn't support void.
1339 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1340 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1341 #if set, will override the value from the customer record.
1343 #If an I<invnum> is specified, this payment (if successful) is applied to the
1344 #specified invoice. If you don't specify an I<invnum> you might want to
1345 #call the B<apply_payments> method.
1349 #some false laziness w/realtime_bop, not enough to make it worth merging
1350 #but some useful small subs should be pulled out
1351 sub realtime_refund_bop {
1354 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1357 if (ref($_[0]) eq 'HASH') {
1358 %options = %{$_[0]};
1362 $options{method} = $method;
1366 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1367 warn " $_ => $options{$_}\n" foreach keys %options;
1373 # look up the original payment and optionally a gateway for that payment
1377 my $amount = $options{'amount'};
1379 my( $processor, $login, $password, @bop_options, $namespace ) ;
1380 my( $auth, $order_number ) = ( '', '', '' );
1381 my $gatewaynum = '';
1383 if ( $options{'paynum'} ) {
1385 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1386 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1387 or return "Unknown paynum $options{'paynum'}";
1388 $amount ||= $cust_pay->paid;
1390 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1391 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1393 if ( $cust_pay->get('processor') ) {
1394 ($gatewaynum, $processor, $auth, $order_number) =
1396 $cust_pay->gatewaynum,
1397 $cust_pay->processor,
1399 $cust_pay->order_number,
1402 # this payment wasn't upgraded, which probably means this won't work,
1404 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1405 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1406 $cust_pay->paybatch;
1407 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1410 if ( $gatewaynum ) { #gateway for the payment to be refunded
1412 my $payment_gateway =
1413 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1414 die "payment gateway $gatewaynum not found"
1415 unless $payment_gateway;
1417 $processor = $payment_gateway->gateway_module;
1418 $login = $payment_gateway->gateway_username;
1419 $password = $payment_gateway->gateway_password;
1420 $namespace = $payment_gateway->gateway_namespace;
1421 @bop_options = $payment_gateway->options;
1423 } else { #try the default gateway
1426 my $payment_gateway =
1427 $self->agent->payment_gateway('method' => $options{method});
1429 ( $conf_processor, $login, $password, $namespace ) =
1430 map { my $method = "gateway_$_"; $payment_gateway->$method }
1431 qw( module username password namespace );
1433 @bop_options = $payment_gateway->gatewaynum
1434 ? $payment_gateway->options
1435 : @{ $payment_gateway->get('options') };
1437 return "processor of payment $options{'paynum'} $processor does not".
1438 " match default processor $conf_processor"
1439 unless $processor eq $conf_processor;
1444 } else { # didn't specify a paynum, so look for agent gateway overrides
1445 # like a normal transaction
1447 my $payment_gateway =
1448 $self->agent->payment_gateway( 'method' => $options{method},
1449 #'payinfo' => $payinfo,
1451 my( $processor, $login, $password, $namespace ) =
1452 map { my $method = "gateway_$_"; $payment_gateway->$method }
1453 qw( module username password namespace );
1455 my @bop_options = $payment_gateway->gatewaynum
1456 ? $payment_gateway->options
1457 : @{ $payment_gateway->get('options') };
1460 return "neither amount nor paynum specified" unless $amount;
1462 eval "use $namespace";
1467 'type' => $options{method},
1469 'password' => $password,
1470 'order_number' => $order_number,
1471 'amount' => $amount,
1473 $content{authorization} = $auth
1474 if length($auth); #echeck/ACH transactions have an order # but no auth
1475 #(at least with authorize.net)
1477 my $currency = $conf->exists('business-onlinepayment-currency')
1478 && $conf->config('business-onlinepayment-currency');
1479 $content{currency} = $currency if $currency;
1481 my $disable_void_after;
1482 if ($conf->exists('disable_void_after')
1483 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1484 $disable_void_after = $1;
1487 #first try void if applicable
1488 my $void = new Business::OnlinePayment( $processor, @bop_options );
1491 if ($void->can('info')) {
1493 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1494 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1495 my %supported_actions = $void->info('supported_actions');
1497 if ( %supported_actions && $paytype
1498 && defined($supported_actions{$paytype})
1499 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1502 if ( $cust_pay && $cust_pay->paid == $amount
1504 ( not defined($disable_void_after) )
1505 || ( time < ($cust_pay->_date + $disable_void_after ) )
1509 warn " attempting void\n" if $DEBUG > 1;
1510 if ( $void->can('info') ) {
1511 if ( $cust_pay->payby eq 'CARD'
1512 && $void->info('CC_void_requires_card') )
1514 $content{'card_number'} = $cust_pay->payinfo;
1515 } elsif ( $cust_pay->payby eq 'CHEK'
1516 && $void->info('ECHECK_void_requires_account') )
1518 ( $content{'account_number'}, $content{'routing_code'} ) =
1519 split('@', $cust_pay->payinfo);
1520 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1523 $void->content( 'action' => 'void', %content );
1524 $void->test_transaction(1)
1525 if $conf->exists('business-onlinepayment-test_transaction');
1527 if ( $void->is_success ) {
1528 my $error = $cust_pay->void($options{'reason'});
1530 # gah, even with transactions.
1531 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1532 "error voiding payment: $error";
1536 warn " void successful\n" if $DEBUG > 1;
1541 warn " void unsuccessful, trying refund\n"
1545 my $address = $self->address1;
1546 $address .= ", ". $self->address2 if $self->address2;
1548 my($payname, $payfirst, $paylast);
1549 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1550 $payname = $self->payname;
1551 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1552 or return "Illegal payname $payname";
1553 ($payfirst, $paylast) = ($1, $2);
1555 $payfirst = $self->getfield('first');
1556 $paylast = $self->getfield('last');
1557 $payname = "$payfirst $paylast";
1560 my @invoicing_list = $self->invoicing_list_emailonly;
1561 if ( $conf->exists('emailinvoiceautoalways')
1562 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1563 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1564 push @invoicing_list, $self->all_emails;
1567 my $email = ($conf->exists('business-onlinepayment-email-override'))
1568 ? $conf->config('business-onlinepayment-email-override')
1569 : $invoicing_list[0];
1571 my $payip = exists($options{'payip'})
1574 $content{customer_ip} = $payip
1578 if ( $options{method} eq 'CC' ) {
1581 $content{card_number} = $payinfo = $cust_pay->payinfo;
1582 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1583 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1584 ($content{expiration} = "$2/$1"); # where available
1586 $content{card_number} = $payinfo = $self->payinfo;
1587 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1588 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1589 $content{expiration} = "$2/$1";
1592 } elsif ( $options{method} eq 'ECHECK' ) {
1595 $payinfo = $cust_pay->payinfo;
1597 $payinfo = $self->payinfo;
1599 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1600 $content{bank_name} = $self->payname;
1601 $content{account_type} = 'CHECKING';
1602 $content{account_name} = $payname;
1603 $content{customer_org} = $self->company ? 'B' : 'I';
1604 $content{customer_ssn} = $self->ss;
1605 } elsif ( $options{method} eq 'LEC' ) {
1606 $content{phone} = $payinfo = $self->payinfo;
1610 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1611 my %sub_content = $refund->content(
1612 'action' => 'credit',
1613 'customer_id' => $self->custnum,
1614 'last_name' => $paylast,
1615 'first_name' => $payfirst,
1617 'address' => $address,
1618 'city' => $self->city,
1619 'state' => $self->state,
1620 'zip' => $self->zip,
1621 'country' => $self->country,
1623 'phone' => $self->daytime || $self->night,
1626 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1628 $refund->test_transaction(1)
1629 if $conf->exists('business-onlinepayment-test_transaction');
1632 return "$processor error: ". $refund->error_message
1633 unless $refund->is_success();
1635 $order_number = $refund->order_number if $refund->can('order_number');
1637 # change this to just use $cust_pay->delete_cust_bill_pay?
1638 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1639 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1640 last unless @cust_bill_pay;
1641 my $cust_bill_pay = pop @cust_bill_pay;
1642 my $error = $cust_bill_pay->delete;
1646 my $cust_refund = new FS::cust_refund ( {
1647 'custnum' => $self->custnum,
1648 'paynum' => $options{'paynum'},
1649 'refund' => $amount,
1651 'payby' => $bop_method2payby{$options{method}},
1652 'payinfo' => $payinfo,
1653 'reason' => $options{'reason'} || 'card or ACH refund',
1654 'gatewaynum' => $gatewaynum, # may be null
1655 'processor' => $processor,
1656 'auth' => $refund->authorization,
1657 'order_number' => $order_number,
1659 my $error = $cust_refund->insert;
1661 $cust_refund->paynum(''); #try again with no specific paynum
1662 my $error2 = $cust_refund->insert;
1664 # gah, even with transactions.
1665 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1666 "error inserting refund ($processor): $error2".
1667 " (previously tried insert with paynum #$options{'paynum'}" .
1686 L<FS::cust_main>, L<FS::cust_main::Billing>