1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
8 use Business::CreditCard 0.28;
10 use FS::Record qw( qsearch qsearchs );
13 use FS::cust_pay_pending;
14 use FS::cust_bill_pay;
18 $realtime_bop_decline_quiet = 0;
20 # 1 is mostly method/subroutine entry and options
21 # 2 traces progress of some operations
22 # 3 is even more information including possibly sensitive data
24 $me = '[FS::cust_main::Billing_Realtime]';
27 our $BOP_TESTING_SUCCESS = 1;
29 install_callback FS::UID sub {
31 #yes, need it for stuff below (prolly should be cached)
36 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
42 These methods are available on FS::cust_main objects.
48 =item realtime_cust_payby
52 sub realtime_cust_payby {
53 my( $self, %options ) = @_;
55 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
57 $options{amount} = $self->balance unless exists( $options{amount} );
59 my @cust_payby = $self->cust_payby('CARD','CHEK');
62 foreach my $cust_payby (@cust_payby) {
63 $error = $cust_payby->realtime_bop( %options, );
67 #XXX what about the earlier errors?
73 =item realtime_collect [ OPTION => VALUE ... ]
75 Attempt to collect the customer's current balance with a realtime credit
76 card, electronic check, or phone bill transaction (see realtime_bop() below).
78 Returns the result of realtime_bop(): nothing, an error message, or a
79 hashref of state information for a third-party transaction.
81 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
83 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
84 then it is deduced from the customer record.
86 If no I<amount> is specified, then the customer balance is used.
88 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
89 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
90 if set, will override the value from the customer record.
92 I<description> is a free-text field passed to the gateway. It defaults to
93 the value defined by the business-onlinepayment-description configuration
94 option, or "Internet services" if that is unset.
96 If an I<invnum> is specified, this payment (if successful) is applied to the
99 I<apply> will automatically apply a resulting payment.
101 I<quiet> can be set true to suppress email decline notices.
103 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
104 resulting paynum, if any.
106 I<payunique> is a unique identifier for this payment.
108 I<session_id> is a session identifier associated with this payment.
110 I<depend_jobnum> allows payment capture to unlock export jobs
114 sub realtime_collect {
115 my( $self, %options ) = @_;
117 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
120 warn "$me realtime_collect:\n";
121 warn " $_ => $options{$_}\n" foreach keys %options;
124 $options{amount} = $self->balance unless exists( $options{amount} );
125 $options{method} = FS::payby->payby2bop($self->payby)
126 unless exists( $options{method} );
128 return $self->realtime_bop({%options});
132 =item realtime_bop { [ ARG => VALUE ... ] }
134 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
135 via a Business::OnlinePayment realtime gateway. See
136 L<http://420.am/business-onlinepayment> for supported gateways.
138 Required arguments in the hashref are I<method>, and I<amount>
140 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
142 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
144 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
145 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
146 if set, will override the value from the customer record.
148 I<description> is a free-text field passed to the gateway. It defaults to
149 the value defined by the business-onlinepayment-description configuration
150 option, or "Internet services" if that is unset.
152 If an I<invnum> is specified, this payment (if successful) is applied to the
153 specified invoice. If the customer has exactly one open invoice, that
154 invoice number will be assumed. If you don't specify an I<invnum> you might
155 want to call the B<apply_payments> method or set the I<apply> option.
157 I<apply> can be set to true to apply a resulting payment.
159 I<quiet> can be set true to surpress email decline notices.
161 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
162 resulting paynum, if any.
164 I<payunique> is a unique identifier for this payment.
166 I<session_id> is a session identifier associated with this payment.
168 I<depend_jobnum> allows payment capture to unlock export jobs
170 I<discount_term> attempts to take a discount by prepaying for discount_term.
171 The payment will fail if I<amount> is incorrect for this discount term.
173 A direct (Business::OnlinePayment) transaction will return nothing on success,
174 or an error message on failure.
176 A third-party transaction will return a hashref containing:
178 - popup_url: the URL to which a browser should be redirected to complete
180 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
181 - reference: a reference ID for the transaction, to show the customer.
183 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
187 # some helper routines
188 sub _bop_recurring_billing {
189 my( $self, %opt ) = @_;
191 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
193 if ( defined($method) && $method eq 'transaction_is_recur' ) {
195 return 1 if $opt{'trans_is_recur'};
199 # return 1 if the payinfo has been used for another payment
200 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
208 sub _payment_gateway {
209 my ($self, $options) = @_;
211 if ( $options->{'selfservice'} ) {
212 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
214 return $options->{payment_gateway} ||=
215 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
219 if ( $options->{'fake_gatewaynum'} ) {
220 $options->{payment_gateway} =
221 qsearchs('payment_gateway',
222 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
226 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
227 unless exists($options->{payment_gateway});
229 $options->{payment_gateway};
233 my ($self, $options) = @_;
236 'login' => $options->{payment_gateway}->gateway_username,
237 'password' => $options->{payment_gateway}->gateway_password,
242 my ($self, $options) = @_;
244 $options->{payment_gateway}->gatewaynum
245 ? $options->{payment_gateway}->options
246 : @{ $options->{payment_gateway}->get('options') };
251 my ($self, $options) = @_;
253 unless ( $options->{'description'} ) {
254 if ( $conf->exists('business-onlinepayment-description') ) {
255 my $dtempl = $conf->config('business-onlinepayment-description');
257 my $agent = $self->agent->agent;
259 $options->{'description'} = eval qq("$dtempl");
261 $options->{'description'} = 'Internet services';
265 unless ( exists( $options->{'payinfo'} ) ) {
266 $options->{'payinfo'} = $self->payinfo;
267 $options->{'paymask'} = $self->paymask;
270 # Default invoice number if the customer has exactly one open invoice.
271 if( ! $options->{'invnum'} ) {
272 $options->{'invnum'} = '';
273 my @open = $self->open_cust_bill;
274 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
277 $options->{payname} = $self->payname unless exists( $options->{payname} );
281 my ($self, $options) = @_;
284 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
285 $content{customer_ip} = $payip if length($payip);
287 $content{invoice_number} = $options->{'invnum'}
288 if exists($options->{'invnum'}) && length($options->{'invnum'});
290 $content{email_customer} =
291 ( $conf->exists('business-onlinepayment-email_customer')
292 || $conf->exists('business-onlinepayment-email-override') );
294 my ($payname, $payfirst, $paylast);
295 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
296 ($payname = $options->{payname}) =~
297 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
298 or return "Illegal payname $payname";
299 ($payfirst, $paylast) = ($1, $2);
301 $payfirst = $self->getfield('first');
302 $paylast = $self->getfield('last');
303 $payname = "$payfirst $paylast";
306 $content{last_name} = $paylast;
307 $content{first_name} = $payfirst;
309 $content{name} = $payname;
311 $content{address} = exists($options->{'address1'})
312 ? $options->{'address1'}
314 my $address2 = exists($options->{'address2'})
315 ? $options->{'address2'}
317 $content{address} .= ", ". $address2 if length($address2);
319 $content{city} = exists($options->{city})
322 $content{state} = exists($options->{state})
325 $content{zip} = exists($options->{zip})
328 $content{country} = exists($options->{country})
329 ? $options->{country}
332 $content{phone} = $self->daytime || $self->night;
334 my $currency = $conf->exists('business-onlinepayment-currency')
335 && $conf->config('business-onlinepayment-currency');
336 $content{currency} = $currency if $currency;
341 my %bop_method2payby = (
351 confess "Can't call realtime_bop within another transaction ".
352 '($FS::UID::AutoCommit is false)'
353 unless $FS::UID::AutoCommit;
355 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
358 if (ref($_[0]) eq 'HASH') {
361 my ( $method, $amount ) = ( shift, shift );
363 $options{method} = $method;
364 $options{amount} = $amount;
369 # optional credit card surcharge
372 my $cc_surcharge = 0;
373 my $cc_surcharge_pct = 0;
374 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
375 if $conf->config('credit-card-surcharge-percentage')
376 && $options{method} eq 'CC';
378 # always add cc surcharge if called from event
379 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
380 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
381 $options{'amount'} += $cc_surcharge;
382 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
384 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
385 # payment screen), so consider the given
386 # amount as post-surcharge
387 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
390 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
391 $options{'cc_surcharge'} = $cc_surcharge;
395 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
396 warn " cc_surcharge = $cc_surcharge\n";
399 warn " $_ => $options{$_}\n" foreach keys %options;
402 return $self->fake_bop(\%options) if $options{'fake'};
404 $self->_bop_defaults(\%options);
407 # set trans_is_recur based on invnum if there is one
410 my $trans_is_recur = 0;
411 if ( $options{'invnum'} ) {
413 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
414 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
420 $cust_bill->cust_bill_pkg;
423 if grep { $_->freq ne '0' } @part_pkg;
431 my $payment_gateway = $self->_payment_gateway( \%options );
432 my $namespace = $payment_gateway->gateway_namespace;
434 eval "use $namespace";
438 # check for banned credit card/ACH
441 my $ban = FS::banned_pay->ban_search(
442 'payby' => $bop_method2payby{$options{method}},
443 'payinfo' => $options{payinfo},
445 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
448 # check for term discount validity
451 my $discount_term = $options{discount_term};
452 if ( $discount_term ) {
453 my $bill = ($self->cust_bill)[-1]
454 or return "Can't apply a term discount to an unbilled customer";
455 my $plan = FS::discount_plan->new(
457 months => $discount_term
458 ) or return "No discount available for term '$discount_term'";
460 if ( $plan->discounted_total != $options{amount} ) {
461 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
469 my $bop_content = $self->_bop_content(\%options);
470 return $bop_content unless ref($bop_content);
472 my @invoicing_list = $self->invoicing_list_emailonly;
473 if ( $conf->exists('emailinvoiceautoalways')
474 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
475 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
476 push @invoicing_list, $self->all_emails;
479 my $email = ($conf->exists('business-onlinepayment-email-override'))
480 ? $conf->config('business-onlinepayment-email-override')
481 : $invoicing_list[0];
486 if ( $namespace eq 'Business::OnlinePayment' ) {
488 if ( $options{method} eq 'CC' ) {
490 $content{card_number} = $options{payinfo};
491 $paydate = exists($options{'paydate'})
492 ? $options{'paydate'}
494 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
495 $content{expiration} = "$2/$1";
497 my $paycvv = exists($options{'paycvv'})
500 $content{cvv2} = $paycvv
503 my $paystart_month = exists($options{'paystart_month'})
504 ? $options{'paystart_month'}
505 : $self->paystart_month;
507 my $paystart_year = exists($options{'paystart_year'})
508 ? $options{'paystart_year'}
509 : $self->paystart_year;
511 $content{card_start} = "$paystart_month/$paystart_year"
512 if $paystart_month && $paystart_year;
514 my $payissue = exists($options{'payissue'})
515 ? $options{'payissue'}
517 $content{issue_number} = $payissue if $payissue;
519 if ( $self->_bop_recurring_billing(
520 'payinfo' => $options{'payinfo'},
521 'trans_is_recur' => $trans_is_recur,
525 $content{recurring_billing} = 'YES';
526 $content{acct_code} = 'rebill'
527 if $conf->exists('credit_card-recurring_billing_acct_code');
530 } elsif ( $options{method} eq 'ECHECK' ){
532 ( $content{account_number}, $content{routing_code} ) =
533 split('@', $options{payinfo});
534 $content{bank_name} = $options{payname};
535 $content{bank_state} = exists($options{'paystate'})
536 ? $options{'paystate'}
537 : $self->getfield('paystate');
538 $content{account_type}=
539 (exists($options{'paytype'}) && $options{'paytype'})
540 ? uc($options{'paytype'})
541 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
543 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
544 $content{account_name} = $self->company;
546 $content{account_name} = $self->getfield('first'). ' '.
547 $self->getfield('last');
550 $content{customer_org} = $self->company ? 'B' : 'I';
551 $content{state_id} = exists($options{'stateid'})
552 ? $options{'stateid'}
553 : $self->getfield('stateid');
554 $content{state_id_state} = exists($options{'stateid_state'})
555 ? $options{'stateid_state'}
556 : $self->getfield('stateid_state');
557 $content{customer_ssn} = exists($options{'ss'})
561 } elsif ( $options{method} eq 'LEC' ) {
562 $content{phone} = $options{payinfo};
564 die "unknown method ". $options{method};
567 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
570 die "unknown namespace $namespace";
577 my $balance = exists( $options{'balance'} )
578 ? $options{'balance'}
581 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
582 $self->select_for_update; #mutex ... just until we get our pending record in
583 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
585 #the checks here are intended to catch concurrent payments
586 #double-form-submission prevention is taken care of in cust_pay_pending::check
589 return "The customer's balance has changed; $options{method} transaction aborted."
590 if $self->balance < $balance;
592 #also check and make sure there aren't *other* pending payments for this cust
594 my @pending = qsearch('cust_pay_pending', {
595 'custnum' => $self->custnum,
596 'status' => { op=>'!=', value=>'done' }
599 #for third-party payments only, remove pending payments if they're in the
600 #'thirdparty' (waiting for customer action) state.
601 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
602 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
603 my $error = $_->delete;
604 warn "error deleting unfinished third-party payment ".
605 $_->paypendingnum . ": $error\n"
608 @pending = grep { $_->status ne 'thirdparty' } @pending;
611 return "A payment is already being processed for this customer (".
612 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
613 "); $options{method} transaction aborted."
616 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
618 my $cust_pay_pending = new FS::cust_pay_pending {
619 'custnum' => $self->custnum,
620 'paid' => $options{amount},
622 'payby' => $bop_method2payby{$options{method}},
623 'payinfo' => $options{payinfo},
624 'paymask' => $options{paymask},
625 'paydate' => $paydate,
626 'recurring_billing' => $content{recurring_billing},
627 'pkgnum' => $options{'pkgnum'},
629 'gatewaynum' => $payment_gateway->gatewaynum || '',
630 'session_id' => $options{session_id} || '',
631 'jobnum' => $options{depend_jobnum} || '',
633 $cust_pay_pending->payunique( $options{payunique} )
634 if defined($options{payunique}) && length($options{payunique});
636 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
638 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
639 return $cpp_new_err if $cpp_new_err;
641 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
643 warn Dumper($cust_pay_pending) if $DEBUG > 2;
645 my( $action1, $action2 ) =
646 split( /\s*\,\s*/, $payment_gateway->gateway_action );
648 my $transaction = new $namespace( $payment_gateway->gateway_module,
649 $self->_bop_options(\%options),
652 $transaction->content(
653 'type' => $options{method},
654 $self->_bop_auth(\%options),
655 'action' => $action1,
656 'description' => $options{'description'},
657 'amount' => $options{amount},
658 #'invoice_number' => $options{'invnum'},
659 'customer_id' => $self->custnum,
661 'reference' => $cust_pay_pending->paypendingnum, #for now
662 'callback_url' => $payment_gateway->gateway_callback_url,
663 'cancel_url' => $payment_gateway->gateway_cancel_url,
668 $cust_pay_pending->status('pending');
669 my $cpp_pending_err = $cust_pay_pending->replace;
670 return $cpp_pending_err if $cpp_pending_err;
672 warn Dumper($transaction) if $DEBUG > 2;
674 unless ( $BOP_TESTING ) {
675 $transaction->test_transaction(1)
676 if $conf->exists('business-onlinepayment-test_transaction');
677 $transaction->submit();
679 if ( $BOP_TESTING_SUCCESS ) {
680 $transaction->is_success(1);
681 $transaction->authorization('fake auth');
683 $transaction->is_success(0);
684 $transaction->error_message('fake failure');
688 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
690 $cust_pay_pending->status('thirdparty');
691 my $cpp_err = $cust_pay_pending->replace;
692 return { error => $cpp_err } if $cpp_err;
693 return { reference => $cust_pay_pending->paypendingnum,
694 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
696 } elsif ( $transaction->is_success() && $action2 ) {
698 $cust_pay_pending->status('authorized');
699 my $cpp_authorized_err = $cust_pay_pending->replace;
700 return $cpp_authorized_err if $cpp_authorized_err;
702 my $auth = $transaction->authorization;
703 my $ordernum = $transaction->can('order_number')
704 ? $transaction->order_number
708 new Business::OnlinePayment( $payment_gateway->gateway_module,
709 $self->_bop_options(\%options),
714 type => $options{method},
716 $self->_bop_auth(\%options),
717 order_number => $ordernum,
718 amount => $options{amount},
719 authorization => $auth,
720 description => $options{'description'},
723 foreach my $field (qw( authorization_source_code returned_ACI
724 transaction_identifier validation_code
725 transaction_sequence_num local_transaction_date
726 local_transaction_time AVS_result_code )) {
727 $capture{$field} = $transaction->$field() if $transaction->can($field);
730 $capture->content( %capture );
732 $capture->test_transaction(1)
733 if $conf->exists('business-onlinepayment-test_transaction');
736 unless ( $capture->is_success ) {
737 my $e = "Authorization successful but capture failed, custnum #".
738 $self->custnum. ': '. $capture->result_code.
739 ": ". $capture->error_message;
747 # remove paycvv after initial transaction
750 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
751 if ( length($self->paycvv)
752 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
754 my $error = $self->remove_cvv;
756 warn "WARNING: error removing cvv: $error\n";
765 if ( $transaction->can('card_token') && $transaction->card_token ) {
767 if ( $options{'payinfo'} eq $self->payinfo ) {
768 $self->payinfo($transaction->card_token);
769 my $error = $self->replace;
771 warn "WARNING: error storing token: $error, but proceeding anyway\n";
781 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
793 if (ref($_[0]) eq 'HASH') {
796 my ( $method, $amount ) = ( shift, shift );
798 $options{method} = $method;
799 $options{amount} = $amount;
802 if ( $options{'fake_failure'} ) {
803 return "Error: No error; test failure requested with fake_failure";
806 my $cust_pay = new FS::cust_pay ( {
807 'custnum' => $self->custnum,
808 'invnum' => $options{'invnum'},
809 'paid' => $options{amount},
811 'payby' => $bop_method2payby{$options{method}},
812 #'payinfo' => $payinfo,
813 'payinfo' => '4111111111111111',
814 #'paydate' => $paydate,
815 'paydate' => '2012-05-01',
816 'processor' => 'FakeProcessor',
818 'order_number' => '32',
820 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
823 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
824 warn " $_ => $options{$_}\n" foreach keys %options;
827 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
830 $cust_pay->invnum(''); #try again with no specific invnum
831 my $error2 = $cust_pay->insert( $options{'manual'} ?
832 ( 'manual' => 1 ) : ()
835 # gah, even with transactions.
836 my $e = 'WARNING: Card/ACH debited but database not updated - '.
837 "error inserting (fake!) payment: $error2".
838 " (previously tried insert with invnum #$options{'invnum'}" .
845 if ( $options{'paynum_ref'} ) {
846 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
854 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
856 # Wraps up processing of a realtime credit card, ACH (electronic check) or
857 # phone bill transaction.
859 sub _realtime_bop_result {
860 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
862 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
865 warn "$me _realtime_bop_result: pending transaction ".
866 $cust_pay_pending->paypendingnum. "\n";
867 warn " $_ => $options{$_}\n" foreach keys %options;
870 my $payment_gateway = $options{payment_gateway}
871 or return "no payment gateway in arguments to _realtime_bop_result";
873 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
874 my $cpp_captured_err = $cust_pay_pending->replace;
875 return $cpp_captured_err if $cpp_captured_err;
877 if ( $transaction->is_success() ) {
879 my $order_number = $transaction->order_number
880 if $transaction->can('order_number');
882 my $cust_pay = new FS::cust_pay ( {
883 'custnum' => $self->custnum,
884 'invnum' => $options{'invnum'},
885 'paid' => $cust_pay_pending->paid,
887 'payby' => $cust_pay_pending->payby,
888 'payinfo' => $options{'payinfo'},
889 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
890 'paydate' => $cust_pay_pending->paydate,
891 'pkgnum' => $cust_pay_pending->pkgnum,
892 'discount_term' => $options{'discount_term'},
893 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
894 'processor' => $payment_gateway->gateway_module,
895 'auth' => $transaction->authorization,
896 'order_number' => $order_number || '',
899 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
900 $cust_pay->payunique( $options{payunique} )
901 if defined($options{payunique}) && length($options{payunique});
903 my $oldAutoCommit = $FS::UID::AutoCommit;
904 local $FS::UID::AutoCommit = 0;
907 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
909 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
912 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
913 $cust_pay->invnum(''); #try again with no specific invnum
914 $cust_pay->paynum('');
915 my $error2 = $cust_pay->insert( $options{'manual'} ?
916 ( 'manual' => 1 ) : ()
919 # gah. but at least we have a record of the state we had to abort in
920 # from cust_pay_pending now.
921 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
922 my $e = "WARNING: $options{method} captured but payment not recorded -".
923 " error inserting payment (". $payment_gateway->gateway_module.
925 " (previously tried insert with invnum #$options{'invnum'}" .
926 ": $error ) - pending payment saved as paypendingnum ".
927 $cust_pay_pending->paypendingnum. "\n";
933 my $jobnum = $cust_pay_pending->jobnum;
935 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
937 unless ( $placeholder ) {
938 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
939 my $e = "WARNING: $options{method} captured but job $jobnum not ".
940 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
945 $error = $placeholder->delete;
948 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
949 my $e = "WARNING: $options{method} captured but could not delete ".
950 "job $jobnum for paypendingnum ".
951 $cust_pay_pending->paypendingnum. ": $error\n";
956 $cust_pay_pending->set('jobnum','');
960 if ( $options{'paynum_ref'} ) {
961 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
964 $cust_pay_pending->status('done');
965 $cust_pay_pending->statustext('captured');
966 $cust_pay_pending->paynum($cust_pay->paynum);
967 my $cpp_done_err = $cust_pay_pending->replace;
969 if ( $cpp_done_err ) {
971 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
972 my $e = "WARNING: $options{method} captured but payment not recorded - ".
973 "error updating status for paypendingnum ".
974 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
980 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
982 if ( $options{'apply'} ) {
983 my $apply_error = $self->apply_payments_and_credits;
984 if ( $apply_error ) {
985 warn "WARNING: error applying payment: $apply_error\n";
986 #but we still should return no error cause the payment otherwise went
991 # have a CC surcharge portion --> one-time charge
992 if ( $options{'cc_surcharge'} > 0 ) {
993 # XXX: this whole block needs to be in a transaction?
996 $invnum = $options{'invnum'} if $options{'invnum'};
997 unless ( $invnum ) { # probably from a payment screen
998 # do we have any open invoices? pick earliest
999 # uses the fact that cust_main->cust_bill sorts by date ascending
1000 my @open = $self->open_cust_bill;
1001 $invnum = $open[0]->invnum if scalar(@open);
1004 unless ( $invnum ) { # still nothing? pick last closed invoice
1005 # again uses fact that cust_main->cust_bill sorts by date ascending
1006 my @closed = $self->cust_bill;
1007 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1010 unless ( $invnum ) {
1011 # XXX: unlikely case - pre-paying before any invoices generated
1012 # what it should do is create a new invoice and pick it
1013 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1018 my $charge_error = $self->charge({
1019 'amount' => $options{'cc_surcharge'},
1020 'pkg' => 'Credit Card Surcharge',
1022 'cust_pkg_ref' => \$cust_pkg,
1025 warn 'Unable to add CC surcharge cust_pkg';
1029 $cust_pkg->setup(time);
1030 my $cp_error = $cust_pkg->replace;
1032 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1036 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1037 unless ( $cust_bill ) {
1038 warn "race condition + invoice deletion just happened";
1043 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1045 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1049 return ''; #no error
1055 my $perror = $transaction->error_message;
1056 #$payment_gateway->gateway_module. " error: ".
1057 # removed for conciseness
1059 my $jobnum = $cust_pay_pending->jobnum;
1061 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1063 if ( $placeholder ) {
1064 my $error = $placeholder->depended_delete;
1065 $error ||= $placeholder->delete;
1066 $cust_pay_pending->set('jobnum','');
1067 warn "error removing provisioning jobs after declined paypendingnum ".
1068 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1070 my $e = "error finding job $jobnum for declined paypendingnum ".
1071 $cust_pay_pending->paypendingnum. "\n";
1077 unless ( $transaction->error_message ) {
1080 if ( $transaction->can('response_page') ) {
1082 'page' => ( $transaction->can('response_page')
1083 ? $transaction->response_page
1086 'code' => ( $transaction->can('response_code')
1087 ? $transaction->response_code
1090 'headers' => ( $transaction->can('response_headers')
1091 ? $transaction->response_headers
1097 "No additional debugging information available for ".
1098 $payment_gateway->gateway_module;
1101 $perror .= "No error_message returned from ".
1102 $payment_gateway->gateway_module. " -- ".
1103 ( ref($t_response) ? Dumper($t_response) : $t_response );
1107 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1108 && $conf->exists('emaildecline', $self->agentnum)
1109 && grep { $_ ne 'POST' } $self->invoicing_list
1110 && ! grep { $transaction->error_message =~ /$_/ }
1111 $conf->config('emaildecline-exclude', $self->agentnum)
1114 # Send a decline alert to the customer.
1115 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1118 # include the raw error message in the transaction state
1119 $cust_pay_pending->setfield('error', $transaction->error_message);
1120 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1121 $error = $msg_template->send( 'cust_main' => $self,
1122 'object' => $cust_pay_pending );
1126 $perror .= " (also received error sending decline notification: $error)"
1131 $cust_pay_pending->status('done');
1132 $cust_pay_pending->statustext($perror);
1133 #'declined:': no, that's failure_status
1134 if ( $transaction->can('failure_status') ) {
1135 $cust_pay_pending->failure_status( $transaction->failure_status );
1137 my $cpp_done_err = $cust_pay_pending->replace;
1138 if ( $cpp_done_err ) {
1139 my $e = "WARNING: $options{method} declined but pending payment not ".
1140 "resolved - error updating status for paypendingnum ".
1141 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1143 $perror = "$e ($perror)";
1151 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1153 Verifies successful third party processing of a realtime credit card,
1154 ACH (electronic check) or phone bill transaction via a
1155 Business::OnlineThirdPartyPayment realtime gateway. See
1156 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1158 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1160 The additional options I<payname>, I<city>, I<state>,
1161 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1162 if set, will override the value from the customer record.
1164 I<description> is a free-text field passed to the gateway. It defaults to
1165 "Internet services".
1167 If an I<invnum> is specified, this payment (if successful) is applied to the
1168 specified invoice. If you don't specify an I<invnum> you might want to
1169 call the B<apply_payments> method.
1171 I<quiet> can be set true to surpress email decline notices.
1173 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1174 resulting paynum, if any.
1176 I<payunique> is a unique identifier for this payment.
1178 Returns a hashref containing elements bill_error (which will be undefined
1179 upon success) and session_id of any associated session.
1183 sub realtime_botpp_capture {
1184 my( $self, $cust_pay_pending, %options ) = @_;
1186 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1189 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1190 warn " $_ => $options{$_}\n" foreach keys %options;
1193 eval "use Business::OnlineThirdPartyPayment";
1197 # select the gateway
1200 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1202 my $payment_gateway;
1203 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1204 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1205 { gatewaynum => $gatewaynum }
1207 : $self->agent->payment_gateway( 'method' => $method,
1208 # 'invnum' => $cust_pay_pending->invnum,
1209 # 'payinfo' => $cust_pay_pending->payinfo,
1212 $options{payment_gateway} = $payment_gateway; # for the helper subs
1218 my @invoicing_list = $self->invoicing_list_emailonly;
1219 if ( $conf->exists('emailinvoiceautoalways')
1220 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1221 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1222 push @invoicing_list, $self->all_emails;
1225 my $email = ($conf->exists('business-onlinepayment-email-override'))
1226 ? $conf->config('business-onlinepayment-email-override')
1227 : $invoicing_list[0];
1231 $content{email_customer} =
1232 ( $conf->exists('business-onlinepayment-email_customer')
1233 || $conf->exists('business-onlinepayment-email-override') );
1236 # run transaction(s)
1240 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1241 $self->_bop_options(\%options),
1244 $transaction->reference({ %options });
1246 $transaction->content(
1248 $self->_bop_auth(\%options),
1249 'action' => 'Post Authorization',
1250 'description' => $options{'description'},
1251 'amount' => $cust_pay_pending->paid,
1252 #'invoice_number' => $options{'invnum'},
1253 'customer_id' => $self->custnum,
1254 'reference' => $cust_pay_pending->paypendingnum,
1256 'phone' => $self->daytime || $self->night,
1258 # plus whatever is required for bogus capture avoidance
1261 $transaction->submit();
1264 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1266 if ( $options{'apply'} ) {
1267 my $apply_error = $self->apply_payments_and_credits;
1268 if ( $apply_error ) {
1269 warn "WARNING: error applying payment: $apply_error\n";
1274 bill_error => $error,
1275 session_id => $cust_pay_pending->session_id,
1280 =item default_payment_gateway
1282 DEPRECATED -- use agent->payment_gateway
1286 sub default_payment_gateway {
1287 my( $self, $method ) = @_;
1289 die "Real-time processing not enabled\n"
1290 unless $conf->exists('business-onlinepayment');
1292 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1295 my $bop_config = 'business-onlinepayment';
1296 $bop_config .= '-ach'
1297 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1298 my ( $processor, $login, $password, $action, @bop_options ) =
1299 $conf->config($bop_config);
1300 $action ||= 'normal authorization';
1301 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1302 die "No real-time processor is enabled - ".
1303 "did you set the business-onlinepayment configuration value?\n"
1306 ( $processor, $login, $password, $action, @bop_options )
1309 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1311 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1312 via a Business::OnlinePayment realtime gateway. See
1313 L<http://420.am/business-onlinepayment> for supported gateways.
1315 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1317 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1319 Most gateways require a reference to an original payment transaction to refund,
1320 so you probably need to specify a I<paynum>.
1322 I<amount> defaults to the original amount of the payment if not specified.
1324 I<reason> specifies a reason for the refund.
1326 I<paydate> specifies the expiration date for a credit card overriding the
1327 value from the customer record or the payment record. Specified as yyyy-mm-dd
1329 Implementation note: If I<amount> is unspecified or equal to the amount of the
1330 orignal payment, first an attempt is made to "void" the transaction via
1331 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1332 the normal attempt is made to "refund" ("credit") the transaction via the
1333 gateway is attempted. No attempt to "void" the transaction is made if the
1334 gateway has introspection data and doesn't support void.
1336 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1337 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1338 #if set, will override the value from the customer record.
1340 #If an I<invnum> is specified, this payment (if successful) is applied to the
1341 #specified invoice. If you don't specify an I<invnum> you might want to
1342 #call the B<apply_payments> method.
1346 #some false laziness w/realtime_bop, not enough to make it worth merging
1347 #but some useful small subs should be pulled out
1348 sub realtime_refund_bop {
1351 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1354 if (ref($_[0]) eq 'HASH') {
1355 %options = %{$_[0]};
1359 $options{method} = $method;
1363 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1364 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 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1388 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1390 if ( $cust_pay->get('processor') ) {
1391 ($gatewaynum, $processor, $auth, $order_number) =
1393 $cust_pay->gatewaynum,
1394 $cust_pay->processor,
1396 $cust_pay->order_number,
1399 # this payment wasn't upgraded, which probably means this won't work,
1401 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1402 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1403 $cust_pay->paybatch;
1404 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1407 if ( $gatewaynum ) { #gateway for the payment to be refunded
1409 my $payment_gateway =
1410 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1411 die "payment gateway $gatewaynum not found"
1412 unless $payment_gateway;
1414 $processor = $payment_gateway->gateway_module;
1415 $login = $payment_gateway->gateway_username;
1416 $password = $payment_gateway->gateway_password;
1417 $namespace = $payment_gateway->gateway_namespace;
1418 @bop_options = $payment_gateway->options;
1420 } else { #try the default gateway
1423 my $payment_gateway =
1424 $self->agent->payment_gateway('method' => $options{method});
1426 ( $conf_processor, $login, $password, $namespace ) =
1427 map { my $method = "gateway_$_"; $payment_gateway->$method }
1428 qw( module username password namespace );
1430 @bop_options = $payment_gateway->gatewaynum
1431 ? $payment_gateway->options
1432 : @{ $payment_gateway->get('options') };
1434 return "processor of payment $options{'paynum'} $processor does not".
1435 " match default processor $conf_processor"
1436 unless $processor eq $conf_processor;
1441 } else { # didn't specify a paynum, so look for agent gateway overrides
1442 # like a normal transaction
1444 my $payment_gateway =
1445 $self->agent->payment_gateway( 'method' => $options{method},
1446 #'payinfo' => $payinfo,
1448 my( $processor, $login, $password, $namespace ) =
1449 map { my $method = "gateway_$_"; $payment_gateway->$method }
1450 qw( module username password namespace );
1452 my @bop_options = $payment_gateway->gatewaynum
1453 ? $payment_gateway->options
1454 : @{ $payment_gateway->get('options') };
1457 return "neither amount nor paynum specified" unless $amount;
1459 eval "use $namespace";
1464 'type' => $options{method},
1466 'password' => $password,
1467 'order_number' => $order_number,
1468 'amount' => $amount,
1470 $content{authorization} = $auth
1471 if length($auth); #echeck/ACH transactions have an order # but no auth
1472 #(at least with authorize.net)
1474 my $currency = $conf->exists('business-onlinepayment-currency')
1475 && $conf->config('business-onlinepayment-currency');
1476 $content{currency} = $currency if $currency;
1478 my $disable_void_after;
1479 if ($conf->exists('disable_void_after')
1480 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1481 $disable_void_after = $1;
1484 #first try void if applicable
1485 my $void = new Business::OnlinePayment( $processor, @bop_options );
1488 if ($void->can('info')) {
1490 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1491 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1492 my %supported_actions = $void->info('supported_actions');
1494 if ( %supported_actions && $paytype
1495 && defined($supported_actions{$paytype})
1496 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1499 if ( $cust_pay && $cust_pay->paid == $amount
1501 ( not defined($disable_void_after) )
1502 || ( time < ($cust_pay->_date + $disable_void_after ) )
1506 warn " attempting void\n" if $DEBUG > 1;
1507 if ( $void->can('info') ) {
1508 if ( $cust_pay->payby eq 'CARD'
1509 && $void->info('CC_void_requires_card') )
1511 $content{'card_number'} = $cust_pay->payinfo;
1512 } elsif ( $cust_pay->payby eq 'CHEK'
1513 && $void->info('ECHECK_void_requires_account') )
1515 ( $content{'account_number'}, $content{'routing_code'} ) =
1516 split('@', $cust_pay->payinfo);
1517 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1520 $void->content( 'action' => 'void', %content );
1521 $void->test_transaction(1)
1522 if $conf->exists('business-onlinepayment-test_transaction');
1524 if ( $void->is_success ) {
1525 my $error = $cust_pay->void($options{'reason'});
1527 # gah, even with transactions.
1528 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1529 "error voiding payment: $error";
1533 warn " void successful\n" if $DEBUG > 1;
1538 warn " void unsuccessful, trying refund\n"
1542 my $address = $self->address1;
1543 $address .= ", ". $self->address2 if $self->address2;
1545 my($payname, $payfirst, $paylast);
1546 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1547 $payname = $self->payname;
1548 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1549 or return "Illegal payname $payname";
1550 ($payfirst, $paylast) = ($1, $2);
1552 $payfirst = $self->getfield('first');
1553 $paylast = $self->getfield('last');
1554 $payname = "$payfirst $paylast";
1557 my @invoicing_list = $self->invoicing_list_emailonly;
1558 if ( $conf->exists('emailinvoiceautoalways')
1559 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1560 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1561 push @invoicing_list, $self->all_emails;
1564 my $email = ($conf->exists('business-onlinepayment-email-override'))
1565 ? $conf->config('business-onlinepayment-email-override')
1566 : $invoicing_list[0];
1568 my $payip = exists($options{'payip'})
1571 $content{customer_ip} = $payip
1575 if ( $options{method} eq 'CC' ) {
1578 $content{card_number} = $payinfo = $cust_pay->payinfo;
1579 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1580 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1581 ($content{expiration} = "$2/$1"); # where available
1583 $content{card_number} = $payinfo = $self->payinfo;
1584 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1585 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1586 $content{expiration} = "$2/$1";
1589 } elsif ( $options{method} eq 'ECHECK' ) {
1592 $payinfo = $cust_pay->payinfo;
1594 $payinfo = $self->payinfo;
1596 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1597 $content{bank_name} = $self->payname;
1598 $content{account_type} = 'CHECKING';
1599 $content{account_name} = $payname;
1600 $content{customer_org} = $self->company ? 'B' : 'I';
1601 $content{customer_ssn} = $self->ss;
1602 } elsif ( $options{method} eq 'LEC' ) {
1603 $content{phone} = $payinfo = $self->payinfo;
1607 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1608 my %sub_content = $refund->content(
1609 'action' => 'credit',
1610 'customer_id' => $self->custnum,
1611 'last_name' => $paylast,
1612 'first_name' => $payfirst,
1614 'address' => $address,
1615 'city' => $self->city,
1616 'state' => $self->state,
1617 'zip' => $self->zip,
1618 'country' => $self->country,
1620 'phone' => $self->daytime || $self->night,
1623 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1625 $refund->test_transaction(1)
1626 if $conf->exists('business-onlinepayment-test_transaction');
1629 return "$processor error: ". $refund->error_message
1630 unless $refund->is_success();
1632 $order_number = $refund->order_number if $refund->can('order_number');
1634 # change this to just use $cust_pay->delete_cust_bill_pay?
1635 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1636 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1637 last unless @cust_bill_pay;
1638 my $cust_bill_pay = pop @cust_bill_pay;
1639 my $error = $cust_bill_pay->delete;
1643 my $cust_refund = new FS::cust_refund ( {
1644 'custnum' => $self->custnum,
1645 'paynum' => $options{'paynum'},
1646 'refund' => $amount,
1648 'payby' => $bop_method2payby{$options{method}},
1649 'payinfo' => $payinfo,
1650 'reason' => $options{'reason'} || 'card or ACH refund',
1651 'gatewaynum' => $gatewaynum, # may be null
1652 'processor' => $processor,
1653 'auth' => $refund->authorization,
1654 'order_number' => $order_number,
1656 my $error = $cust_refund->insert;
1658 $cust_refund->paynum(''); #try again with no specific paynum
1659 my $error2 = $cust_refund->insert;
1661 # gah, even with transactions.
1662 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1663 "error inserting refund ($processor): $error2".
1664 " (previously tried insert with paynum #$options{'paynum'}" .
1683 L<FS::cust_main>, L<FS::cust_main::Billing>