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;
17 $realtime_bop_decline_quiet = 0;
19 # 1 is mostly method/subroutine entry and options
20 # 2 traces progress of some operations
21 # 3 is even more information including possibly sensitive data
23 $me = '[FS::cust_main::Billing_Realtime]';
26 our $BOP_TESTING_SUCCESS = 1;
28 install_callback FS::UID sub {
30 #yes, need it for stuff below (prolly should be cached)
35 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
41 These methods are available on FS::cust_main objects.
47 =item realtime_cust_payby
51 sub realtime_cust_payby {
52 my( $self, %options ) = @_;
54 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
56 $options{amount} = $self->balance unless exists( $options{amount} );
58 my @cust_payby = qsearch({
59 'table' => 'cust_payby',
60 'hashref' => { 'custnum' => $self->custnum, },
61 'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
62 'order_by' => 'ORDER BY weight ASC',
66 foreach my $cust_payby (@cust_payby) {
67 $error = $cust_payby->realtime_bop( %options, );
71 #XXX what about the earlier errors?
77 =item realtime_collect [ OPTION => VALUE ... ]
79 Attempt to collect the customer's current balance with a realtime credit
80 card, electronic check, or phone bill transaction (see realtime_bop() below).
82 Returns the result of realtime_bop(): nothing, an error message, or a
83 hashref of state information for a third-party transaction.
85 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
87 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
88 then it is deduced from the customer record.
90 If no I<amount> is specified, then the customer balance is used.
92 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
93 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
94 if set, will override the value from the customer record.
96 I<description> is a free-text field passed to the gateway. It defaults to
97 the value defined by the business-onlinepayment-description configuration
98 option, or "Internet services" if that is unset.
100 If an I<invnum> is specified, this payment (if successful) is applied to the
103 I<apply> will automatically apply a resulting payment.
105 I<quiet> can be set true to suppress email decline notices.
107 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
108 resulting paynum, if any.
110 I<payunique> is a unique identifier for this payment.
112 I<session_id> is a session identifier associated with this payment.
114 I<depend_jobnum> allows payment capture to unlock export jobs
118 sub realtime_collect {
119 my( $self, %options ) = @_;
121 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
124 warn "$me realtime_collect:\n";
125 warn " $_ => $options{$_}\n" foreach keys %options;
128 $options{amount} = $self->balance unless exists( $options{amount} );
129 $options{method} = FS::payby->payby2bop($self->payby)
130 unless exists( $options{method} );
132 return $self->realtime_bop({%options});
136 =item realtime_bop { [ ARG => VALUE ... ] }
138 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
139 via a Business::OnlinePayment realtime gateway. See
140 L<http://420.am/business-onlinepayment> for supported gateways.
142 Required arguments in the hashref are I<method>, and I<amount>
144 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
146 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
148 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
149 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
150 if set, will override the value from the customer record.
152 I<description> is a free-text field passed to the gateway. It defaults to
153 the value defined by the business-onlinepayment-description configuration
154 option, or "Internet services" if that is unset.
156 If an I<invnum> is specified, this payment (if successful) is applied to the
157 specified invoice. If the customer has exactly one open invoice, that
158 invoice number will be assumed. If you don't specify an I<invnum> you might
159 want to call the B<apply_payments> method or set the I<apply> option.
161 I<apply> can be set to true to apply a resulting payment.
163 I<quiet> can be set true to surpress email decline notices.
165 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
166 resulting paynum, if any.
168 I<payunique> is a unique identifier for this payment.
170 I<session_id> is a session identifier associated with this payment.
172 I<depend_jobnum> allows payment capture to unlock export jobs
174 I<discount_term> attempts to take a discount by prepaying for discount_term.
175 The payment will fail if I<amount> is incorrect for this discount term.
177 A direct (Business::OnlinePayment) transaction will return nothing on success,
178 or an error message on failure.
180 A third-party transaction will return a hashref containing:
182 - popup_url: the URL to which a browser should be redirected to complete
184 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
185 - reference: a reference ID for the transaction, to show the customer.
187 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
191 # some helper routines
192 sub _bop_recurring_billing {
193 my( $self, %opt ) = @_;
195 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
197 if ( defined($method) && $method eq 'transaction_is_recur' ) {
199 return 1 if $opt{'trans_is_recur'};
203 # return 1 if the payinfo has been used for another payment
204 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
212 sub _payment_gateway {
213 my ($self, $options) = @_;
215 if ( $options->{'selfservice'} ) {
216 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
218 return $options->{payment_gateway} ||=
219 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
223 if ( $options->{'fake_gatewaynum'} ) {
224 $options->{payment_gateway} =
225 qsearchs('payment_gateway',
226 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
230 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
231 unless exists($options->{payment_gateway});
233 $options->{payment_gateway};
237 my ($self, $options) = @_;
240 'login' => $options->{payment_gateway}->gateway_username,
241 'password' => $options->{payment_gateway}->gateway_password,
246 my ($self, $options) = @_;
248 $options->{payment_gateway}->gatewaynum
249 ? $options->{payment_gateway}->options
250 : @{ $options->{payment_gateway}->get('options') };
255 my ($self, $options) = @_;
257 unless ( $options->{'description'} ) {
258 if ( $conf->exists('business-onlinepayment-description') ) {
259 my $dtempl = $conf->config('business-onlinepayment-description');
261 my $agent = $self->agent->agent;
263 $options->{'description'} = eval qq("$dtempl");
265 $options->{'description'} = 'Internet services';
269 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
271 # Default invoice number if the customer has exactly one open invoice.
272 if( ! $options->{'invnum'} ) {
273 $options->{'invnum'} = '';
274 my @open = $self->open_cust_bill;
275 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
278 $options->{payname} = $self->payname unless exists( $options->{payname} );
282 my ($self, $options) = @_;
285 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
286 $content{customer_ip} = $payip if length($payip);
288 $content{invoice_number} = $options->{'invnum'}
289 if exists($options->{'invnum'}) && length($options->{'invnum'});
291 $content{email_customer} =
292 ( $conf->exists('business-onlinepayment-email_customer')
293 || $conf->exists('business-onlinepayment-email-override') );
295 my ($payname, $payfirst, $paylast);
296 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
297 ($payname = $options->{payname}) =~
298 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
299 or return "Illegal payname $payname";
300 ($payfirst, $paylast) = ($1, $2);
302 $payfirst = $self->getfield('first');
303 $paylast = $self->getfield('last');
304 $payname = "$payfirst $paylast";
307 $content{last_name} = $paylast;
308 $content{first_name} = $payfirst;
310 $content{name} = $payname;
312 $content{address} = exists($options->{'address1'})
313 ? $options->{'address1'}
315 my $address2 = exists($options->{'address2'})
316 ? $options->{'address2'}
318 $content{address} .= ", ". $address2 if length($address2);
320 $content{city} = exists($options->{city})
323 $content{state} = exists($options->{state})
326 $content{zip} = exists($options->{zip})
329 $content{country} = exists($options->{country})
330 ? $options->{country}
333 $content{phone} = $self->daytime || $self->night;
335 my $currency = $conf->exists('business-onlinepayment-currency')
336 && $conf->config('business-onlinepayment-currency');
337 $content{currency} = $currency if $currency;
342 my %bop_method2payby = (
352 confess "Can't call realtime_bop within another transaction ".
353 '($FS::UID::AutoCommit is false)'
354 unless $FS::UID::AutoCommit;
356 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
359 if (ref($_[0]) eq 'HASH') {
362 my ( $method, $amount ) = ( shift, shift );
364 $options{method} = $method;
365 $options{amount} = $amount;
370 # optional credit card surcharge
373 my $cc_surcharge = 0;
374 my $cc_surcharge_pct = 0;
375 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
376 if $conf->config('credit-card-surcharge-percentage')
377 && $options{method} eq 'CC';
379 # always add cc surcharge if called from event
380 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
381 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
382 $options{'amount'} += $cc_surcharge;
383 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
385 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
386 # payment screen), so consider the given
387 # amount as post-surcharge
388 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
391 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
392 $options{'cc_surcharge'} = $cc_surcharge;
396 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
397 warn " cc_surcharge = $cc_surcharge\n";
400 warn " $_ => $options{$_}\n" foreach keys %options;
403 return $self->fake_bop(\%options) if $options{'fake'};
405 $self->_bop_defaults(\%options);
408 # set trans_is_recur based on invnum if there is one
411 my $trans_is_recur = 0;
412 if ( $options{'invnum'} ) {
414 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
415 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
421 $cust_bill->cust_bill_pkg;
424 if grep { $_->freq ne '0' } @part_pkg;
432 my $payment_gateway = $self->_payment_gateway( \%options );
433 my $namespace = $payment_gateway->gateway_namespace;
435 eval "use $namespace";
439 # check for banned credit card/ACH
442 my $ban = FS::banned_pay->ban_search(
443 'payby' => $bop_method2payby{$options{method}},
444 'payinfo' => $options{payinfo},
446 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
449 # check for term discount validity
452 my $discount_term = $options{discount_term};
453 if ( $discount_term ) {
454 my $bill = ($self->cust_bill)[-1]
455 or return "Can't apply a term discount to an unbilled customer";
456 my $plan = FS::discount_plan->new(
458 months => $discount_term
459 ) or return "No discount available for term '$discount_term'";
461 if ( $plan->discounted_total != $options{amount} ) {
462 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
470 my $bop_content = $self->_bop_content(\%options);
471 return $bop_content unless ref($bop_content);
473 my @invoicing_list = $self->invoicing_list_emailonly;
474 if ( $conf->exists('emailinvoiceautoalways')
475 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
476 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
477 push @invoicing_list, $self->all_emails;
480 my $email = ($conf->exists('business-onlinepayment-email-override'))
481 ? $conf->config('business-onlinepayment-email-override')
482 : $invoicing_list[0];
487 if ( $namespace eq 'Business::OnlinePayment' ) {
489 if ( $options{method} eq 'CC' ) {
491 $content{card_number} = $options{payinfo};
492 $paydate = exists($options{'paydate'})
493 ? $options{'paydate'}
495 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
496 $content{expiration} = "$2/$1";
498 my $paycvv = exists($options{'paycvv'})
501 $content{cvv2} = $paycvv
504 my $paystart_month = exists($options{'paystart_month'})
505 ? $options{'paystart_month'}
506 : $self->paystart_month;
508 my $paystart_year = exists($options{'paystart_year'})
509 ? $options{'paystart_year'}
510 : $self->paystart_year;
512 $content{card_start} = "$paystart_month/$paystart_year"
513 if $paystart_month && $paystart_year;
515 my $payissue = exists($options{'payissue'})
516 ? $options{'payissue'}
518 $content{issue_number} = $payissue if $payissue;
520 if ( $self->_bop_recurring_billing(
521 'payinfo' => $options{'payinfo'},
522 'trans_is_recur' => $trans_is_recur,
526 $content{recurring_billing} = 'YES';
527 $content{acct_code} = 'rebill'
528 if $conf->exists('credit_card-recurring_billing_acct_code');
531 } elsif ( $options{method} eq 'ECHECK' ){
533 ( $content{account_number}, $content{routing_code} ) =
534 split('@', $options{payinfo});
535 $content{bank_name} = $options{payname};
536 $content{bank_state} = exists($options{'paystate'})
537 ? $options{'paystate'}
538 : $self->getfield('paystate');
539 $content{account_type}=
540 (exists($options{'paytype'}) && $options{'paytype'})
541 ? uc($options{'paytype'})
542 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
544 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
545 $content{account_name} = $self->company;
547 $content{account_name} = $self->getfield('first'). ' '.
548 $self->getfield('last');
551 $content{customer_org} = $self->company ? 'B' : 'I';
552 $content{state_id} = exists($options{'stateid'})
553 ? $options{'stateid'}
554 : $self->getfield('stateid');
555 $content{state_id_state} = exists($options{'stateid_state'})
556 ? $options{'stateid_state'}
557 : $self->getfield('stateid_state');
558 $content{customer_ssn} = exists($options{'ss'})
562 } elsif ( $options{method} eq 'LEC' ) {
563 $content{phone} = $options{payinfo};
565 die "unknown method ". $options{method};
568 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
571 die "unknown namespace $namespace";
578 my $balance = exists( $options{'balance'} )
579 ? $options{'balance'}
582 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
583 $self->select_for_update; #mutex ... just until we get our pending record in
584 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
586 #the checks here are intended to catch concurrent payments
587 #double-form-submission prevention is taken care of in cust_pay_pending::check
590 return "The customer's balance has changed; $options{method} transaction aborted."
591 if $self->balance < $balance;
593 #also check and make sure there aren't *other* pending payments for this cust
595 my @pending = qsearch('cust_pay_pending', {
596 'custnum' => $self->custnum,
597 'status' => { op=>'!=', value=>'done' }
600 #for third-party payments only, remove pending payments if they're in the
601 #'thirdparty' (waiting for customer action) state.
602 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
603 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
604 my $error = $_->delete;
605 warn "error deleting unfinished third-party payment ".
606 $_->paypendingnum . ": $error\n"
609 @pending = grep { $_->status ne 'thirdparty' } @pending;
612 return "A payment is already being processed for this customer (".
613 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
614 "); $options{method} transaction aborted."
617 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
619 my $cust_pay_pending = new FS::cust_pay_pending {
620 'custnum' => $self->custnum,
621 'paid' => $options{amount},
623 'payby' => $bop_method2payby{$options{method}},
624 'payinfo' => $options{payinfo},
625 'paymask' => $options{paymask},
626 'paydate' => $paydate,
627 'recurring_billing' => $content{recurring_billing},
628 'pkgnum' => $options{'pkgnum'},
630 'gatewaynum' => $payment_gateway->gatewaynum || '',
631 'session_id' => $options{session_id} || '',
632 'jobnum' => $options{depend_jobnum} || '',
634 $cust_pay_pending->payunique( $options{payunique} )
635 if defined($options{payunique}) && length($options{payunique});
637 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
639 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
640 return $cpp_new_err if $cpp_new_err;
642 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
644 warn Dumper($cust_pay_pending) if $DEBUG > 2;
646 my( $action1, $action2 ) =
647 split( /\s*\,\s*/, $payment_gateway->gateway_action );
649 my $transaction = new $namespace( $payment_gateway->gateway_module,
650 $self->_bop_options(\%options),
653 $transaction->content(
654 'type' => $options{method},
655 $self->_bop_auth(\%options),
656 'action' => $action1,
657 'description' => $options{'description'},
658 'amount' => $options{amount},
659 #'invoice_number' => $options{'invnum'},
660 'customer_id' => $self->custnum,
662 'reference' => $cust_pay_pending->paypendingnum, #for now
663 'callback_url' => $payment_gateway->gateway_callback_url,
664 'cancel_url' => $payment_gateway->gateway_cancel_url,
669 $cust_pay_pending->status('pending');
670 my $cpp_pending_err = $cust_pay_pending->replace;
671 return $cpp_pending_err if $cpp_pending_err;
673 warn Dumper($transaction) if $DEBUG > 2;
675 unless ( $BOP_TESTING ) {
676 $transaction->test_transaction(1)
677 if $conf->exists('business-onlinepayment-test_transaction');
678 $transaction->submit();
680 if ( $BOP_TESTING_SUCCESS ) {
681 $transaction->is_success(1);
682 $transaction->authorization('fake auth');
684 $transaction->is_success(0);
685 $transaction->error_message('fake failure');
689 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
691 $cust_pay_pending->status('thirdparty');
692 my $cpp_err = $cust_pay_pending->replace;
693 return { error => $cpp_err } if $cpp_err;
694 return { reference => $cust_pay_pending->paypendingnum,
695 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
697 } elsif ( $transaction->is_success() && $action2 ) {
699 $cust_pay_pending->status('authorized');
700 my $cpp_authorized_err = $cust_pay_pending->replace;
701 return $cpp_authorized_err if $cpp_authorized_err;
703 my $auth = $transaction->authorization;
704 my $ordernum = $transaction->can('order_number')
705 ? $transaction->order_number
709 new Business::OnlinePayment( $payment_gateway->gateway_module,
710 $self->_bop_options(\%options),
715 type => $options{method},
717 $self->_bop_auth(\%options),
718 order_number => $ordernum,
719 amount => $options{amount},
720 authorization => $auth,
721 description => $options{'description'},
724 foreach my $field (qw( authorization_source_code returned_ACI
725 transaction_identifier validation_code
726 transaction_sequence_num local_transaction_date
727 local_transaction_time AVS_result_code )) {
728 $capture{$field} = $transaction->$field() if $transaction->can($field);
731 $capture->content( %capture );
733 $capture->test_transaction(1)
734 if $conf->exists('business-onlinepayment-test_transaction');
737 unless ( $capture->is_success ) {
738 my $e = "Authorization successful but capture failed, custnum #".
739 $self->custnum. ': '. $capture->result_code.
740 ": ". $capture->error_message;
748 # remove paycvv after initial transaction
751 #false laziness w/misc/process/payment.cgi - check both to make sure working
753 if ( length($self->paycvv)
754 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
756 my $error = $self->remove_cvv;
758 warn "WARNING: error removing cvv: $error\n";
767 if ( $transaction->can('card_token') && $transaction->card_token ) {
769 if ( $options{'payinfo'} eq $self->payinfo ) {
770 $self->payinfo($transaction->card_token);
771 my $error = $self->replace;
773 warn "WARNING: error storing token: $error, but proceeding anyway\n";
783 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
795 if (ref($_[0]) eq 'HASH') {
798 my ( $method, $amount ) = ( shift, shift );
800 $options{method} = $method;
801 $options{amount} = $amount;
804 if ( $options{'fake_failure'} ) {
805 return "Error: No error; test failure requested with fake_failure";
808 my $cust_pay = new FS::cust_pay ( {
809 'custnum' => $self->custnum,
810 'invnum' => $options{'invnum'},
811 'paid' => $options{amount},
813 'payby' => $bop_method2payby{$options{method}},
814 #'payinfo' => $payinfo,
815 'payinfo' => '4111111111111111',
816 #'paydate' => $paydate,
817 'paydate' => '2012-05-01',
818 'processor' => 'FakeProcessor',
820 'order_number' => '32',
822 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
825 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
826 warn " $_ => $options{$_}\n" foreach keys %options;
829 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
832 $cust_pay->invnum(''); #try again with no specific invnum
833 my $error2 = $cust_pay->insert( $options{'manual'} ?
834 ( 'manual' => 1 ) : ()
837 # gah, even with transactions.
838 my $e = 'WARNING: Card/ACH debited but database not updated - '.
839 "error inserting (fake!) payment: $error2".
840 " (previously tried insert with invnum #$options{'invnum'}" .
847 if ( $options{'paynum_ref'} ) {
848 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
856 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
858 # Wraps up processing of a realtime credit card, ACH (electronic check) or
859 # phone bill transaction.
861 sub _realtime_bop_result {
862 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
864 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
867 warn "$me _realtime_bop_result: pending transaction ".
868 $cust_pay_pending->paypendingnum. "\n";
869 warn " $_ => $options{$_}\n" foreach keys %options;
872 my $payment_gateway = $options{payment_gateway}
873 or return "no payment gateway in arguments to _realtime_bop_result";
875 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
876 my $cpp_captured_err = $cust_pay_pending->replace;
877 return $cpp_captured_err if $cpp_captured_err;
879 if ( $transaction->is_success() ) {
881 my $order_number = $transaction->order_number
882 if $transaction->can('order_number');
884 my $cust_pay = new FS::cust_pay ( {
885 'custnum' => $self->custnum,
886 'invnum' => $options{'invnum'},
887 'paid' => $cust_pay_pending->paid,
889 'payby' => $cust_pay_pending->payby,
890 'payinfo' => $options{'payinfo'},
891 'paymask' => $options{'paymask'},
892 'paydate' => $cust_pay_pending->paydate,
893 'pkgnum' => $cust_pay_pending->pkgnum,
894 'discount_term' => $options{'discount_term'},
895 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
896 'processor' => $payment_gateway->gateway_module,
897 'auth' => $transaction->authorization,
898 'order_number' => $order_number || '',
901 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
902 $cust_pay->payunique( $options{payunique} )
903 if defined($options{payunique}) && length($options{payunique});
905 my $oldAutoCommit = $FS::UID::AutoCommit;
906 local $FS::UID::AutoCommit = 0;
909 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
911 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
914 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
915 $cust_pay->invnum(''); #try again with no specific invnum
916 $cust_pay->paynum('');
917 my $error2 = $cust_pay->insert( $options{'manual'} ?
918 ( 'manual' => 1 ) : ()
921 # gah. but at least we have a record of the state we had to abort in
922 # from cust_pay_pending now.
923 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
924 my $e = "WARNING: $options{method} captured but payment not recorded -".
925 " error inserting payment (". $payment_gateway->gateway_module.
927 " (previously tried insert with invnum #$options{'invnum'}" .
928 ": $error ) - pending payment saved as paypendingnum ".
929 $cust_pay_pending->paypendingnum. "\n";
935 my $jobnum = $cust_pay_pending->jobnum;
937 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
939 unless ( $placeholder ) {
940 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
941 my $e = "WARNING: $options{method} captured but job $jobnum not ".
942 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
947 $error = $placeholder->delete;
950 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
951 my $e = "WARNING: $options{method} captured but could not delete ".
952 "job $jobnum for paypendingnum ".
953 $cust_pay_pending->paypendingnum. ": $error\n";
958 $cust_pay_pending->set('jobnum','');
962 if ( $options{'paynum_ref'} ) {
963 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
966 $cust_pay_pending->status('done');
967 $cust_pay_pending->statustext('captured');
968 $cust_pay_pending->paynum($cust_pay->paynum);
969 my $cpp_done_err = $cust_pay_pending->replace;
971 if ( $cpp_done_err ) {
973 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
974 my $e = "WARNING: $options{method} captured but payment not recorded - ".
975 "error updating status for paypendingnum ".
976 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
982 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
984 if ( $options{'apply'} ) {
985 my $apply_error = $self->apply_payments_and_credits;
986 if ( $apply_error ) {
987 warn "WARNING: error applying payment: $apply_error\n";
988 #but we still should return no error cause the payment otherwise went
993 # have a CC surcharge portion --> one-time charge
994 if ( $options{'cc_surcharge'} > 0 ) {
995 # XXX: this whole block needs to be in a transaction?
998 $invnum = $options{'invnum'} if $options{'invnum'};
999 unless ( $invnum ) { # probably from a payment screen
1000 # do we have any open invoices? pick earliest
1001 # uses the fact that cust_main->cust_bill sorts by date ascending
1002 my @open = $self->open_cust_bill;
1003 $invnum = $open[0]->invnum if scalar(@open);
1006 unless ( $invnum ) { # still nothing? pick last closed invoice
1007 # again uses fact that cust_main->cust_bill sorts by date ascending
1008 my @closed = $self->cust_bill;
1009 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1012 unless ( $invnum ) {
1013 # XXX: unlikely case - pre-paying before any invoices generated
1014 # what it should do is create a new invoice and pick it
1015 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1020 my $charge_error = $self->charge({
1021 'amount' => $options{'cc_surcharge'},
1022 'pkg' => 'Credit Card Surcharge',
1024 'cust_pkg_ref' => \$cust_pkg,
1027 warn 'Unable to add CC surcharge cust_pkg';
1031 $cust_pkg->setup(time);
1032 my $cp_error = $cust_pkg->replace;
1034 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1038 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1039 unless ( $cust_bill ) {
1040 warn "race condition + invoice deletion just happened";
1045 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1047 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1051 return ''; #no error
1057 my $perror = $transaction->error_message;
1058 #$payment_gateway->gateway_module. " error: ".
1059 # removed for conciseness
1061 my $jobnum = $cust_pay_pending->jobnum;
1063 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1065 if ( $placeholder ) {
1066 my $error = $placeholder->depended_delete;
1067 $error ||= $placeholder->delete;
1068 $cust_pay_pending->set('jobnum','');
1069 warn "error removing provisioning jobs after declined paypendingnum ".
1070 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1072 my $e = "error finding job $jobnum for declined paypendingnum ".
1073 $cust_pay_pending->paypendingnum. "\n";
1079 unless ( $transaction->error_message ) {
1082 if ( $transaction->can('response_page') ) {
1084 'page' => ( $transaction->can('response_page')
1085 ? $transaction->response_page
1088 'code' => ( $transaction->can('response_code')
1089 ? $transaction->response_code
1092 'headers' => ( $transaction->can('response_headers')
1093 ? $transaction->response_headers
1099 "No additional debugging information available for ".
1100 $payment_gateway->gateway_module;
1103 $perror .= "No error_message returned from ".
1104 $payment_gateway->gateway_module. " -- ".
1105 ( ref($t_response) ? Dumper($t_response) : $t_response );
1109 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1110 && $conf->exists('emaildecline', $self->agentnum)
1111 && grep { $_ ne 'POST' } $self->invoicing_list
1112 && ! grep { $transaction->error_message =~ /$_/ }
1113 $conf->config('emaildecline-exclude', $self->agentnum)
1116 # Send a decline alert to the customer.
1117 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1120 # include the raw error message in the transaction state
1121 $cust_pay_pending->setfield('error', $transaction->error_message);
1122 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1123 $error = $msg_template->send( 'cust_main' => $self,
1124 'object' => $cust_pay_pending );
1128 $perror .= " (also received error sending decline notification: $error)"
1133 $cust_pay_pending->status('done');
1134 $cust_pay_pending->statustext($perror);
1135 #'declined:': no, that's failure_status
1136 if ( $transaction->can('failure_status') ) {
1137 $cust_pay_pending->failure_status( $transaction->failure_status );
1139 my $cpp_done_err = $cust_pay_pending->replace;
1140 if ( $cpp_done_err ) {
1141 my $e = "WARNING: $options{method} declined but pending payment not ".
1142 "resolved - error updating status for paypendingnum ".
1143 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1145 $perror = "$e ($perror)";
1153 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1155 Verifies successful third party processing of a realtime credit card,
1156 ACH (electronic check) or phone bill transaction via a
1157 Business::OnlineThirdPartyPayment realtime gateway. See
1158 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1160 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1162 The additional options I<payname>, I<city>, I<state>,
1163 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1164 if set, will override the value from the customer record.
1166 I<description> is a free-text field passed to the gateway. It defaults to
1167 "Internet services".
1169 If an I<invnum> is specified, this payment (if successful) is applied to the
1170 specified invoice. If you don't specify an I<invnum> you might want to
1171 call the B<apply_payments> method.
1173 I<quiet> can be set true to surpress email decline notices.
1175 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1176 resulting paynum, if any.
1178 I<payunique> is a unique identifier for this payment.
1180 Returns a hashref containing elements bill_error (which will be undefined
1181 upon success) and session_id of any associated session.
1185 sub realtime_botpp_capture {
1186 my( $self, $cust_pay_pending, %options ) = @_;
1188 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1191 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1192 warn " $_ => $options{$_}\n" foreach keys %options;
1195 eval "use Business::OnlineThirdPartyPayment";
1199 # select the gateway
1202 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1204 my $payment_gateway;
1205 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1206 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1207 { gatewaynum => $gatewaynum }
1209 : $self->agent->payment_gateway( 'method' => $method,
1210 # 'invnum' => $cust_pay_pending->invnum,
1211 # 'payinfo' => $cust_pay_pending->payinfo,
1214 $options{payment_gateway} = $payment_gateway; # for the helper subs
1220 my @invoicing_list = $self->invoicing_list_emailonly;
1221 if ( $conf->exists('emailinvoiceautoalways')
1222 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1223 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1224 push @invoicing_list, $self->all_emails;
1227 my $email = ($conf->exists('business-onlinepayment-email-override'))
1228 ? $conf->config('business-onlinepayment-email-override')
1229 : $invoicing_list[0];
1233 $content{email_customer} =
1234 ( $conf->exists('business-onlinepayment-email_customer')
1235 || $conf->exists('business-onlinepayment-email-override') );
1238 # run transaction(s)
1242 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1243 $self->_bop_options(\%options),
1246 $transaction->reference({ %options });
1248 $transaction->content(
1250 $self->_bop_auth(\%options),
1251 'action' => 'Post Authorization',
1252 'description' => $options{'description'},
1253 'amount' => $cust_pay_pending->paid,
1254 #'invoice_number' => $options{'invnum'},
1255 'customer_id' => $self->custnum,
1256 'reference' => $cust_pay_pending->paypendingnum,
1258 'phone' => $self->daytime || $self->night,
1260 # plus whatever is required for bogus capture avoidance
1263 $transaction->submit();
1266 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1268 if ( $options{'apply'} ) {
1269 my $apply_error = $self->apply_payments_and_credits;
1270 if ( $apply_error ) {
1271 warn "WARNING: error applying payment: $apply_error\n";
1276 bill_error => $error,
1277 session_id => $cust_pay_pending->session_id,
1282 =item default_payment_gateway
1284 DEPRECATED -- use agent->payment_gateway
1288 sub default_payment_gateway {
1289 my( $self, $method ) = @_;
1291 die "Real-time processing not enabled\n"
1292 unless $conf->exists('business-onlinepayment');
1294 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1297 my $bop_config = 'business-onlinepayment';
1298 $bop_config .= '-ach'
1299 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1300 my ( $processor, $login, $password, $action, @bop_options ) =
1301 $conf->config($bop_config);
1302 $action ||= 'normal authorization';
1303 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1304 die "No real-time processor is enabled - ".
1305 "did you set the business-onlinepayment configuration value?\n"
1308 ( $processor, $login, $password, $action, @bop_options )
1311 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1313 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1314 via a Business::OnlinePayment realtime gateway. See
1315 L<http://420.am/business-onlinepayment> for supported gateways.
1317 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1319 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1321 Most gateways require a reference to an original payment transaction to refund,
1322 so you probably need to specify a I<paynum>.
1324 I<amount> defaults to the original amount of the payment if not specified.
1326 I<reason> specifies a reason for the refund.
1328 I<paydate> specifies the expiration date for a credit card overriding the
1329 value from the customer record or the payment record. Specified as yyyy-mm-dd
1331 Implementation note: If I<amount> is unspecified or equal to the amount of the
1332 orignal payment, first an attempt is made to "void" the transaction via
1333 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1334 the normal attempt is made to "refund" ("credit") the transaction via the
1335 gateway is attempted. No attempt to "void" the transaction is made if the
1336 gateway has introspection data and doesn't support void.
1338 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1339 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1340 #if set, will override the value from the customer record.
1342 #If an I<invnum> is specified, this payment (if successful) is applied to the
1343 #specified invoice. If you don't specify an I<invnum> you might want to
1344 #call the B<apply_payments> method.
1348 #some false laziness w/realtime_bop, not enough to make it worth merging
1349 #but some useful small subs should be pulled out
1350 sub realtime_refund_bop {
1353 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1356 if (ref($_[0]) eq 'HASH') {
1357 %options = %{$_[0]};
1361 $options{method} = $method;
1365 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1366 warn " $_ => $options{$_}\n" foreach keys %options;
1370 # look up the original payment and optionally a gateway for that payment
1374 my $amount = $options{'amount'};
1376 my( $processor, $login, $password, @bop_options, $namespace ) ;
1377 my( $auth, $order_number ) = ( '', '', '' );
1378 my $gatewaynum = '';
1380 if ( $options{'paynum'} ) {
1382 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1383 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1384 or return "Unknown paynum $options{'paynum'}";
1385 $amount ||= $cust_pay->paid;
1387 if ( $cust_pay->get('processor') ) {
1388 ($gatewaynum, $processor, $auth, $order_number) =
1390 $cust_pay->gatewaynum,
1391 $cust_pay->processor,
1393 $cust_pay->order_number,
1396 # this payment wasn't upgraded, which probably means this won't work,
1398 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1399 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1400 $cust_pay->paybatch;
1401 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1404 if ( $gatewaynum ) { #gateway for the payment to be refunded
1406 my $payment_gateway =
1407 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1408 die "payment gateway $gatewaynum not found"
1409 unless $payment_gateway;
1411 $processor = $payment_gateway->gateway_module;
1412 $login = $payment_gateway->gateway_username;
1413 $password = $payment_gateway->gateway_password;
1414 $namespace = $payment_gateway->gateway_namespace;
1415 @bop_options = $payment_gateway->options;
1417 } else { #try the default gateway
1420 my $payment_gateway =
1421 $self->agent->payment_gateway('method' => $options{method});
1423 ( $conf_processor, $login, $password, $namespace ) =
1424 map { my $method = "gateway_$_"; $payment_gateway->$method }
1425 qw( module username password namespace );
1427 @bop_options = $payment_gateway->gatewaynum
1428 ? $payment_gateway->options
1429 : @{ $payment_gateway->get('options') };
1431 return "processor of payment $options{'paynum'} $processor does not".
1432 " match default processor $conf_processor"
1433 unless $processor eq $conf_processor;
1438 } else { # didn't specify a paynum, so look for agent gateway overrides
1439 # like a normal transaction
1441 my $payment_gateway =
1442 $self->agent->payment_gateway( 'method' => $options{method},
1443 #'payinfo' => $payinfo,
1445 my( $processor, $login, $password, $namespace ) =
1446 map { my $method = "gateway_$_"; $payment_gateway->$method }
1447 qw( module username password namespace );
1449 my @bop_options = $payment_gateway->gatewaynum
1450 ? $payment_gateway->options
1451 : @{ $payment_gateway->get('options') };
1454 return "neither amount nor paynum specified" unless $amount;
1456 eval "use $namespace";
1460 'type' => $options{method},
1462 'password' => $password,
1463 'order_number' => $order_number,
1464 'amount' => $amount,
1466 $content{authorization} = $auth
1467 if length($auth); #echeck/ACH transactions have an order # but no auth
1468 #(at least with authorize.net)
1470 my $currency = $conf->exists('business-onlinepayment-currency')
1471 && $conf->config('business-onlinepayment-currency');
1472 $content{currency} = $currency if $currency;
1474 my $disable_void_after;
1475 if ($conf->exists('disable_void_after')
1476 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1477 $disable_void_after = $1;
1480 #first try void if applicable
1481 my $void = new Business::OnlinePayment( $processor, @bop_options );
1484 if ($void->can('info')) {
1486 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1487 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1488 my %supported_actions = $void->info('supported_actions');
1490 if ( %supported_actions && $paytype
1491 && defined($supported_actions{$paytype})
1492 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1495 if ( $cust_pay && $cust_pay->paid == $amount
1497 ( not defined($disable_void_after) )
1498 || ( time < ($cust_pay->_date + $disable_void_after ) )
1502 warn " attempting void\n" if $DEBUG > 1;
1503 if ( $void->can('info') ) {
1504 if ( $cust_pay->payby eq 'CARD'
1505 && $void->info('CC_void_requires_card') )
1507 $content{'card_number'} = $cust_pay->payinfo;
1508 } elsif ( $cust_pay->payby eq 'CHEK'
1509 && $void->info('ECHECK_void_requires_account') )
1511 ( $content{'account_number'}, $content{'routing_code'} ) =
1512 split('@', $cust_pay->payinfo);
1513 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1516 $void->content( 'action' => 'void', %content );
1517 $void->test_transaction(1)
1518 if $conf->exists('business-onlinepayment-test_transaction');
1520 if ( $void->is_success ) {
1521 my $error = $cust_pay->void($options{'reason'});
1523 # gah, even with transactions.
1524 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1525 "error voiding payment: $error";
1529 warn " void successful\n" if $DEBUG > 1;
1534 warn " void unsuccessful, trying refund\n"
1538 my $address = $self->address1;
1539 $address .= ", ". $self->address2 if $self->address2;
1541 my($payname, $payfirst, $paylast);
1542 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1543 $payname = $self->payname;
1544 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1545 or return "Illegal payname $payname";
1546 ($payfirst, $paylast) = ($1, $2);
1548 $payfirst = $self->getfield('first');
1549 $paylast = $self->getfield('last');
1550 $payname = "$payfirst $paylast";
1553 my @invoicing_list = $self->invoicing_list_emailonly;
1554 if ( $conf->exists('emailinvoiceautoalways')
1555 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1556 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1557 push @invoicing_list, $self->all_emails;
1560 my $email = ($conf->exists('business-onlinepayment-email-override'))
1561 ? $conf->config('business-onlinepayment-email-override')
1562 : $invoicing_list[0];
1564 my $payip = exists($options{'payip'})
1567 $content{customer_ip} = $payip
1571 if ( $options{method} eq 'CC' ) {
1574 $content{card_number} = $payinfo = $cust_pay->payinfo;
1575 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1576 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1577 ($content{expiration} = "$2/$1"); # where available
1579 $content{card_number} = $payinfo = $self->payinfo;
1580 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1581 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1582 $content{expiration} = "$2/$1";
1585 } elsif ( $options{method} eq 'ECHECK' ) {
1588 $payinfo = $cust_pay->payinfo;
1590 $payinfo = $self->payinfo;
1592 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1593 $content{bank_name} = $self->payname;
1594 $content{account_type} = 'CHECKING';
1595 $content{account_name} = $payname;
1596 $content{customer_org} = $self->company ? 'B' : 'I';
1597 $content{customer_ssn} = $self->ss;
1598 } elsif ( $options{method} eq 'LEC' ) {
1599 $content{phone} = $payinfo = $self->payinfo;
1603 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1604 my %sub_content = $refund->content(
1605 'action' => 'credit',
1606 'customer_id' => $self->custnum,
1607 'last_name' => $paylast,
1608 'first_name' => $payfirst,
1610 'address' => $address,
1611 'city' => $self->city,
1612 'state' => $self->state,
1613 'zip' => $self->zip,
1614 'country' => $self->country,
1616 'phone' => $self->daytime || $self->night,
1619 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1621 $refund->test_transaction(1)
1622 if $conf->exists('business-onlinepayment-test_transaction');
1625 return "$processor error: ". $refund->error_message
1626 unless $refund->is_success();
1628 $order_number = $refund->order_number if $refund->can('order_number');
1630 # change this to just use $cust_pay->delete_cust_bill_pay?
1631 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1632 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1633 last unless @cust_bill_pay;
1634 my $cust_bill_pay = pop @cust_bill_pay;
1635 my $error = $cust_bill_pay->delete;
1639 my $cust_refund = new FS::cust_refund ( {
1640 'custnum' => $self->custnum,
1641 'paynum' => $options{'paynum'},
1642 'refund' => $amount,
1644 'payby' => $bop_method2payby{$options{method}},
1645 'payinfo' => $payinfo,
1646 'reason' => $options{'reason'} || 'card or ACH refund',
1647 'gatewaynum' => $gatewaynum, # may be null
1648 'processor' => $processor,
1649 'auth' => $refund->authorization,
1650 'order_number' => $order_number,
1652 my $error = $cust_refund->insert;
1654 $cust_refund->paynum(''); #try again with no specific paynum
1655 my $error2 = $cust_refund->insert;
1657 # gah, even with transactions.
1658 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1659 "error inserting refund ($processor): $error2".
1660 " (previously tried insert with paynum #$options{'paynum'}" .
1679 L<FS::cust_main>, L<FS::cust_main::Billing>