1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.28;
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
17 $realtime_bop_decline_quiet = 0;
19 # 1 is mostly method/subroutine entry and options
20 # 2 traces progress of some operations
21 # 3 is even more information including possibly sensitive data
23 $me = '[FS::cust_main::Billing_Realtime]';
26 our $BOP_TESTING_SUCCESS = 1;
28 install_callback FS::UID sub {
30 #yes, need it for stuff below (prolly should be cached)
35 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
41 These methods are available on FS::cust_main objects.
47 =item realtime_collect [ OPTION => VALUE ... ]
49 Attempt to collect the customer's current balance with a realtime credit
50 card, electronic check, or phone bill transaction (see realtime_bop() below).
52 Returns the result of realtime_bop(): nothing, an error message, or a
53 hashref of state information for a third-party transaction.
55 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
57 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
58 then it is deduced from the customer record.
60 If no I<amount> is specified, then the customer balance is used.
62 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
63 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
64 if set, will override the value from the customer record.
66 I<description> is a free-text field passed to the gateway. It defaults to
67 the value defined by the business-onlinepayment-description configuration
68 option, or "Internet services" if that is unset.
70 If an I<invnum> is specified, this payment (if successful) is applied to the
73 I<apply> will automatically apply a resulting payment.
75 I<quiet> can be set true to suppress email decline notices.
77 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
78 resulting paynum, if any.
80 I<payunique> is a unique identifier for this payment.
82 I<session_id> is a session identifier associated with this payment.
84 I<depend_jobnum> allows payment capture to unlock export jobs
88 sub realtime_collect {
89 my( $self, %options ) = @_;
91 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
94 warn "$me realtime_collect:\n";
95 warn " $_ => $options{$_}\n" foreach keys %options;
98 $options{amount} = $self->balance unless exists( $options{amount} );
99 $options{method} = FS::payby->payby2bop($self->payby)
100 unless exists( $options{method} );
102 return $self->realtime_bop({%options});
106 =item realtime_bop { [ ARG => VALUE ... ] }
108 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
109 via a Business::OnlinePayment realtime gateway. See
110 L<http://420.am/business-onlinepayment> for supported gateways.
112 Required arguments in the hashref are I<method>, and I<amount>
114 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
116 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
118 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
119 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
120 if set, will override the value from the customer record.
122 I<description> is a free-text field passed to the gateway. It defaults to
123 the value defined by the business-onlinepayment-description configuration
124 option, or "Internet services" if that is unset.
126 If an I<invnum> is specified, this payment (if successful) is applied to the
127 specified invoice. If the customer has exactly one open invoice, that
128 invoice number will be assumed. If you don't specify an I<invnum> you might
129 want to call the B<apply_payments> method or set the I<apply> option.
131 I<apply> can be set to true to apply a resulting payment.
133 I<quiet> can be set true to surpress email decline notices.
135 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
136 resulting paynum, if any.
138 I<payunique> is a unique identifier for this payment.
140 I<session_id> is a session identifier associated with this payment.
142 I<depend_jobnum> allows payment capture to unlock export jobs
144 I<discount_term> attempts to take a discount by prepaying for discount_term.
145 The payment will fail if I<amount> is incorrect for this discount term.
147 A direct (Business::OnlinePayment) transaction will return nothing on success,
148 or an error message on failure.
150 A third-party transaction will return a hashref containing:
152 - popup_url: the URL to which a browser should be redirected to complete
154 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
155 - reference: a reference ID for the transaction, to show the customer.
157 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
161 # some helper routines
162 sub _bop_recurring_billing {
163 my( $self, %opt ) = @_;
165 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
167 if ( defined($method) && $method eq 'transaction_is_recur' ) {
169 return 1 if $opt{'trans_is_recur'};
173 # return 1 if the payinfo has been used for another payment
174 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
182 sub _payment_gateway {
183 my ($self, $options) = @_;
185 if ( $options->{'selfservice'} ) {
186 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
188 return $options->{payment_gateway} ||=
189 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
193 if ( $options->{'fake_gatewaynum'} ) {
194 $options->{payment_gateway} =
195 qsearchs('payment_gateway',
196 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
200 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
201 unless exists($options->{payment_gateway});
203 $options->{payment_gateway};
207 my ($self, $options) = @_;
210 'login' => $options->{payment_gateway}->gateway_username,
211 'password' => $options->{payment_gateway}->gateway_password,
216 my ($self, $options) = @_;
218 $options->{payment_gateway}->gatewaynum
219 ? $options->{payment_gateway}->options
220 : @{ $options->{payment_gateway}->get('options') };
225 my ($self, $options) = @_;
227 unless ( $options->{'description'} ) {
228 if ( $conf->exists('business-onlinepayment-description') ) {
229 my $dtempl = $conf->config('business-onlinepayment-description');
231 my $agent = $self->agent->agent;
233 $options->{'description'} = eval qq("$dtempl");
235 $options->{'description'} = 'Internet services';
239 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
241 # Default invoice number if the customer has exactly one open invoice.
242 if( ! $options->{'invnum'} ) {
243 $options->{'invnum'} = '';
244 my @open = $self->open_cust_bill;
245 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
248 $options->{payname} = $self->payname unless exists( $options->{payname} );
252 my ($self, $options) = @_;
255 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
256 $content{customer_ip} = $payip if length($payip);
258 $content{invoice_number} = $options->{'invnum'}
259 if exists($options->{'invnum'}) && length($options->{'invnum'});
261 $content{email_customer} =
262 ( $conf->exists('business-onlinepayment-email_customer')
263 || $conf->exists('business-onlinepayment-email-override') );
265 my ($payname, $payfirst, $paylast);
266 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
267 ($payname = $options->{payname}) =~
268 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
269 or return "Illegal payname $payname";
270 ($payfirst, $paylast) = ($1, $2);
272 $payfirst = $self->getfield('first');
273 $paylast = $self->getfield('last');
274 $payname = "$payfirst $paylast";
277 $content{last_name} = $paylast;
278 $content{first_name} = $payfirst;
280 $content{name} = $payname;
282 $content{address} = exists($options->{'address1'})
283 ? $options->{'address1'}
285 my $address2 = exists($options->{'address2'})
286 ? $options->{'address2'}
288 $content{address} .= ", ". $address2 if length($address2);
290 $content{city} = exists($options->{city})
293 $content{state} = exists($options->{state})
296 $content{zip} = exists($options->{zip})
299 $content{country} = exists($options->{country})
300 ? $options->{country}
303 #3.0 is a good a time as any to get rid of this... add a config to pass it
304 # if anyone still needs it
305 #$content{referer} = 'http://cleanwhisker.420.am/';
307 $content{phone} = $self->daytime || $self->night;
309 my $currency = $conf->exists('business-onlinepayment-currency')
310 && $conf->config('business-onlinepayment-currency');
311 $content{currency} = $currency if $currency;
316 my %bop_method2payby = (
326 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
329 if (ref($_[0]) eq 'HASH') {
332 my ( $method, $amount ) = ( shift, shift );
334 $options{method} = $method;
335 $options{amount} = $amount;
340 # optional credit card surcharge
343 my $cc_surcharge = 0;
344 my $cc_surcharge_pct = 0;
345 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
346 if $conf->config('credit-card-surcharge-percentage')
347 && $options{method} eq 'CC';
349 # always add cc surcharge if called from event
350 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
351 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
352 $options{'amount'} += $cc_surcharge;
353 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
355 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
356 # payment screen), so consider the given
357 # amount as post-surcharge
358 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
361 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
362 $options{'cc_surcharge'} = $cc_surcharge;
366 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
367 warn " cc_surcharge = $cc_surcharge\n";
370 warn " $_ => $options{$_}\n" foreach keys %options;
373 return $self->fake_bop(\%options) if $options{'fake'};
375 $self->_bop_defaults(\%options);
378 # set trans_is_recur based on invnum if there is one
381 my $trans_is_recur = 0;
382 if ( $options{'invnum'} ) {
384 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
385 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
391 $cust_bill->cust_bill_pkg;
394 if grep { $_->freq ne '0' } @part_pkg;
402 my $payment_gateway = $self->_payment_gateway( \%options );
403 my $namespace = $payment_gateway->gateway_namespace;
405 eval "use $namespace";
409 # check for banned credit card/ACH
412 my $ban = FS::banned_pay->ban_search(
413 'payby' => $bop_method2payby{$options{method}},
414 'payinfo' => $options{payinfo},
416 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
419 # check for term discount validity
422 my $discount_term = $options{discount_term};
423 if ( $discount_term ) {
424 my $bill = ($self->cust_bill)[-1]
425 or return "Can't apply a term discount to an unbilled customer";
426 my $plan = FS::discount_plan->new(
428 months => $discount_term
429 ) or return "No discount available for term '$discount_term'";
431 if ( $plan->discounted_total != $options{amount} ) {
432 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
440 my $bop_content = $self->_bop_content(\%options);
441 return $bop_content unless ref($bop_content);
443 my @invoicing_list = $self->invoicing_list_emailonly;
444 if ( $conf->exists('emailinvoiceautoalways')
445 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
446 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
447 push @invoicing_list, $self->all_emails;
450 my $email = ($conf->exists('business-onlinepayment-email-override'))
451 ? $conf->config('business-onlinepayment-email-override')
452 : $invoicing_list[0];
457 if ( $namespace eq 'Business::OnlinePayment' ) {
459 if ( $options{method} eq 'CC' ) {
461 $content{card_number} = $options{payinfo};
462 $paydate = exists($options{'paydate'})
463 ? $options{'paydate'}
465 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
466 $content{expiration} = "$2/$1";
468 my $paycvv = exists($options{'paycvv'})
471 $content{cvv2} = $paycvv
474 my $paystart_month = exists($options{'paystart_month'})
475 ? $options{'paystart_month'}
476 : $self->paystart_month;
478 my $paystart_year = exists($options{'paystart_year'})
479 ? $options{'paystart_year'}
480 : $self->paystart_year;
482 $content{card_start} = "$paystart_month/$paystart_year"
483 if $paystart_month && $paystart_year;
485 my $payissue = exists($options{'payissue'})
486 ? $options{'payissue'}
488 $content{issue_number} = $payissue if $payissue;
490 if ( $self->_bop_recurring_billing(
491 'payinfo' => $options{'payinfo'},
492 'trans_is_recur' => $trans_is_recur,
496 $content{recurring_billing} = 'YES';
497 $content{acct_code} = 'rebill'
498 if $conf->exists('credit_card-recurring_billing_acct_code');
501 } elsif ( $options{method} eq 'ECHECK' ){
503 ( $content{account_number}, $content{routing_code} ) =
504 split('@', $options{payinfo});
505 $content{bank_name} = $options{payname};
506 $content{bank_state} = exists($options{'paystate'})
507 ? $options{'paystate'}
508 : $self->getfield('paystate');
509 $content{account_type}=
510 (exists($options{'paytype'}) && $options{'paytype'})
511 ? uc($options{'paytype'})
512 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
514 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
515 $content{account_name} = $self->company;
517 $content{account_name} = $self->getfield('first'). ' '.
518 $self->getfield('last');
521 $content{customer_org} = $self->company ? 'B' : 'I';
522 $content{state_id} = exists($options{'stateid'})
523 ? $options{'stateid'}
524 : $self->getfield('stateid');
525 $content{state_id_state} = exists($options{'stateid_state'})
526 ? $options{'stateid_state'}
527 : $self->getfield('stateid_state');
528 $content{customer_ssn} = exists($options{'ss'})
532 } elsif ( $options{method} eq 'LEC' ) {
533 $content{phone} = $options{payinfo};
535 die "unknown method ". $options{method};
538 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
541 die "unknown namespace $namespace";
548 my $balance = exists( $options{'balance'} )
549 ? $options{'balance'}
552 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
553 $self->select_for_update; #mutex ... just until we get our pending record in
554 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
556 #the checks here are intended to catch concurrent payments
557 #double-form-submission prevention is taken care of in cust_pay_pending::check
560 return "The customer's balance has changed; $options{method} transaction aborted."
561 if $self->balance < $balance;
563 #also check and make sure there aren't *other* pending payments for this cust
565 my @pending = qsearch('cust_pay_pending', {
566 'custnum' => $self->custnum,
567 'status' => { op=>'!=', value=>'done' }
570 #for third-party payments only, remove pending payments if they're in the
571 #'thirdparty' (waiting for customer action) state.
572 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
573 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
574 my $error = $_->delete;
575 warn "error deleting unfinished third-party payment ".
576 $_->paypendingnum . ": $error\n"
579 @pending = grep { $_->status ne 'thirdparty' } @pending;
582 return "A payment is already being processed for this customer (".
583 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
584 "); $options{method} transaction aborted."
587 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
589 my $cust_pay_pending = new FS::cust_pay_pending {
590 'custnum' => $self->custnum,
591 'paid' => $options{amount},
593 'payby' => $bop_method2payby{$options{method}},
594 'payinfo' => $options{payinfo},
595 'paydate' => $paydate,
596 'recurring_billing' => $content{recurring_billing},
597 'pkgnum' => $options{'pkgnum'},
599 'gatewaynum' => $payment_gateway->gatewaynum || '',
600 'session_id' => $options{session_id} || '',
601 'jobnum' => $options{depend_jobnum} || '',
603 $cust_pay_pending->payunique( $options{payunique} )
604 if defined($options{payunique}) && length($options{payunique});
606 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
608 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
609 return $cpp_new_err if $cpp_new_err;
611 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
613 warn Dumper($cust_pay_pending) if $DEBUG > 2;
615 my( $action1, $action2 ) =
616 split( /\s*\,\s*/, $payment_gateway->gateway_action );
618 my $transaction = new $namespace( $payment_gateway->gateway_module,
619 $self->_bop_options(\%options),
622 $transaction->content(
623 'type' => $options{method},
624 $self->_bop_auth(\%options),
625 'action' => $action1,
626 'description' => $options{'description'},
627 'amount' => $options{amount},
628 #'invoice_number' => $options{'invnum'},
629 'customer_id' => $self->custnum,
631 'reference' => $cust_pay_pending->paypendingnum, #for now
632 'callback_url' => $payment_gateway->gateway_callback_url,
633 'cancel_url' => $payment_gateway->gateway_cancel_url,
638 $cust_pay_pending->status('pending');
639 my $cpp_pending_err = $cust_pay_pending->replace;
640 return $cpp_pending_err if $cpp_pending_err;
642 warn Dumper($transaction) if $DEBUG > 2;
644 unless ( $BOP_TESTING ) {
645 $transaction->test_transaction(1)
646 if $conf->exists('business-onlinepayment-test_transaction');
647 $transaction->submit();
649 if ( $BOP_TESTING_SUCCESS ) {
650 $transaction->is_success(1);
651 $transaction->authorization('fake auth');
653 $transaction->is_success(0);
654 $transaction->error_message('fake failure');
658 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
660 $cust_pay_pending->status('thirdparty');
661 my $cpp_err = $cust_pay_pending->replace;
662 return { error => $cpp_err } if $cpp_err;
663 return { reference => $cust_pay_pending->paypendingnum,
664 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
666 } elsif ( $transaction->is_success() && $action2 ) {
668 $cust_pay_pending->status('authorized');
669 my $cpp_authorized_err = $cust_pay_pending->replace;
670 return $cpp_authorized_err if $cpp_authorized_err;
672 my $auth = $transaction->authorization;
673 my $ordernum = $transaction->can('order_number')
674 ? $transaction->order_number
678 new Business::OnlinePayment( $payment_gateway->gateway_module,
679 $self->_bop_options(\%options),
684 type => $options{method},
686 $self->_bop_auth(\%options),
687 order_number => $ordernum,
688 amount => $options{amount},
689 authorization => $auth,
690 description => $options{'description'},
693 foreach my $field (qw( authorization_source_code returned_ACI
694 transaction_identifier validation_code
695 transaction_sequence_num local_transaction_date
696 local_transaction_time AVS_result_code )) {
697 $capture{$field} = $transaction->$field() if $transaction->can($field);
700 $capture->content( %capture );
702 $capture->test_transaction(1)
703 if $conf->exists('business-onlinepayment-test_transaction');
706 unless ( $capture->is_success ) {
707 my $e = "Authorization successful but capture failed, custnum #".
708 $self->custnum. ': '. $capture->result_code.
709 ": ". $capture->error_message;
717 # remove paycvv after initial transaction
720 #false laziness w/misc/process/payment.cgi - check both to make sure working
722 if ( length($self->paycvv)
723 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
725 my $error = $self->remove_cvv;
727 warn "WARNING: error removing cvv: $error\n";
736 if ( $transaction->can('card_token') && $transaction->card_token ) {
738 if ( $options{'payinfo'} eq $self->payinfo ) {
739 $self->payinfo($transaction->card_token);
740 my $error = $self->replace;
742 warn "WARNING: error storing token: $error, but proceeding anyway\n";
752 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
764 if (ref($_[0]) eq 'HASH') {
767 my ( $method, $amount ) = ( shift, shift );
769 $options{method} = $method;
770 $options{amount} = $amount;
773 if ( $options{'fake_failure'} ) {
774 return "Error: No error; test failure requested with fake_failure";
777 my $cust_pay = new FS::cust_pay ( {
778 'custnum' => $self->custnum,
779 'invnum' => $options{'invnum'},
780 'paid' => $options{amount},
782 'payby' => $bop_method2payby{$options{method}},
783 #'payinfo' => $payinfo,
784 'payinfo' => '4111111111111111',
785 #'paydate' => $paydate,
786 'paydate' => '2012-05-01',
787 'processor' => 'FakeProcessor',
789 'order_number' => '32',
791 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
794 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
795 warn " $_ => $options{$_}\n" foreach keys %options;
798 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
801 $cust_pay->invnum(''); #try again with no specific invnum
802 my $error2 = $cust_pay->insert( $options{'manual'} ?
803 ( 'manual' => 1 ) : ()
806 # gah, even with transactions.
807 my $e = 'WARNING: Card/ACH debited but database not updated - '.
808 "error inserting (fake!) payment: $error2".
809 " (previously tried insert with invnum #$options{'invnum'}" .
816 if ( $options{'paynum_ref'} ) {
817 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
825 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
827 # Wraps up processing of a realtime credit card, ACH (electronic check) or
828 # phone bill transaction.
830 sub _realtime_bop_result {
831 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
833 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
836 warn "$me _realtime_bop_result: pending transaction ".
837 $cust_pay_pending->paypendingnum. "\n";
838 warn " $_ => $options{$_}\n" foreach keys %options;
841 my $payment_gateway = $options{payment_gateway}
842 or return "no payment gateway in arguments to _realtime_bop_result";
844 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
845 my $cpp_captured_err = $cust_pay_pending->replace;
846 return $cpp_captured_err if $cpp_captured_err;
848 if ( $transaction->is_success() ) {
850 my $order_number = $transaction->order_number
851 if $transaction->can('order_number');
853 my $cust_pay = new FS::cust_pay ( {
854 'custnum' => $self->custnum,
855 'invnum' => $options{'invnum'},
856 'paid' => $cust_pay_pending->paid,
858 'payby' => $cust_pay_pending->payby,
859 'payinfo' => $options{'payinfo'},
860 'paydate' => $cust_pay_pending->paydate,
861 'pkgnum' => $cust_pay_pending->pkgnum,
862 'discount_term' => $options{'discount_term'},
863 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
864 'processor' => $payment_gateway->gateway_module,
865 'auth' => $transaction->authorization,
866 'order_number' => $order_number || '',
869 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
870 $cust_pay->payunique( $options{payunique} )
871 if defined($options{payunique}) && length($options{payunique});
873 my $oldAutoCommit = $FS::UID::AutoCommit;
874 local $FS::UID::AutoCommit = 0;
877 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
879 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
882 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
883 $cust_pay->invnum(''); #try again with no specific invnum
884 $cust_pay->paynum('');
885 my $error2 = $cust_pay->insert( $options{'manual'} ?
886 ( 'manual' => 1 ) : ()
889 # gah. but at least we have a record of the state we had to abort in
890 # from cust_pay_pending now.
891 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
892 my $e = "WARNING: $options{method} captured but payment not recorded -".
893 " error inserting payment (". $payment_gateway->gateway_module.
895 " (previously tried insert with invnum #$options{'invnum'}" .
896 ": $error ) - pending payment saved as paypendingnum ".
897 $cust_pay_pending->paypendingnum. "\n";
903 my $jobnum = $cust_pay_pending->jobnum;
905 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
907 unless ( $placeholder ) {
908 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
909 my $e = "WARNING: $options{method} captured but job $jobnum not ".
910 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
915 $error = $placeholder->delete;
918 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
919 my $e = "WARNING: $options{method} captured but could not delete ".
920 "job $jobnum for paypendingnum ".
921 $cust_pay_pending->paypendingnum. ": $error\n";
928 if ( $options{'paynum_ref'} ) {
929 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
932 $cust_pay_pending->status('done');
933 $cust_pay_pending->statustext('captured');
934 $cust_pay_pending->paynum($cust_pay->paynum);
935 my $cpp_done_err = $cust_pay_pending->replace;
937 if ( $cpp_done_err ) {
939 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
940 my $e = "WARNING: $options{method} captured but payment not recorded - ".
941 "error updating status for paypendingnum ".
942 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
948 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
950 if ( $options{'apply'} ) {
951 my $apply_error = $self->apply_payments_and_credits;
952 if ( $apply_error ) {
953 warn "WARNING: error applying payment: $apply_error\n";
954 #but we still should return no error cause the payment otherwise went
959 # have a CC surcharge portion --> one-time charge
960 if ( $options{'cc_surcharge'} > 0 ) {
961 # XXX: this whole block needs to be in a transaction?
964 $invnum = $options{'invnum'} if $options{'invnum'};
965 unless ( $invnum ) { # probably from a payment screen
966 # do we have any open invoices? pick earliest
967 # uses the fact that cust_main->cust_bill sorts by date ascending
968 my @open = $self->open_cust_bill;
969 $invnum = $open[0]->invnum if scalar(@open);
972 unless ( $invnum ) { # still nothing? pick last closed invoice
973 # again uses fact that cust_main->cust_bill sorts by date ascending
974 my @closed = $self->cust_bill;
975 $invnum = $closed[$#closed]->invnum if scalar(@closed);
979 # XXX: unlikely case - pre-paying before any invoices generated
980 # what it should do is create a new invoice and pick it
981 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
986 my $charge_error = $self->charge({
987 'amount' => $options{'cc_surcharge'},
988 'pkg' => 'Credit Card Surcharge',
990 'cust_pkg_ref' => \$cust_pkg,
993 warn 'Unable to add CC surcharge cust_pkg';
997 $cust_pkg->setup(time);
998 my $cp_error = $cust_pkg->replace;
1000 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1004 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1005 unless ( $cust_bill ) {
1006 warn "race condition + invoice deletion just happened";
1011 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1013 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1017 return ''; #no error
1023 my $perror = $payment_gateway->gateway_module. " error: ".
1024 $transaction->error_message;
1026 my $jobnum = $cust_pay_pending->jobnum;
1028 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1030 if ( $placeholder ) {
1031 my $error = $placeholder->depended_delete;
1032 $error ||= $placeholder->delete;
1033 warn "error removing provisioning jobs after declined paypendingnum ".
1034 $cust_pay_pending->paypendingnum. ": $error\n";
1036 my $e = "error finding job $jobnum for declined paypendingnum ".
1037 $cust_pay_pending->paypendingnum. "\n";
1043 unless ( $transaction->error_message ) {
1046 if ( $transaction->can('response_page') ) {
1048 'page' => ( $transaction->can('response_page')
1049 ? $transaction->response_page
1052 'code' => ( $transaction->can('response_code')
1053 ? $transaction->response_code
1056 'headers' => ( $transaction->can('response_headers')
1057 ? $transaction->response_headers
1063 "No additional debugging information available for ".
1064 $payment_gateway->gateway_module;
1067 $perror .= "No error_message returned from ".
1068 $payment_gateway->gateway_module. " -- ".
1069 ( ref($t_response) ? Dumper($t_response) : $t_response );
1073 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1074 && $conf->exists('emaildecline', $self->agentnum)
1075 && grep { $_ ne 'POST' } $self->invoicing_list
1076 && ! grep { $transaction->error_message =~ /$_/ }
1077 $conf->config('emaildecline-exclude', $self->agentnum)
1080 # Send a decline alert to the customer.
1081 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1084 # include the raw error message in the transaction state
1085 $cust_pay_pending->setfield('error', $transaction->error_message);
1086 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1087 $error = $msg_template->send( 'cust_main' => $self,
1088 'object' => $cust_pay_pending );
1092 my @templ = $conf->config('declinetemplate');
1093 my $template = new Text::Template (
1095 SOURCE => [ map "$_\n", @templ ],
1096 ) or return "($perror) can't create template: $Text::Template::ERROR";
1097 $template->compile()
1098 or return "($perror) can't compile template: $Text::Template::ERROR";
1102 scalar( $conf->config('company_name', $self->agentnum ) ),
1103 'company_address' =>
1104 join("\n", $conf->config('company_address', $self->agentnum ) ),
1105 'error' => $transaction->error_message,
1108 my $error = send_email(
1109 'from' => $conf->invoice_from_full( $self->agentnum ),
1110 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1111 'subject' => 'Your payment could not be processed',
1112 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1116 $perror .= " (also received error sending decline notification: $error)"
1121 $cust_pay_pending->status('done');
1122 $cust_pay_pending->statustext("declined: $perror");
1123 my $cpp_done_err = $cust_pay_pending->replace;
1124 if ( $cpp_done_err ) {
1125 my $e = "WARNING: $options{method} declined but pending payment not ".
1126 "resolved - error updating status for paypendingnum ".
1127 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1129 $perror = "$e ($perror)";
1137 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1139 Verifies successful third party processing of a realtime credit card,
1140 ACH (electronic check) or phone bill transaction via a
1141 Business::OnlineThirdPartyPayment realtime gateway. See
1142 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1144 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1146 The additional options I<payname>, I<city>, I<state>,
1147 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1148 if set, will override the value from the customer record.
1150 I<description> is a free-text field passed to the gateway. It defaults to
1151 "Internet services".
1153 If an I<invnum> is specified, this payment (if successful) is applied to the
1154 specified invoice. If you don't specify an I<invnum> you might want to
1155 call the B<apply_payments> method.
1157 I<quiet> can be set true to surpress email decline notices.
1159 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1160 resulting paynum, if any.
1162 I<payunique> is a unique identifier for this payment.
1164 Returns a hashref containing elements bill_error (which will be undefined
1165 upon success) and session_id of any associated session.
1169 sub realtime_botpp_capture {
1170 my( $self, $cust_pay_pending, %options ) = @_;
1172 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1175 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1176 warn " $_ => $options{$_}\n" foreach keys %options;
1179 eval "use Business::OnlineThirdPartyPayment";
1183 # select the gateway
1186 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1188 my $payment_gateway;
1189 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1190 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1191 { gatewaynum => $gatewaynum }
1193 : $self->agent->payment_gateway( 'method' => $method,
1194 # 'invnum' => $cust_pay_pending->invnum,
1195 # 'payinfo' => $cust_pay_pending->payinfo,
1198 $options{payment_gateway} = $payment_gateway; # for the helper subs
1204 my @invoicing_list = $self->invoicing_list_emailonly;
1205 if ( $conf->exists('emailinvoiceautoalways')
1206 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1207 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1208 push @invoicing_list, $self->all_emails;
1211 my $email = ($conf->exists('business-onlinepayment-email-override'))
1212 ? $conf->config('business-onlinepayment-email-override')
1213 : $invoicing_list[0];
1217 $content{email_customer} =
1218 ( $conf->exists('business-onlinepayment-email_customer')
1219 || $conf->exists('business-onlinepayment-email-override') );
1222 # run transaction(s)
1226 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1227 $self->_bop_options(\%options),
1230 $transaction->reference({ %options });
1232 $transaction->content(
1234 $self->_bop_auth(\%options),
1235 'action' => 'Post Authorization',
1236 'description' => $options{'description'},
1237 'amount' => $cust_pay_pending->paid,
1238 #'invoice_number' => $options{'invnum'},
1239 'customer_id' => $self->custnum,
1241 #3.0 is a good a time as any to get rid of this... add a config to pass it
1242 # if anyone still needs it
1243 #'referer' => 'http://cleanwhisker.420.am/',
1245 'reference' => $cust_pay_pending->paypendingnum,
1247 'phone' => $self->daytime || $self->night,
1249 # plus whatever is required for bogus capture avoidance
1252 $transaction->submit();
1255 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1257 if ( $options{'apply'} ) {
1258 my $apply_error = $self->apply_payments_and_credits;
1259 if ( $apply_error ) {
1260 warn "WARNING: error applying payment: $apply_error\n";
1265 bill_error => $error,
1266 session_id => $cust_pay_pending->session_id,
1271 =item default_payment_gateway
1273 DEPRECATED -- use agent->payment_gateway
1277 sub default_payment_gateway {
1278 my( $self, $method ) = @_;
1280 die "Real-time processing not enabled\n"
1281 unless $conf->exists('business-onlinepayment');
1283 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1286 my $bop_config = 'business-onlinepayment';
1287 $bop_config .= '-ach'
1288 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1289 my ( $processor, $login, $password, $action, @bop_options ) =
1290 $conf->config($bop_config);
1291 $action ||= 'normal authorization';
1292 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1293 die "No real-time processor is enabled - ".
1294 "did you set the business-onlinepayment configuration value?\n"
1297 ( $processor, $login, $password, $action, @bop_options )
1300 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1302 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1303 via a Business::OnlinePayment realtime gateway. See
1304 L<http://420.am/business-onlinepayment> for supported gateways.
1306 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1308 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1310 Most gateways require a reference to an original payment transaction to refund,
1311 so you probably need to specify a I<paynum>.
1313 I<amount> defaults to the original amount of the payment if not specified.
1315 I<reason> specifies a reason for the refund.
1317 I<paydate> specifies the expiration date for a credit card overriding the
1318 value from the customer record or the payment record. Specified as yyyy-mm-dd
1320 Implementation note: If I<amount> is unspecified or equal to the amount of the
1321 orignal payment, first an attempt is made to "void" the transaction via
1322 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1323 the normal attempt is made to "refund" ("credit") the transaction via the
1324 gateway is attempted. No attempt to "void" the transaction is made if the
1325 gateway has introspection data and doesn't support void.
1327 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1328 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1329 #if set, will override the value from the customer record.
1331 #If an I<invnum> is specified, this payment (if successful) is applied to the
1332 #specified invoice. If you don't specify an I<invnum> you might want to
1333 #call the B<apply_payments> method.
1337 #some false laziness w/realtime_bop, not enough to make it worth merging
1338 #but some useful small subs should be pulled out
1339 sub realtime_refund_bop {
1342 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1345 if (ref($_[0]) eq 'HASH') {
1346 %options = %{$_[0]};
1350 $options{method} = $method;
1354 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1355 warn " $_ => $options{$_}\n" foreach keys %options;
1359 # look up the original payment and optionally a gateway for that payment
1363 my $amount = $options{'amount'};
1365 my( $processor, $login, $password, @bop_options, $namespace ) ;
1366 my( $auth, $order_number ) = ( '', '', '' );
1367 my $gatewaynum = '';
1369 if ( $options{'paynum'} ) {
1371 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1372 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1373 or return "Unknown paynum $options{'paynum'}";
1374 $amount ||= $cust_pay->paid;
1376 if ( $cust_pay->get('processor') ) {
1377 ($gatewaynum, $processor, $auth, $order_number) =
1379 $cust_pay->gatewaynum,
1380 $cust_pay->processor,
1382 $cust_pay->order_number,
1385 # this payment wasn't upgraded, which probably means this won't work,
1387 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1388 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1389 $cust_pay->paybatch;
1390 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1393 if ( $gatewaynum ) { #gateway for the payment to be refunded
1395 my $payment_gateway =
1396 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1397 die "payment gateway $gatewaynum not found"
1398 unless $payment_gateway;
1400 $processor = $payment_gateway->gateway_module;
1401 $login = $payment_gateway->gateway_username;
1402 $password = $payment_gateway->gateway_password;
1403 $namespace = $payment_gateway->gateway_namespace;
1404 @bop_options = $payment_gateway->options;
1406 } else { #try the default gateway
1409 my $payment_gateway =
1410 $self->agent->payment_gateway('method' => $options{method});
1412 ( $conf_processor, $login, $password, $namespace ) =
1413 map { my $method = "gateway_$_"; $payment_gateway->$method }
1414 qw( module username password namespace );
1416 @bop_options = $payment_gateway->gatewaynum
1417 ? $payment_gateway->options
1418 : @{ $payment_gateway->get('options') };
1420 return "processor of payment $options{'paynum'} $processor does not".
1421 " match default processor $conf_processor"
1422 unless $processor eq $conf_processor;
1427 } else { # didn't specify a paynum, so look for agent gateway overrides
1428 # like a normal transaction
1430 my $payment_gateway =
1431 $self->agent->payment_gateway( 'method' => $options{method},
1432 #'payinfo' => $payinfo,
1434 my( $processor, $login, $password, $namespace ) =
1435 map { my $method = "gateway_$_"; $payment_gateway->$method }
1436 qw( module username password namespace );
1438 my @bop_options = $payment_gateway->gatewaynum
1439 ? $payment_gateway->options
1440 : @{ $payment_gateway->get('options') };
1443 return "neither amount nor paynum specified" unless $amount;
1445 eval "use $namespace";
1449 'type' => $options{method},
1451 'password' => $password,
1452 'order_number' => $order_number,
1453 'amount' => $amount,
1455 #3.0 is a good a time as any to get rid of this... add a config to pass it
1456 # if anyone still needs it
1457 #'referer' => 'http://cleanwhisker.420.am/',
1459 $content{authorization} = $auth
1460 if length($auth); #echeck/ACH transactions have an order # but no auth
1461 #(at least with authorize.net)
1463 my $currency = $conf->exists('business-onlinepayment-currency')
1464 && $conf->config('business-onlinepayment-currency');
1465 $content{currency} = $currency if $currency;
1467 my $disable_void_after;
1468 if ($conf->exists('disable_void_after')
1469 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1470 $disable_void_after = $1;
1473 #first try void if applicable
1474 my $void = new Business::OnlinePayment( $processor, @bop_options );
1477 if ($void->can('info')) {
1479 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1480 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1481 my %supported_actions = $void->info('supported_actions');
1483 if ( %supported_actions && $paytype
1484 && defined($supported_actions{$paytype})
1485 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1488 if ( $cust_pay && $cust_pay->paid == $amount
1490 ( not defined($disable_void_after) )
1491 || ( time < ($cust_pay->_date + $disable_void_after ) )
1495 warn " attempting void\n" if $DEBUG > 1;
1496 if ( $void->can('info') ) {
1497 if ( $cust_pay->payby eq 'CARD'
1498 && $void->info('CC_void_requires_card') )
1500 $content{'card_number'} = $cust_pay->payinfo;
1501 } elsif ( $cust_pay->payby eq 'CHEK'
1502 && $void->info('ECHECK_void_requires_account') )
1504 ( $content{'account_number'}, $content{'routing_code'} ) =
1505 split('@', $cust_pay->payinfo);
1506 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1509 $void->content( 'action' => 'void', %content );
1510 $void->test_transaction(1)
1511 if $conf->exists('business-onlinepayment-test_transaction');
1513 if ( $void->is_success ) {
1514 my $error = $cust_pay->void($options{'reason'});
1516 # gah, even with transactions.
1517 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1518 "error voiding payment: $error";
1522 warn " void successful\n" if $DEBUG > 1;
1527 warn " void unsuccessful, trying refund\n"
1531 my $address = $self->address1;
1532 $address .= ", ". $self->address2 if $self->address2;
1534 my($payname, $payfirst, $paylast);
1535 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1536 $payname = $self->payname;
1537 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1538 or return "Illegal payname $payname";
1539 ($payfirst, $paylast) = ($1, $2);
1541 $payfirst = $self->getfield('first');
1542 $paylast = $self->getfield('last');
1543 $payname = "$payfirst $paylast";
1546 my @invoicing_list = $self->invoicing_list_emailonly;
1547 if ( $conf->exists('emailinvoiceautoalways')
1548 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1549 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1550 push @invoicing_list, $self->all_emails;
1553 my $email = ($conf->exists('business-onlinepayment-email-override'))
1554 ? $conf->config('business-onlinepayment-email-override')
1555 : $invoicing_list[0];
1557 my $payip = exists($options{'payip'})
1560 $content{customer_ip} = $payip
1564 if ( $options{method} eq 'CC' ) {
1567 $content{card_number} = $payinfo = $cust_pay->payinfo;
1568 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1569 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1570 ($content{expiration} = "$2/$1"); # where available
1572 $content{card_number} = $payinfo = $self->payinfo;
1573 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1574 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1575 $content{expiration} = "$2/$1";
1578 } elsif ( $options{method} eq 'ECHECK' ) {
1581 $payinfo = $cust_pay->payinfo;
1583 $payinfo = $self->payinfo;
1585 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1586 $content{bank_name} = $self->payname;
1587 $content{account_type} = 'CHECKING';
1588 $content{account_name} = $payname;
1589 $content{customer_org} = $self->company ? 'B' : 'I';
1590 $content{customer_ssn} = $self->ss;
1591 } elsif ( $options{method} eq 'LEC' ) {
1592 $content{phone} = $payinfo = $self->payinfo;
1596 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1597 my %sub_content = $refund->content(
1598 'action' => 'credit',
1599 'customer_id' => $self->custnum,
1600 'last_name' => $paylast,
1601 'first_name' => $payfirst,
1603 'address' => $address,
1604 'city' => $self->city,
1605 'state' => $self->state,
1606 'zip' => $self->zip,
1607 'country' => $self->country,
1609 'phone' => $self->daytime || $self->night,
1612 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1614 $refund->test_transaction(1)
1615 if $conf->exists('business-onlinepayment-test_transaction');
1618 return "$processor error: ". $refund->error_message
1619 unless $refund->is_success();
1621 $order_number = $refund->order_number if $refund->can('order_number');
1623 # change this to just use $cust_pay->delete_cust_bill_pay?
1624 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1625 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1626 last unless @cust_bill_pay;
1627 my $cust_bill_pay = pop @cust_bill_pay;
1628 my $error = $cust_bill_pay->delete;
1632 my $cust_refund = new FS::cust_refund ( {
1633 'custnum' => $self->custnum,
1634 'paynum' => $options{'paynum'},
1635 'refund' => $amount,
1637 'payby' => $bop_method2payby{$options{method}},
1638 'payinfo' => $payinfo,
1639 'reason' => $options{'reason'} || 'card or ACH refund',
1640 'gatewaynum' => $gatewaynum, # may be null
1641 'processor' => $processor,
1642 'auth' => $refund->authorization,
1643 'order_number' => $order_number,
1645 my $error = $cust_refund->insert;
1647 $cust_refund->paynum(''); #try again with no specific paynum
1648 my $error2 = $cust_refund->insert;
1650 # gah, even with transactions.
1651 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1652 "error inserting refund ($processor): $error2".
1653 " (previously tried insert with paynum #$options{'paynum'}" .
1672 L<FS::cust_main>, L<FS::cust_main::Billing>