1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
8 use Business::CreditCard 0.28;
10 use FS::Record qw( qsearch qsearchs );
13 use FS::cust_pay_pending;
17 $realtime_bop_decline_quiet = 0;
19 # 1 is mostly method/subroutine entry and options
20 # 2 traces progress of some operations
21 # 3 is even more information including possibly sensitive data
23 $me = '[FS::cust_main::Billing_Realtime]';
26 our $BOP_TESTING_SUCCESS = 1;
28 install_callback FS::UID sub {
30 #yes, need it for stuff below (prolly should be cached)
35 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
41 These methods are available on FS::cust_main objects.
47 =item realtime_cust_payby
51 sub realtime_cust_payby {
52 my( $self, %options ) = @_;
54 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
56 $options{amount} = $self->balance unless exists( $options{amount} );
58 my @cust_payby = qsearch({
59 'table' => 'cust_payby',
60 'hashref' => { 'custnum' => $self->custnum, },
61 'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
62 'order_by' => 'ORDER BY weight ASC',
66 foreach my $cust_payby (@cust_payby) {
67 $error = $cust_payby->realtime_bop( %options, );
71 #XXX what about the earlier errors?
77 =item realtime_collect [ OPTION => VALUE ... ]
79 Attempt to collect the customer's current balance with a realtime credit
80 card, electronic check, or phone bill transaction (see realtime_bop() below).
82 Returns the result of realtime_bop(): nothing, an error message, or a
83 hashref of state information for a third-party transaction.
85 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
87 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
88 then it is deduced from the customer record.
90 If no I<amount> is specified, then the customer balance is used.
92 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
93 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
94 if set, will override the value from the customer record.
96 I<description> is a free-text field passed to the gateway. It defaults to
97 the value defined by the business-onlinepayment-description configuration
98 option, or "Internet services" if that is unset.
100 If an I<invnum> is specified, this payment (if successful) is applied to the
103 I<apply> will automatically apply a resulting payment.
105 I<quiet> can be set true to suppress email decline notices.
107 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
108 resulting paynum, if any.
110 I<payunique> is a unique identifier for this payment.
112 I<session_id> is a session identifier associated with this payment.
114 I<depend_jobnum> allows payment capture to unlock export jobs
118 sub realtime_collect {
119 my( $self, %options ) = @_;
121 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
124 warn "$me realtime_collect:\n";
125 warn " $_ => $options{$_}\n" foreach keys %options;
128 $options{amount} = $self->balance unless exists( $options{amount} );
129 $options{method} = FS::payby->payby2bop($self->payby)
130 unless exists( $options{method} );
132 return $self->realtime_bop({%options});
136 =item realtime_bop { [ ARG => VALUE ... ] }
138 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
139 via a Business::OnlinePayment realtime gateway. See
140 L<http://420.am/business-onlinepayment> for supported gateways.
142 Required arguments in the hashref are I<method>, and I<amount>
144 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
146 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
148 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
149 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
150 if set, will override the value from the customer record.
152 I<description> is a free-text field passed to the gateway. It defaults to
153 the value defined by the business-onlinepayment-description configuration
154 option, or "Internet services" if that is unset.
156 If an I<invnum> is specified, this payment (if successful) is applied to the
157 specified invoice. If the customer has exactly one open invoice, that
158 invoice number will be assumed. If you don't specify an I<invnum> you might
159 want to call the B<apply_payments> method or set the I<apply> option.
161 I<apply> can be set to true to apply a resulting payment.
163 I<quiet> can be set true to surpress email decline notices.
165 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
166 resulting paynum, if any.
168 I<payunique> is a unique identifier for this payment.
170 I<session_id> is a session identifier associated with this payment.
172 I<depend_jobnum> allows payment capture to unlock export jobs
174 I<discount_term> attempts to take a discount by prepaying for discount_term.
175 The payment will fail if I<amount> is incorrect for this discount term.
177 A direct (Business::OnlinePayment) transaction will return nothing on success,
178 or an error message on failure.
180 A third-party transaction will return a hashref containing:
182 - popup_url: the URL to which a browser should be redirected to complete
184 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
185 - reference: a reference ID for the transaction, to show the customer.
187 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
191 # some helper routines
192 sub _bop_recurring_billing {
193 my( $self, %opt ) = @_;
195 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
197 if ( defined($method) && $method eq 'transaction_is_recur' ) {
199 return 1 if $opt{'trans_is_recur'};
203 # return 1 if the payinfo has been used for another payment
204 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
212 sub _payment_gateway {
213 my ($self, $options) = @_;
215 if ( $options->{'selfservice'} ) {
216 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
218 return $options->{payment_gateway} ||=
219 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
223 if ( $options->{'fake_gatewaynum'} ) {
224 $options->{payment_gateway} =
225 qsearchs('payment_gateway',
226 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
230 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
231 unless exists($options->{payment_gateway});
233 $options->{payment_gateway};
237 my ($self, $options) = @_;
240 'login' => $options->{payment_gateway}->gateway_username,
241 'password' => $options->{payment_gateway}->gateway_password,
246 my ($self, $options) = @_;
248 $options->{payment_gateway}->gatewaynum
249 ? $options->{payment_gateway}->options
250 : @{ $options->{payment_gateway}->get('options') };
255 my ($self, $options) = @_;
257 unless ( $options->{'description'} ) {
258 if ( $conf->exists('business-onlinepayment-description') ) {
259 my $dtempl = $conf->config('business-onlinepayment-description');
261 my $agent = $self->agent->agent;
263 $options->{'description'} = eval qq("$dtempl");
265 $options->{'description'} = 'Internet services';
269 unless ( exists( $options->{'payinfo'} ) ) {
270 $options->{'payinfo'} = $self->payinfo;
271 $options->{'paymask'} = $self->paymask;
274 # Default invoice number if the customer has exactly one open invoice.
275 if( ! $options->{'invnum'} ) {
276 $options->{'invnum'} = '';
277 my @open = $self->open_cust_bill;
278 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
281 $options->{payname} = $self->payname unless exists( $options->{payname} );
285 my ($self, $options) = @_;
288 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
289 $content{customer_ip} = $payip if length($payip);
291 $content{invoice_number} = $options->{'invnum'}
292 if exists($options->{'invnum'}) && length($options->{'invnum'});
294 $content{email_customer} =
295 ( $conf->exists('business-onlinepayment-email_customer')
296 || $conf->exists('business-onlinepayment-email-override') );
298 my ($payname, $payfirst, $paylast);
299 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
300 ($payname = $options->{payname}) =~
301 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
302 or return "Illegal payname $payname";
303 ($payfirst, $paylast) = ($1, $2);
305 $payfirst = $self->getfield('first');
306 $paylast = $self->getfield('last');
307 $payname = "$payfirst $paylast";
310 $content{last_name} = $paylast;
311 $content{first_name} = $payfirst;
313 $content{name} = $payname;
315 $content{address} = exists($options->{'address1'})
316 ? $options->{'address1'}
318 my $address2 = exists($options->{'address2'})
319 ? $options->{'address2'}
321 $content{address} .= ", ". $address2 if length($address2);
323 $content{city} = exists($options->{city})
326 $content{state} = exists($options->{state})
329 $content{zip} = exists($options->{zip})
332 $content{country} = exists($options->{country})
333 ? $options->{country}
336 $content{phone} = $self->daytime || $self->night;
338 my $currency = $conf->exists('business-onlinepayment-currency')
339 && $conf->config('business-onlinepayment-currency');
340 $content{currency} = $currency if $currency;
345 my %bop_method2payby = (
355 confess "Can't call realtime_bop within another transaction ".
356 '($FS::UID::AutoCommit is false)'
357 unless $FS::UID::AutoCommit;
359 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
362 if (ref($_[0]) eq 'HASH') {
365 my ( $method, $amount ) = ( shift, shift );
367 $options{method} = $method;
368 $options{amount} = $amount;
373 # optional credit card surcharge
376 my $cc_surcharge = 0;
377 my $cc_surcharge_pct = 0;
378 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
379 if $conf->config('credit-card-surcharge-percentage')
380 && $options{method} eq 'CC';
382 # always add cc surcharge if called from event
383 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
384 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
385 $options{'amount'} += $cc_surcharge;
386 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
388 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
389 # payment screen), so consider the given
390 # amount as post-surcharge
391 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
394 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
395 $options{'cc_surcharge'} = $cc_surcharge;
399 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
400 warn " cc_surcharge = $cc_surcharge\n";
403 warn " $_ => $options{$_}\n" foreach keys %options;
406 return $self->fake_bop(\%options) if $options{'fake'};
408 $self->_bop_defaults(\%options);
411 # set trans_is_recur based on invnum if there is one
414 my $trans_is_recur = 0;
415 if ( $options{'invnum'} ) {
417 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
418 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
424 $cust_bill->cust_bill_pkg;
427 if grep { $_->freq ne '0' } @part_pkg;
435 my $payment_gateway = $self->_payment_gateway( \%options );
436 my $namespace = $payment_gateway->gateway_namespace;
438 eval "use $namespace";
442 # check for banned credit card/ACH
445 my $ban = FS::banned_pay->ban_search(
446 'payby' => $bop_method2payby{$options{method}},
447 'payinfo' => $options{payinfo},
449 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
452 # check for term discount validity
455 my $discount_term = $options{discount_term};
456 if ( $discount_term ) {
457 my $bill = ($self->cust_bill)[-1]
458 or return "Can't apply a term discount to an unbilled customer";
459 my $plan = FS::discount_plan->new(
461 months => $discount_term
462 ) or return "No discount available for term '$discount_term'";
464 if ( $plan->discounted_total != $options{amount} ) {
465 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
473 my $bop_content = $self->_bop_content(\%options);
474 return $bop_content unless ref($bop_content);
476 my @invoicing_list = $self->invoicing_list_emailonly;
477 if ( $conf->exists('emailinvoiceautoalways')
478 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
479 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
480 push @invoicing_list, $self->all_emails;
483 my $email = ($conf->exists('business-onlinepayment-email-override'))
484 ? $conf->config('business-onlinepayment-email-override')
485 : $invoicing_list[0];
490 if ( $namespace eq 'Business::OnlinePayment' ) {
492 if ( $options{method} eq 'CC' ) {
494 $content{card_number} = $options{payinfo};
495 $paydate = exists($options{'paydate'})
496 ? $options{'paydate'}
498 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
499 $content{expiration} = "$2/$1";
501 my $paycvv = exists($options{'paycvv'})
504 $content{cvv2} = $paycvv
507 my $paystart_month = exists($options{'paystart_month'})
508 ? $options{'paystart_month'}
509 : $self->paystart_month;
511 my $paystart_year = exists($options{'paystart_year'})
512 ? $options{'paystart_year'}
513 : $self->paystart_year;
515 $content{card_start} = "$paystart_month/$paystart_year"
516 if $paystart_month && $paystart_year;
518 my $payissue = exists($options{'payissue'})
519 ? $options{'payissue'}
521 $content{issue_number} = $payissue if $payissue;
523 if ( $self->_bop_recurring_billing(
524 'payinfo' => $options{'payinfo'},
525 'trans_is_recur' => $trans_is_recur,
529 $content{recurring_billing} = 'YES';
530 $content{acct_code} = 'rebill'
531 if $conf->exists('credit_card-recurring_billing_acct_code');
534 } elsif ( $options{method} eq 'ECHECK' ){
536 ( $content{account_number}, $content{routing_code} ) =
537 split('@', $options{payinfo});
538 $content{bank_name} = $options{payname};
539 $content{bank_state} = exists($options{'paystate'})
540 ? $options{'paystate'}
541 : $self->getfield('paystate');
542 $content{account_type}=
543 (exists($options{'paytype'}) && $options{'paytype'})
544 ? uc($options{'paytype'})
545 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
547 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
548 $content{account_name} = $self->company;
550 $content{account_name} = $self->getfield('first'). ' '.
551 $self->getfield('last');
554 $content{customer_org} = $self->company ? 'B' : 'I';
555 $content{state_id} = exists($options{'stateid'})
556 ? $options{'stateid'}
557 : $self->getfield('stateid');
558 $content{state_id_state} = exists($options{'stateid_state'})
559 ? $options{'stateid_state'}
560 : $self->getfield('stateid_state');
561 $content{customer_ssn} = exists($options{'ss'})
565 } elsif ( $options{method} eq 'LEC' ) {
566 $content{phone} = $options{payinfo};
568 die "unknown method ". $options{method};
571 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
574 die "unknown namespace $namespace";
581 my $balance = exists( $options{'balance'} )
582 ? $options{'balance'}
585 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
586 $self->select_for_update; #mutex ... just until we get our pending record in
587 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
589 #the checks here are intended to catch concurrent payments
590 #double-form-submission prevention is taken care of in cust_pay_pending::check
593 return "The customer's balance has changed; $options{method} transaction aborted."
594 if $self->balance < $balance;
596 #also check and make sure there aren't *other* pending payments for this cust
598 my @pending = qsearch('cust_pay_pending', {
599 'custnum' => $self->custnum,
600 'status' => { op=>'!=', value=>'done' }
603 #for third-party payments only, remove pending payments if they're in the
604 #'thirdparty' (waiting for customer action) state.
605 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
606 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
607 my $error = $_->delete;
608 warn "error deleting unfinished third-party payment ".
609 $_->paypendingnum . ": $error\n"
612 @pending = grep { $_->status ne 'thirdparty' } @pending;
615 return "A payment is already being processed for this customer (".
616 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
617 "); $options{method} transaction aborted."
620 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
622 my $cust_pay_pending = new FS::cust_pay_pending {
623 'custnum' => $self->custnum,
624 'paid' => $options{amount},
626 'payby' => $bop_method2payby{$options{method}},
627 'payinfo' => $options{payinfo},
628 'paymask' => $options{paymask},
629 'paydate' => $paydate,
630 'recurring_billing' => $content{recurring_billing},
631 'pkgnum' => $options{'pkgnum'},
633 'gatewaynum' => $payment_gateway->gatewaynum || '',
634 'session_id' => $options{session_id} || '',
635 'jobnum' => $options{depend_jobnum} || '',
637 $cust_pay_pending->payunique( $options{payunique} )
638 if defined($options{payunique}) && length($options{payunique});
640 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
642 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
643 return $cpp_new_err if $cpp_new_err;
645 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
647 warn Dumper($cust_pay_pending) if $DEBUG > 2;
649 my( $action1, $action2 ) =
650 split( /\s*\,\s*/, $payment_gateway->gateway_action );
652 my $transaction = new $namespace( $payment_gateway->gateway_module,
653 $self->_bop_options(\%options),
656 $transaction->content(
657 'type' => $options{method},
658 $self->_bop_auth(\%options),
659 'action' => $action1,
660 'description' => $options{'description'},
661 'amount' => $options{amount},
662 #'invoice_number' => $options{'invnum'},
663 'customer_id' => $self->custnum,
665 'reference' => $cust_pay_pending->paypendingnum, #for now
666 'callback_url' => $payment_gateway->gateway_callback_url,
667 'cancel_url' => $payment_gateway->gateway_cancel_url,
672 $cust_pay_pending->status('pending');
673 my $cpp_pending_err = $cust_pay_pending->replace;
674 return $cpp_pending_err if $cpp_pending_err;
676 warn Dumper($transaction) if $DEBUG > 2;
678 unless ( $BOP_TESTING ) {
679 $transaction->test_transaction(1)
680 if $conf->exists('business-onlinepayment-test_transaction');
681 $transaction->submit();
683 if ( $BOP_TESTING_SUCCESS ) {
684 $transaction->is_success(1);
685 $transaction->authorization('fake auth');
687 $transaction->is_success(0);
688 $transaction->error_message('fake failure');
692 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
694 $cust_pay_pending->status('thirdparty');
695 my $cpp_err = $cust_pay_pending->replace;
696 return { error => $cpp_err } if $cpp_err;
697 return { reference => $cust_pay_pending->paypendingnum,
698 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
700 } elsif ( $transaction->is_success() && $action2 ) {
702 $cust_pay_pending->status('authorized');
703 my $cpp_authorized_err = $cust_pay_pending->replace;
704 return $cpp_authorized_err if $cpp_authorized_err;
706 my $auth = $transaction->authorization;
707 my $ordernum = $transaction->can('order_number')
708 ? $transaction->order_number
712 new Business::OnlinePayment( $payment_gateway->gateway_module,
713 $self->_bop_options(\%options),
718 type => $options{method},
720 $self->_bop_auth(\%options),
721 order_number => $ordernum,
722 amount => $options{amount},
723 authorization => $auth,
724 description => $options{'description'},
727 foreach my $field (qw( authorization_source_code returned_ACI
728 transaction_identifier validation_code
729 transaction_sequence_num local_transaction_date
730 local_transaction_time AVS_result_code )) {
731 $capture{$field} = $transaction->$field() if $transaction->can($field);
734 $capture->content( %capture );
736 $capture->test_transaction(1)
737 if $conf->exists('business-onlinepayment-test_transaction');
740 unless ( $capture->is_success ) {
741 my $e = "Authorization successful but capture failed, custnum #".
742 $self->custnum. ': '. $capture->result_code.
743 ": ". $capture->error_message;
751 # remove paycvv after initial transaction
754 #false laziness w/misc/process/payment.cgi - check both to make sure working
756 if ( length($self->paycvv)
757 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
759 my $error = $self->remove_cvv;
761 warn "WARNING: error removing cvv: $error\n";
770 if ( $transaction->can('card_token') && $transaction->card_token ) {
772 if ( $options{'payinfo'} eq $self->payinfo ) {
773 $self->payinfo($transaction->card_token);
774 my $error = $self->replace;
776 warn "WARNING: error storing token: $error, but proceeding anyway\n";
786 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
798 if (ref($_[0]) eq 'HASH') {
801 my ( $method, $amount ) = ( shift, shift );
803 $options{method} = $method;
804 $options{amount} = $amount;
807 if ( $options{'fake_failure'} ) {
808 return "Error: No error; test failure requested with fake_failure";
811 my $cust_pay = new FS::cust_pay ( {
812 'custnum' => $self->custnum,
813 'invnum' => $options{'invnum'},
814 'paid' => $options{amount},
816 'payby' => $bop_method2payby{$options{method}},
817 #'payinfo' => $payinfo,
818 'payinfo' => '4111111111111111',
819 #'paydate' => $paydate,
820 'paydate' => '2012-05-01',
821 'processor' => 'FakeProcessor',
823 'order_number' => '32',
825 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
828 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
829 warn " $_ => $options{$_}\n" foreach keys %options;
832 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
835 $cust_pay->invnum(''); #try again with no specific invnum
836 my $error2 = $cust_pay->insert( $options{'manual'} ?
837 ( 'manual' => 1 ) : ()
840 # gah, even with transactions.
841 my $e = 'WARNING: Card/ACH debited but database not updated - '.
842 "error inserting (fake!) payment: $error2".
843 " (previously tried insert with invnum #$options{'invnum'}" .
850 if ( $options{'paynum_ref'} ) {
851 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
859 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
861 # Wraps up processing of a realtime credit card, ACH (electronic check) or
862 # phone bill transaction.
864 sub _realtime_bop_result {
865 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
867 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
870 warn "$me _realtime_bop_result: pending transaction ".
871 $cust_pay_pending->paypendingnum. "\n";
872 warn " $_ => $options{$_}\n" foreach keys %options;
875 my $payment_gateway = $options{payment_gateway}
876 or return "no payment gateway in arguments to _realtime_bop_result";
878 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
879 my $cpp_captured_err = $cust_pay_pending->replace;
880 return $cpp_captured_err if $cpp_captured_err;
882 if ( $transaction->is_success() ) {
884 my $order_number = $transaction->order_number
885 if $transaction->can('order_number');
887 my $cust_pay = new FS::cust_pay ( {
888 'custnum' => $self->custnum,
889 'invnum' => $options{'invnum'},
890 'paid' => $cust_pay_pending->paid,
892 'payby' => $cust_pay_pending->payby,
893 'payinfo' => $options{'payinfo'},
894 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
895 'paydate' => $cust_pay_pending->paydate,
896 'pkgnum' => $cust_pay_pending->pkgnum,
897 'discount_term' => $options{'discount_term'},
898 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
899 'processor' => $payment_gateway->gateway_module,
900 'auth' => $transaction->authorization,
901 'order_number' => $order_number || '',
904 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
905 $cust_pay->payunique( $options{payunique} )
906 if defined($options{payunique}) && length($options{payunique});
908 my $oldAutoCommit = $FS::UID::AutoCommit;
909 local $FS::UID::AutoCommit = 0;
912 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
914 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
917 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
918 $cust_pay->invnum(''); #try again with no specific invnum
919 $cust_pay->paynum('');
920 my $error2 = $cust_pay->insert( $options{'manual'} ?
921 ( 'manual' => 1 ) : ()
924 # gah. but at least we have a record of the state we had to abort in
925 # from cust_pay_pending now.
926 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
927 my $e = "WARNING: $options{method} captured but payment not recorded -".
928 " error inserting payment (". $payment_gateway->gateway_module.
930 " (previously tried insert with invnum #$options{'invnum'}" .
931 ": $error ) - pending payment saved as paypendingnum ".
932 $cust_pay_pending->paypendingnum. "\n";
938 my $jobnum = $cust_pay_pending->jobnum;
940 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
942 unless ( $placeholder ) {
943 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
944 my $e = "WARNING: $options{method} captured but job $jobnum not ".
945 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
950 $error = $placeholder->delete;
953 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
954 my $e = "WARNING: $options{method} captured but could not delete ".
955 "job $jobnum for paypendingnum ".
956 $cust_pay_pending->paypendingnum. ": $error\n";
961 $cust_pay_pending->set('jobnum','');
965 if ( $options{'paynum_ref'} ) {
966 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
969 $cust_pay_pending->status('done');
970 $cust_pay_pending->statustext('captured');
971 $cust_pay_pending->paynum($cust_pay->paynum);
972 my $cpp_done_err = $cust_pay_pending->replace;
974 if ( $cpp_done_err ) {
976 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
977 my $e = "WARNING: $options{method} captured but payment not recorded - ".
978 "error updating status for paypendingnum ".
979 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
985 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
987 if ( $options{'apply'} ) {
988 my $apply_error = $self->apply_payments_and_credits;
989 if ( $apply_error ) {
990 warn "WARNING: error applying payment: $apply_error\n";
991 #but we still should return no error cause the payment otherwise went
996 # have a CC surcharge portion --> one-time charge
997 if ( $options{'cc_surcharge'} > 0 ) {
998 # XXX: this whole block needs to be in a transaction?
1001 $invnum = $options{'invnum'} if $options{'invnum'};
1002 unless ( $invnum ) { # probably from a payment screen
1003 # do we have any open invoices? pick earliest
1004 # uses the fact that cust_main->cust_bill sorts by date ascending
1005 my @open = $self->open_cust_bill;
1006 $invnum = $open[0]->invnum if scalar(@open);
1009 unless ( $invnum ) { # still nothing? pick last closed invoice
1010 # again uses fact that cust_main->cust_bill sorts by date ascending
1011 my @closed = $self->cust_bill;
1012 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1015 unless ( $invnum ) {
1016 # XXX: unlikely case - pre-paying before any invoices generated
1017 # what it should do is create a new invoice and pick it
1018 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1023 my $charge_error = $self->charge({
1024 'amount' => $options{'cc_surcharge'},
1025 'pkg' => 'Credit Card Surcharge',
1027 'cust_pkg_ref' => \$cust_pkg,
1030 warn 'Unable to add CC surcharge cust_pkg';
1034 $cust_pkg->setup(time);
1035 my $cp_error = $cust_pkg->replace;
1037 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1041 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1042 unless ( $cust_bill ) {
1043 warn "race condition + invoice deletion just happened";
1048 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1050 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1054 return ''; #no error
1060 my $perror = $transaction->error_message;
1061 #$payment_gateway->gateway_module. " error: ".
1062 # removed for conciseness
1064 my $jobnum = $cust_pay_pending->jobnum;
1066 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1068 if ( $placeholder ) {
1069 my $error = $placeholder->depended_delete;
1070 $error ||= $placeholder->delete;
1071 $cust_pay_pending->set('jobnum','');
1072 warn "error removing provisioning jobs after declined paypendingnum ".
1073 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1075 my $e = "error finding job $jobnum for declined paypendingnum ".
1076 $cust_pay_pending->paypendingnum. "\n";
1082 unless ( $transaction->error_message ) {
1085 if ( $transaction->can('response_page') ) {
1087 'page' => ( $transaction->can('response_page')
1088 ? $transaction->response_page
1091 'code' => ( $transaction->can('response_code')
1092 ? $transaction->response_code
1095 'headers' => ( $transaction->can('response_headers')
1096 ? $transaction->response_headers
1102 "No additional debugging information available for ".
1103 $payment_gateway->gateway_module;
1106 $perror .= "No error_message returned from ".
1107 $payment_gateway->gateway_module. " -- ".
1108 ( ref($t_response) ? Dumper($t_response) : $t_response );
1112 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1113 && $conf->exists('emaildecline', $self->agentnum)
1114 && grep { $_ ne 'POST' } $self->invoicing_list
1115 && ! grep { $transaction->error_message =~ /$_/ }
1116 $conf->config('emaildecline-exclude', $self->agentnum)
1119 # Send a decline alert to the customer.
1120 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1123 # include the raw error message in the transaction state
1124 $cust_pay_pending->setfield('error', $transaction->error_message);
1125 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1126 $error = $msg_template->send( 'cust_main' => $self,
1127 'object' => $cust_pay_pending );
1131 $perror .= " (also received error sending decline notification: $error)"
1136 $cust_pay_pending->status('done');
1137 $cust_pay_pending->statustext($perror);
1138 #'declined:': no, that's failure_status
1139 if ( $transaction->can('failure_status') ) {
1140 $cust_pay_pending->failure_status( $transaction->failure_status );
1142 my $cpp_done_err = $cust_pay_pending->replace;
1143 if ( $cpp_done_err ) {
1144 my $e = "WARNING: $options{method} declined but pending payment not ".
1145 "resolved - error updating status for paypendingnum ".
1146 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1148 $perror = "$e ($perror)";
1156 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1158 Verifies successful third party processing of a realtime credit card,
1159 ACH (electronic check) or phone bill transaction via a
1160 Business::OnlineThirdPartyPayment realtime gateway. See
1161 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1163 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1165 The additional options I<payname>, I<city>, I<state>,
1166 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1167 if set, will override the value from the customer record.
1169 I<description> is a free-text field passed to the gateway. It defaults to
1170 "Internet services".
1172 If an I<invnum> is specified, this payment (if successful) is applied to the
1173 specified invoice. If you don't specify an I<invnum> you might want to
1174 call the B<apply_payments> method.
1176 I<quiet> can be set true to surpress email decline notices.
1178 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1179 resulting paynum, if any.
1181 I<payunique> is a unique identifier for this payment.
1183 Returns a hashref containing elements bill_error (which will be undefined
1184 upon success) and session_id of any associated session.
1188 sub realtime_botpp_capture {
1189 my( $self, $cust_pay_pending, %options ) = @_;
1191 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1194 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1195 warn " $_ => $options{$_}\n" foreach keys %options;
1198 eval "use Business::OnlineThirdPartyPayment";
1202 # select the gateway
1205 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1207 my $payment_gateway;
1208 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1209 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1210 { gatewaynum => $gatewaynum }
1212 : $self->agent->payment_gateway( 'method' => $method,
1213 # 'invnum' => $cust_pay_pending->invnum,
1214 # 'payinfo' => $cust_pay_pending->payinfo,
1217 $options{payment_gateway} = $payment_gateway; # for the helper subs
1223 my @invoicing_list = $self->invoicing_list_emailonly;
1224 if ( $conf->exists('emailinvoiceautoalways')
1225 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1226 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1227 push @invoicing_list, $self->all_emails;
1230 my $email = ($conf->exists('business-onlinepayment-email-override'))
1231 ? $conf->config('business-onlinepayment-email-override')
1232 : $invoicing_list[0];
1236 $content{email_customer} =
1237 ( $conf->exists('business-onlinepayment-email_customer')
1238 || $conf->exists('business-onlinepayment-email-override') );
1241 # run transaction(s)
1245 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1246 $self->_bop_options(\%options),
1249 $transaction->reference({ %options });
1251 $transaction->content(
1253 $self->_bop_auth(\%options),
1254 'action' => 'Post Authorization',
1255 'description' => $options{'description'},
1256 'amount' => $cust_pay_pending->paid,
1257 #'invoice_number' => $options{'invnum'},
1258 'customer_id' => $self->custnum,
1259 'reference' => $cust_pay_pending->paypendingnum,
1261 'phone' => $self->daytime || $self->night,
1263 # plus whatever is required for bogus capture avoidance
1266 $transaction->submit();
1269 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1271 if ( $options{'apply'} ) {
1272 my $apply_error = $self->apply_payments_and_credits;
1273 if ( $apply_error ) {
1274 warn "WARNING: error applying payment: $apply_error\n";
1279 bill_error => $error,
1280 session_id => $cust_pay_pending->session_id,
1285 =item default_payment_gateway
1287 DEPRECATED -- use agent->payment_gateway
1291 sub default_payment_gateway {
1292 my( $self, $method ) = @_;
1294 die "Real-time processing not enabled\n"
1295 unless $conf->exists('business-onlinepayment');
1297 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1300 my $bop_config = 'business-onlinepayment';
1301 $bop_config .= '-ach'
1302 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1303 my ( $processor, $login, $password, $action, @bop_options ) =
1304 $conf->config($bop_config);
1305 $action ||= 'normal authorization';
1306 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1307 die "No real-time processor is enabled - ".
1308 "did you set the business-onlinepayment configuration value?\n"
1311 ( $processor, $login, $password, $action, @bop_options )
1314 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1316 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1317 via a Business::OnlinePayment realtime gateway. See
1318 L<http://420.am/business-onlinepayment> for supported gateways.
1320 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1322 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1324 Most gateways require a reference to an original payment transaction to refund,
1325 so you probably need to specify a I<paynum>.
1327 I<amount> defaults to the original amount of the payment if not specified.
1329 I<reason> specifies a reason for the refund.
1331 I<paydate> specifies the expiration date for a credit card overriding the
1332 value from the customer record or the payment record. Specified as yyyy-mm-dd
1334 Implementation note: If I<amount> is unspecified or equal to the amount of the
1335 orignal payment, first an attempt is made to "void" the transaction via
1336 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1337 the normal attempt is made to "refund" ("credit") the transaction via the
1338 gateway is attempted. No attempt to "void" the transaction is made if the
1339 gateway has introspection data and doesn't support void.
1341 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1342 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1343 #if set, will override the value from the customer record.
1345 #If an I<invnum> is specified, this payment (if successful) is applied to the
1346 #specified invoice. If you don't specify an I<invnum> you might want to
1347 #call the B<apply_payments> method.
1351 #some false laziness w/realtime_bop, not enough to make it worth merging
1352 #but some useful small subs should be pulled out
1353 sub realtime_refund_bop {
1356 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1359 if (ref($_[0]) eq 'HASH') {
1360 %options = %{$_[0]};
1364 $options{method} = $method;
1368 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1369 warn " $_ => $options{$_}\n" foreach keys %options;
1373 # look up the original payment and optionally a gateway for that payment
1377 my $amount = $options{'amount'};
1379 my( $processor, $login, $password, @bop_options, $namespace ) ;
1380 my( $auth, $order_number ) = ( '', '', '' );
1381 my $gatewaynum = '';
1383 if ( $options{'paynum'} ) {
1385 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1386 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1387 or return "Unknown paynum $options{'paynum'}";
1388 $amount ||= $cust_pay->paid;
1390 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";
1463 'type' => $options{method},
1465 'password' => $password,
1466 'order_number' => $order_number,
1467 'amount' => $amount,
1469 $content{authorization} = $auth
1470 if length($auth); #echeck/ACH transactions have an order # but no auth
1471 #(at least with authorize.net)
1473 my $currency = $conf->exists('business-onlinepayment-currency')
1474 && $conf->config('business-onlinepayment-currency');
1475 $content{currency} = $currency if $currency;
1477 my $disable_void_after;
1478 if ($conf->exists('disable_void_after')
1479 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1480 $disable_void_after = $1;
1483 #first try void if applicable
1484 my $void = new Business::OnlinePayment( $processor, @bop_options );
1487 if ($void->can('info')) {
1489 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1490 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1491 my %supported_actions = $void->info('supported_actions');
1493 if ( %supported_actions && $paytype
1494 && defined($supported_actions{$paytype})
1495 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1498 if ( $cust_pay && $cust_pay->paid == $amount
1500 ( not defined($disable_void_after) )
1501 || ( time < ($cust_pay->_date + $disable_void_after ) )
1505 warn " attempting void\n" if $DEBUG > 1;
1506 if ( $void->can('info') ) {
1507 if ( $cust_pay->payby eq 'CARD'
1508 && $void->info('CC_void_requires_card') )
1510 $content{'card_number'} = $cust_pay->payinfo;
1511 } elsif ( $cust_pay->payby eq 'CHEK'
1512 && $void->info('ECHECK_void_requires_account') )
1514 ( $content{'account_number'}, $content{'routing_code'} ) =
1515 split('@', $cust_pay->payinfo);
1516 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1519 $void->content( 'action' => 'void', %content );
1520 $void->test_transaction(1)
1521 if $conf->exists('business-onlinepayment-test_transaction');
1523 if ( $void->is_success ) {
1524 my $error = $cust_pay->void($options{'reason'});
1526 # gah, even with transactions.
1527 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1528 "error voiding payment: $error";
1532 warn " void successful\n" if $DEBUG > 1;
1537 warn " void unsuccessful, trying refund\n"
1541 my $address = $self->address1;
1542 $address .= ", ". $self->address2 if $self->address2;
1544 my($payname, $payfirst, $paylast);
1545 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1546 $payname = $self->payname;
1547 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1548 or return "Illegal payname $payname";
1549 ($payfirst, $paylast) = ($1, $2);
1551 $payfirst = $self->getfield('first');
1552 $paylast = $self->getfield('last');
1553 $payname = "$payfirst $paylast";
1556 my @invoicing_list = $self->invoicing_list_emailonly;
1557 if ( $conf->exists('emailinvoiceautoalways')
1558 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1559 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1560 push @invoicing_list, $self->all_emails;
1563 my $email = ($conf->exists('business-onlinepayment-email-override'))
1564 ? $conf->config('business-onlinepayment-email-override')
1565 : $invoicing_list[0];
1567 my $payip = exists($options{'payip'})
1570 $content{customer_ip} = $payip
1574 if ( $options{method} eq 'CC' ) {
1577 $content{card_number} = $payinfo = $cust_pay->payinfo;
1578 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1579 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1580 ($content{expiration} = "$2/$1"); # where available
1582 $content{card_number} = $payinfo = $self->payinfo;
1583 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1584 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1585 $content{expiration} = "$2/$1";
1588 } elsif ( $options{method} eq 'ECHECK' ) {
1591 $payinfo = $cust_pay->payinfo;
1593 $payinfo = $self->payinfo;
1595 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1596 $content{bank_name} = $self->payname;
1597 $content{account_type} = 'CHECKING';
1598 $content{account_name} = $payname;
1599 $content{customer_org} = $self->company ? 'B' : 'I';
1600 $content{customer_ssn} = $self->ss;
1601 } elsif ( $options{method} eq 'LEC' ) {
1602 $content{phone} = $payinfo = $self->payinfo;
1606 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1607 my %sub_content = $refund->content(
1608 'action' => 'credit',
1609 'customer_id' => $self->custnum,
1610 'last_name' => $paylast,
1611 'first_name' => $payfirst,
1613 'address' => $address,
1614 'city' => $self->city,
1615 'state' => $self->state,
1616 'zip' => $self->zip,
1617 'country' => $self->country,
1619 'phone' => $self->daytime || $self->night,
1622 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1624 $refund->test_transaction(1)
1625 if $conf->exists('business-onlinepayment-test_transaction');
1628 return "$processor error: ". $refund->error_message
1629 unless $refund->is_success();
1631 $order_number = $refund->order_number if $refund->can('order_number');
1633 # change this to just use $cust_pay->delete_cust_bill_pay?
1634 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1635 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1636 last unless @cust_bill_pay;
1637 my $cust_bill_pay = pop @cust_bill_pay;
1638 my $error = $cust_bill_pay->delete;
1642 my $cust_refund = new FS::cust_refund ( {
1643 'custnum' => $self->custnum,
1644 'paynum' => $options{'paynum'},
1645 'refund' => $amount,
1647 'payby' => $bop_method2payby{$options{method}},
1648 'payinfo' => $payinfo,
1649 'reason' => $options{'reason'} || 'card or ACH refund',
1650 'gatewaynum' => $gatewaynum, # may be null
1651 'processor' => $processor,
1652 'auth' => $refund->authorization,
1653 'order_number' => $order_number,
1655 my $error = $cust_refund->insert;
1657 $cust_refund->paynum(''); #try again with no specific paynum
1658 my $error2 = $cust_refund->insert;
1660 # gah, even with transactions.
1661 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1662 "error inserting refund ($processor): $error2".
1663 " (previously tried insert with paynum #$options{'paynum'}" .
1682 L<FS::cust_main>, L<FS::cust_main::Billing>