1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.28;
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
17 $realtime_bop_decline_quiet = 0;
19 # 1 is mostly method/subroutine entry and options
20 # 2 traces progress of some operations
21 # 3 is even more information including possibly sensitive data
23 $me = '[FS::cust_main::Billing_Realtime]';
26 our $BOP_TESTING_SUCCESS = 1;
28 install_callback FS::UID sub {
30 #yes, need it for stuff below (prolly should be cached)
35 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
41 These methods are available on FS::cust_main objects.
47 =item realtime_collect [ OPTION => VALUE ... ]
49 Attempt to collect the customer's current balance with a realtime credit
50 card, electronic check, or phone bill transaction (see realtime_bop() below).
52 Returns the result of realtime_bop(): nothing, an error message, or a
53 hashref of state information for a third-party transaction.
55 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
57 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
58 then it is deduced from the customer record.
60 If no I<amount> is specified, then the customer balance is used.
62 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
63 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
64 if set, will override the value from the customer record.
66 I<description> is a free-text field passed to the gateway. It defaults to
67 the value defined by the business-onlinepayment-description configuration
68 option, or "Internet services" if that is unset.
70 If an I<invnum> is specified, this payment (if successful) is applied to the
73 I<apply> will automatically apply a resulting payment.
75 I<quiet> can be set true to suppress email decline notices.
77 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
78 resulting paynum, if any.
80 I<payunique> is a unique identifier for this payment.
82 I<session_id> is a session identifier associated with this payment.
84 I<depend_jobnum> allows payment capture to unlock export jobs
88 sub realtime_collect {
89 my( $self, %options ) = @_;
91 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
94 warn "$me realtime_collect:\n";
95 warn " $_ => $options{$_}\n" foreach keys %options;
98 $options{amount} = $self->balance unless exists( $options{amount} );
99 $options{method} = FS::payby->payby2bop($self->payby)
100 unless exists( $options{method} );
102 return $self->realtime_bop({%options});
106 =item realtime_bop { [ ARG => VALUE ... ] }
108 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
109 via a Business::OnlinePayment realtime gateway. See
110 L<http://420.am/business-onlinepayment> for supported gateways.
112 Required arguments in the hashref are I<method>, and I<amount>
114 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
116 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
118 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
119 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
120 if set, will override the value from the customer record.
122 I<description> is a free-text field passed to the gateway. It defaults to
123 the value defined by the business-onlinepayment-description configuration
124 option, or "Internet services" if that is unset.
126 If an I<invnum> is specified, this payment (if successful) is applied to the
127 specified invoice. If the customer has exactly one open invoice, that
128 invoice number will be assumed. If you don't specify an I<invnum> you might
129 want to call the B<apply_payments> method or set the I<apply> option.
131 I<apply> can be set to true to apply a resulting payment.
133 I<quiet> can be set true to surpress email decline notices.
135 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
136 resulting paynum, if any.
138 I<payunique> is a unique identifier for this payment.
140 I<session_id> is a session identifier associated with this payment.
142 I<depend_jobnum> allows payment capture to unlock export jobs
144 I<discount_term> attempts to take a discount by prepaying for discount_term.
145 The payment will fail if I<amount> is incorrect for this discount term.
147 A direct (Business::OnlinePayment) transaction will return nothing on success,
148 or an error message on failure.
150 A third-party transaction will return a hashref containing:
152 - popup_url: the URL to which a browser should be redirected to complete
154 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
155 - reference: a reference ID for the transaction, to show the customer.
157 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
161 # some helper routines
162 sub _bop_recurring_billing {
163 my( $self, %opt ) = @_;
165 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
167 if ( defined($method) && $method eq 'transaction_is_recur' ) {
169 return 1 if $opt{'trans_is_recur'};
173 # return 1 if the payinfo has been used for another payment
174 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
182 sub _payment_gateway {
183 my ($self, $options) = @_;
185 if ( $options->{'selfservice'} ) {
186 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
188 return $options->{payment_gateway} ||=
189 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
193 if ( $options->{'fake_gatewaynum'} ) {
194 $options->{payment_gateway} =
195 qsearchs('payment_gateway',
196 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
200 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
201 unless exists($options->{payment_gateway});
203 $options->{payment_gateway};
207 my ($self, $options) = @_;
210 'login' => $options->{payment_gateway}->gateway_username,
211 'password' => $options->{payment_gateway}->gateway_password,
216 my ($self, $options) = @_;
218 $options->{payment_gateway}->gatewaynum
219 ? $options->{payment_gateway}->options
220 : @{ $options->{payment_gateway}->get('options') };
225 my ($self, $options) = @_;
227 unless ( $options->{'description'} ) {
228 if ( $conf->exists('business-onlinepayment-description') ) {
229 my $dtempl = $conf->config('business-onlinepayment-description');
231 my $agent = $self->agent->agent;
233 $options->{'description'} = eval qq("$dtempl");
235 $options->{'description'} = 'Internet services';
239 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
241 # Default invoice number if the customer has exactly one open invoice.
242 if( ! $options->{'invnum'} ) {
243 $options->{'invnum'} = '';
244 my @open = $self->open_cust_bill;
245 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
248 $options->{payname} = $self->payname unless exists( $options->{payname} );
252 my ($self, $options) = @_;
255 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
256 $content{customer_ip} = $payip if length($payip);
258 $content{invoice_number} = $options->{'invnum'}
259 if exists($options->{'invnum'}) && length($options->{'invnum'});
261 $content{email_customer} =
262 ( $conf->exists('business-onlinepayment-email_customer')
263 || $conf->exists('business-onlinepayment-email-override') );
265 my ($payname, $payfirst, $paylast);
266 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
267 ($payname = $options->{payname}) =~
268 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
269 or return "Illegal payname $payname";
270 ($payfirst, $paylast) = ($1, $2);
272 $payfirst = $self->getfield('first');
273 $paylast = $self->getfield('last');
274 $payname = "$payfirst $paylast";
277 $content{last_name} = $paylast;
278 $content{first_name} = $payfirst;
280 $content{name} = $payname;
282 $content{address} = exists($options->{'address1'})
283 ? $options->{'address1'}
285 my $address2 = exists($options->{'address2'})
286 ? $options->{'address2'}
288 $content{address} .= ", ". $address2 if length($address2);
290 $content{city} = exists($options->{city})
293 $content{state} = exists($options->{state})
296 $content{zip} = exists($options->{zip})
299 $content{country} = exists($options->{country})
300 ? $options->{country}
303 $content{phone} = $self->daytime || $self->night;
305 my $currency = $conf->exists('business-onlinepayment-currency')
306 && $conf->config('business-onlinepayment-currency');
307 $content{currency} = $currency if $currency;
312 my %bop_method2payby = (
322 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
325 if (ref($_[0]) eq 'HASH') {
328 my ( $method, $amount ) = ( shift, shift );
330 $options{method} = $method;
331 $options{amount} = $amount;
336 # optional credit card surcharge
339 my $cc_surcharge = 0;
340 my $cc_surcharge_pct = 0;
341 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
342 if $conf->config('credit-card-surcharge-percentage');
344 # always add cc surcharge if called from event
345 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
346 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
347 $options{'amount'} += $cc_surcharge;
348 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
350 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
351 # payment screen), so consider the given
352 # amount as post-surcharge
353 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
356 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
357 $options{'cc_surcharge'} = $cc_surcharge;
361 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
362 warn " cc_surcharge = $cc_surcharge\n";
363 warn " $_ => $options{$_}\n" foreach keys %options;
366 return $self->fake_bop(\%options) if $options{'fake'};
368 $self->_bop_defaults(\%options);
371 # set trans_is_recur based on invnum if there is one
374 my $trans_is_recur = 0;
375 if ( $options{'invnum'} ) {
377 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
378 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
384 $cust_bill->cust_bill_pkg;
387 if grep { $_->freq ne '0' } @part_pkg;
395 my $payment_gateway = $self->_payment_gateway( \%options );
396 my $namespace = $payment_gateway->gateway_namespace;
398 eval "use $namespace";
402 # check for banned credit card/ACH
405 my $ban = FS::banned_pay->ban_search(
406 'payby' => $bop_method2payby{$options{method}},
407 'payinfo' => $options{payinfo},
409 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
412 # check for term discount validity
415 my $discount_term = $options{discount_term};
416 if ( $discount_term ) {
417 my $bill = ($self->cust_bill)[-1]
418 or return "Can't apply a term discount to an unbilled customer";
419 my $plan = FS::discount_plan->new(
421 months => $discount_term
422 ) or return "No discount available for term '$discount_term'";
424 if ( $plan->discounted_total != $options{amount} ) {
425 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
433 my $bop_content = $self->_bop_content(\%options);
434 return $bop_content unless ref($bop_content);
436 my @invoicing_list = $self->invoicing_list_emailonly;
437 if ( $conf->exists('emailinvoiceautoalways')
438 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
439 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
440 push @invoicing_list, $self->all_emails;
443 my $email = ($conf->exists('business-onlinepayment-email-override'))
444 ? $conf->config('business-onlinepayment-email-override')
445 : $invoicing_list[0];
450 if ( $namespace eq 'Business::OnlinePayment' ) {
452 if ( $options{method} eq 'CC' ) {
454 $content{card_number} = $options{payinfo};
455 $paydate = exists($options{'paydate'})
456 ? $options{'paydate'}
458 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
459 $content{expiration} = "$2/$1";
461 my $paycvv = exists($options{'paycvv'})
464 $content{cvv2} = $paycvv
467 my $paystart_month = exists($options{'paystart_month'})
468 ? $options{'paystart_month'}
469 : $self->paystart_month;
471 my $paystart_year = exists($options{'paystart_year'})
472 ? $options{'paystart_year'}
473 : $self->paystart_year;
475 $content{card_start} = "$paystart_month/$paystart_year"
476 if $paystart_month && $paystart_year;
478 my $payissue = exists($options{'payissue'})
479 ? $options{'payissue'}
481 $content{issue_number} = $payissue if $payissue;
483 if ( $self->_bop_recurring_billing(
484 'payinfo' => $options{'payinfo'},
485 'trans_is_recur' => $trans_is_recur,
489 $content{recurring_billing} = 'YES';
490 $content{acct_code} = 'rebill'
491 if $conf->exists('credit_card-recurring_billing_acct_code');
494 } elsif ( $options{method} eq 'ECHECK' ){
496 ( $content{account_number}, $content{routing_code} ) =
497 split('@', $options{payinfo});
498 $content{bank_name} = $options{payname};
499 $content{bank_state} = exists($options{'paystate'})
500 ? $options{'paystate'}
501 : $self->getfield('paystate');
502 $content{account_type}=
503 (exists($options{'paytype'}) && $options{'paytype'})
504 ? uc($options{'paytype'})
505 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
507 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
508 $content{account_name} = $self->company;
510 $content{account_name} = $self->getfield('first'). ' '.
511 $self->getfield('last');
514 $content{customer_org} = $self->company ? 'B' : 'I';
515 $content{state_id} = exists($options{'stateid'})
516 ? $options{'stateid'}
517 : $self->getfield('stateid');
518 $content{state_id_state} = exists($options{'stateid_state'})
519 ? $options{'stateid_state'}
520 : $self->getfield('stateid_state');
521 $content{customer_ssn} = exists($options{'ss'})
525 } elsif ( $options{method} eq 'LEC' ) {
526 $content{phone} = $options{payinfo};
528 die "unknown method ". $options{method};
531 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
534 die "unknown namespace $namespace";
541 my $balance = exists( $options{'balance'} )
542 ? $options{'balance'}
545 $self->select_for_update; #mutex ... just until we get our pending record in
547 #the checks here are intended to catch concurrent payments
548 #double-form-submission prevention is taken care of in cust_pay_pending::check
551 return "The customer's balance has changed; $options{method} transaction aborted."
552 if $self->balance < $balance;
554 #also check and make sure there aren't *other* pending payments for this cust
556 my @pending = qsearch('cust_pay_pending', {
557 'custnum' => $self->custnum,
558 'status' => { op=>'!=', value=>'done' }
561 #for third-party payments only, remove pending payments if they're in the
562 #'thirdparty' (waiting for customer action) state.
563 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
564 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
565 my $error = $_->delete;
566 warn "error deleting unfinished third-party payment ".
567 $_->paypendingnum . ": $error\n"
570 @pending = grep { $_->status ne 'thirdparty' } @pending;
573 return "A payment is already being processed for this customer (".
574 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
575 "); $options{method} transaction aborted."
578 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
580 my $cust_pay_pending = new FS::cust_pay_pending {
581 'custnum' => $self->custnum,
582 'paid' => $options{amount},
584 'payby' => $bop_method2payby{$options{method}},
585 'payinfo' => $options{payinfo},
586 'paydate' => $paydate,
587 'recurring_billing' => $content{recurring_billing},
588 'pkgnum' => $options{'pkgnum'},
590 'gatewaynum' => $payment_gateway->gatewaynum || '',
591 'session_id' => $options{session_id} || '',
592 'jobnum' => $options{depend_jobnum} || '',
594 $cust_pay_pending->payunique( $options{payunique} )
595 if defined($options{payunique}) && length($options{payunique});
596 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
597 return $cpp_new_err if $cpp_new_err;
599 my( $action1, $action2 ) =
600 split( /\s*\,\s*/, $payment_gateway->gateway_action );
602 my $transaction = new $namespace( $payment_gateway->gateway_module,
603 $self->_bop_options(\%options),
606 $transaction->content(
607 'type' => $options{method},
608 $self->_bop_auth(\%options),
609 'action' => $action1,
610 'description' => $options{'description'},
611 'amount' => $options{amount},
612 #'invoice_number' => $options{'invnum'},
613 'customer_id' => $self->custnum,
615 'reference' => $cust_pay_pending->paypendingnum, #for now
616 'callback_url' => $payment_gateway->gateway_callback_url,
617 'cancel_url' => $payment_gateway->gateway_cancel_url,
622 $cust_pay_pending->status('pending');
623 my $cpp_pending_err = $cust_pay_pending->replace;
624 return $cpp_pending_err if $cpp_pending_err;
626 warn Dumper($transaction) if $DEBUG > 2;
628 unless ( $BOP_TESTING ) {
629 $transaction->test_transaction(1)
630 if $conf->exists('business-onlinepayment-test_transaction');
631 $transaction->submit();
633 if ( $BOP_TESTING_SUCCESS ) {
634 $transaction->is_success(1);
635 $transaction->authorization('fake auth');
637 $transaction->is_success(0);
638 $transaction->error_message('fake failure');
642 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
644 $cust_pay_pending->status('thirdparty');
645 my $cpp_err = $cust_pay_pending->replace;
646 return { error => $cpp_err } if $cpp_err;
647 return { reference => $cust_pay_pending->paypendingnum,
648 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
650 } elsif ( $transaction->is_success() && $action2 ) {
652 $cust_pay_pending->status('authorized');
653 my $cpp_authorized_err = $cust_pay_pending->replace;
654 return $cpp_authorized_err if $cpp_authorized_err;
656 my $auth = $transaction->authorization;
657 my $ordernum = $transaction->can('order_number')
658 ? $transaction->order_number
662 new Business::OnlinePayment( $payment_gateway->gateway_module,
663 $self->_bop_options(\%options),
668 type => $options{method},
670 $self->_bop_auth(\%options),
671 order_number => $ordernum,
672 amount => $options{amount},
673 authorization => $auth,
674 description => $options{'description'},
677 foreach my $field (qw( authorization_source_code returned_ACI
678 transaction_identifier validation_code
679 transaction_sequence_num local_transaction_date
680 local_transaction_time AVS_result_code )) {
681 $capture{$field} = $transaction->$field() if $transaction->can($field);
684 $capture->content( %capture );
686 $capture->test_transaction(1)
687 if $conf->exists('business-onlinepayment-test_transaction');
690 unless ( $capture->is_success ) {
691 my $e = "Authorization successful but capture failed, custnum #".
692 $self->custnum. ': '. $capture->result_code.
693 ": ". $capture->error_message;
701 # remove paycvv after initial transaction
704 #false laziness w/misc/process/payment.cgi - check both to make sure working
706 if ( length($self->paycvv)
707 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
709 my $error = $self->remove_cvv;
711 warn "WARNING: error removing cvv: $error\n";
720 if ( $transaction->can('card_token') && $transaction->card_token ) {
722 $self->card_token($transaction->card_token);
724 if ( $options{'payinfo'} eq $self->payinfo ) {
725 $self->payinfo($transaction->card_token);
726 my $error = $self->replace;
728 warn "WARNING: error storing token: $error, but proceeding anyway\n";
738 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
750 if (ref($_[0]) eq 'HASH') {
753 my ( $method, $amount ) = ( shift, shift );
755 $options{method} = $method;
756 $options{amount} = $amount;
759 if ( $options{'fake_failure'} ) {
760 return "Error: No error; test failure requested with fake_failure";
763 my $cust_pay = new FS::cust_pay ( {
764 'custnum' => $self->custnum,
765 'invnum' => $options{'invnum'},
766 'paid' => $options{amount},
768 'payby' => $bop_method2payby{$options{method}},
769 #'payinfo' => $payinfo,
770 'payinfo' => '4111111111111111',
771 #'paydate' => $paydate,
772 'paydate' => '2012-05-01',
773 'processor' => 'FakeProcessor',
775 'order_number' => '32',
777 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
780 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
781 warn " $_ => $options{$_}\n" foreach keys %options;
784 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
787 $cust_pay->invnum(''); #try again with no specific invnum
788 my $error2 = $cust_pay->insert( $options{'manual'} ?
789 ( 'manual' => 1 ) : ()
792 # gah, even with transactions.
793 my $e = 'WARNING: Card/ACH debited but database not updated - '.
794 "error inserting (fake!) payment: $error2".
795 " (previously tried insert with invnum #$options{'invnum'}" .
802 if ( $options{'paynum_ref'} ) {
803 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
811 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
813 # Wraps up processing of a realtime credit card, ACH (electronic check) or
814 # phone bill transaction.
816 sub _realtime_bop_result {
817 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
819 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
822 warn "$me _realtime_bop_result: pending transaction ".
823 $cust_pay_pending->paypendingnum. "\n";
824 warn " $_ => $options{$_}\n" foreach keys %options;
827 my $payment_gateway = $options{payment_gateway}
828 or return "no payment gateway in arguments to _realtime_bop_result";
830 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
831 my $cpp_captured_err = $cust_pay_pending->replace;
832 return $cpp_captured_err if $cpp_captured_err;
834 if ( $transaction->is_success() ) {
836 my $order_number = $transaction->order_number
837 if $transaction->can('order_number');
839 my $cust_pay = new FS::cust_pay ( {
840 'custnum' => $self->custnum,
841 'invnum' => $options{'invnum'},
842 'paid' => $cust_pay_pending->paid,
844 'payby' => $cust_pay_pending->payby,
845 'payinfo' => $options{'payinfo'},
846 'paydate' => $cust_pay_pending->paydate,
847 'pkgnum' => $cust_pay_pending->pkgnum,
848 'discount_term' => $options{'discount_term'},
849 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
850 'processor' => $payment_gateway->gateway_module,
851 'auth' => $transaction->authorization,
852 'order_number' => $order_number || '',
855 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
856 $cust_pay->payunique( $options{payunique} )
857 if defined($options{payunique}) && length($options{payunique});
859 my $oldAutoCommit = $FS::UID::AutoCommit;
860 local $FS::UID::AutoCommit = 0;
863 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
865 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
868 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
869 $cust_pay->invnum(''); #try again with no specific invnum
870 $cust_pay->paynum('');
871 my $error2 = $cust_pay->insert( $options{'manual'} ?
872 ( 'manual' => 1 ) : ()
875 # gah. but at least we have a record of the state we had to abort in
876 # from cust_pay_pending now.
877 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
878 my $e = "WARNING: $options{method} captured but payment not recorded -".
879 " error inserting payment (". $payment_gateway->gateway_module.
881 " (previously tried insert with invnum #$options{'invnum'}" .
882 ": $error ) - pending payment saved as paypendingnum ".
883 $cust_pay_pending->paypendingnum. "\n";
889 my $jobnum = $cust_pay_pending->jobnum;
891 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
893 unless ( $placeholder ) {
894 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
895 my $e = "WARNING: $options{method} captured but job $jobnum not ".
896 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
901 $error = $placeholder->delete;
904 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
905 my $e = "WARNING: $options{method} captured but could not delete ".
906 "job $jobnum for paypendingnum ".
907 $cust_pay_pending->paypendingnum. ": $error\n";
914 if ( $options{'paynum_ref'} ) {
915 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
918 $cust_pay_pending->status('done');
919 $cust_pay_pending->statustext('captured');
920 $cust_pay_pending->paynum($cust_pay->paynum);
921 my $cpp_done_err = $cust_pay_pending->replace;
923 if ( $cpp_done_err ) {
925 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
926 my $e = "WARNING: $options{method} captured but payment not recorded - ".
927 "error updating status for paypendingnum ".
928 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
934 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
936 if ( $options{'apply'} ) {
937 my $apply_error = $self->apply_payments_and_credits;
938 if ( $apply_error ) {
939 warn "WARNING: error applying payment: $apply_error\n";
940 #but we still should return no error cause the payment otherwise went
945 # have a CC surcharge portion --> one-time charge
946 if ( $options{'cc_surcharge'} > 0 ) {
947 # XXX: this whole block needs to be in a transaction?
950 $invnum = $options{'invnum'} if $options{'invnum'};
951 unless ( $invnum ) { # probably from a payment screen
952 # do we have any open invoices? pick earliest
953 # uses the fact that cust_main->cust_bill sorts by date ascending
954 my @open = $self->open_cust_bill;
955 $invnum = $open[0]->invnum if scalar(@open);
958 unless ( $invnum ) { # still nothing? pick last closed invoice
959 # again uses fact that cust_main->cust_bill sorts by date ascending
960 my @closed = $self->cust_bill;
961 $invnum = $closed[$#closed]->invnum if scalar(@closed);
965 # XXX: unlikely case - pre-paying before any invoices generated
966 # what it should do is create a new invoice and pick it
967 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
972 my $charge_error = $self->charge({
973 'amount' => $options{'cc_surcharge'},
974 'pkg' => 'Credit Card Surcharge',
976 'cust_pkg_ref' => \$cust_pkg,
979 warn 'Unable to add CC surcharge cust_pkg';
983 $cust_pkg->setup(time);
984 my $cp_error = $cust_pkg->replace;
986 warn 'Unable to set setup time on cust_pkg for cc surcharge';
990 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
991 unless ( $cust_bill ) {
992 warn "race condition + invoice deletion just happened";
997 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
999 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1003 return ''; #no error
1009 my $perror = $transaction->error_message;
1010 #$payment_gateway->gateway_module. " error: ".
1011 # removed for conciseness
1013 my $jobnum = $cust_pay_pending->jobnum;
1015 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1017 if ( $placeholder ) {
1018 my $error = $placeholder->depended_delete;
1019 $error ||= $placeholder->delete;
1020 warn "error removing provisioning jobs after declined paypendingnum ".
1021 $cust_pay_pending->paypendingnum. ": $error\n";
1023 my $e = "error finding job $jobnum for declined paypendingnum ".
1024 $cust_pay_pending->paypendingnum. "\n";
1030 unless ( $transaction->error_message ) {
1033 if ( $transaction->can('response_page') ) {
1035 'page' => ( $transaction->can('response_page')
1036 ? $transaction->response_page
1039 'code' => ( $transaction->can('response_code')
1040 ? $transaction->response_code
1043 'headers' => ( $transaction->can('response_headers')
1044 ? $transaction->response_headers
1050 "No additional debugging information available for ".
1051 $payment_gateway->gateway_module;
1054 $perror .= "No error_message returned from ".
1055 $payment_gateway->gateway_module. " -- ".
1056 ( ref($t_response) ? Dumper($t_response) : $t_response );
1060 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1061 && $conf->exists('emaildecline', $self->agentnum)
1062 && grep { $_ ne 'POST' } $self->invoicing_list
1063 && ! grep { $transaction->error_message =~ /$_/ }
1064 $conf->config('emaildecline-exclude', $self->agentnum)
1067 # Send a decline alert to the customer.
1068 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1071 # include the raw error message in the transaction state
1072 $cust_pay_pending->setfield('error', $transaction->error_message);
1073 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1074 $error = $msg_template->send( 'cust_main' => $self,
1075 'object' => $cust_pay_pending );
1079 my @templ = $conf->config('declinetemplate');
1080 my $template = new Text::Template (
1082 SOURCE => [ map "$_\n", @templ ],
1083 ) or return "($perror) can't create template: $Text::Template::ERROR";
1084 $template->compile()
1085 or return "($perror) can't compile template: $Text::Template::ERROR";
1089 scalar( $conf->config('company_name', $self->agentnum ) ),
1090 'company_address' =>
1091 join("\n", $conf->config('company_address', $self->agentnum ) ),
1092 'error' => $transaction->error_message,
1095 my $error = send_email(
1096 'from' => $conf->config('invoice_from', $self->agentnum ),
1097 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1098 'subject' => 'Your payment could not be processed',
1099 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1103 $perror .= " (also received error sending decline notification: $error)"
1108 $cust_pay_pending->status('done');
1109 $cust_pay_pending->statustext($perror);
1110 #'declined:': no, that's failure_status
1111 if ( $transaction->can('failure_status') ) {
1112 $cust_pay_pending->failure_status( $transaction->failure_status );
1114 my $cpp_done_err = $cust_pay_pending->replace;
1115 if ( $cpp_done_err ) {
1116 my $e = "WARNING: $options{method} declined but pending payment not ".
1117 "resolved - error updating status for paypendingnum ".
1118 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1120 $perror = "$e ($perror)";
1128 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1130 Verifies successful third party processing of a realtime credit card,
1131 ACH (electronic check) or phone bill transaction via a
1132 Business::OnlineThirdPartyPayment realtime gateway. See
1133 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1135 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1137 The additional options I<payname>, I<city>, I<state>,
1138 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1139 if set, will override the value from the customer record.
1141 I<description> is a free-text field passed to the gateway. It defaults to
1142 "Internet services".
1144 If an I<invnum> is specified, this payment (if successful) is applied to the
1145 specified invoice. If you don't specify an I<invnum> you might want to
1146 call the B<apply_payments> method.
1148 I<quiet> can be set true to surpress email decline notices.
1150 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1151 resulting paynum, if any.
1153 I<payunique> is a unique identifier for this payment.
1155 Returns a hashref containing elements bill_error (which will be undefined
1156 upon success) and session_id of any associated session.
1160 sub realtime_botpp_capture {
1161 my( $self, $cust_pay_pending, %options ) = @_;
1163 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1166 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1167 warn " $_ => $options{$_}\n" foreach keys %options;
1170 eval "use Business::OnlineThirdPartyPayment";
1174 # select the gateway
1177 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1179 my $payment_gateway;
1180 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1181 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1182 { gatewaynum => $gatewaynum }
1184 : $self->agent->payment_gateway( 'method' => $method,
1185 # 'invnum' => $cust_pay_pending->invnum,
1186 # 'payinfo' => $cust_pay_pending->payinfo,
1189 $options{payment_gateway} = $payment_gateway; # for the helper subs
1195 my @invoicing_list = $self->invoicing_list_emailonly;
1196 if ( $conf->exists('emailinvoiceautoalways')
1197 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1198 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1199 push @invoicing_list, $self->all_emails;
1202 my $email = ($conf->exists('business-onlinepayment-email-override'))
1203 ? $conf->config('business-onlinepayment-email-override')
1204 : $invoicing_list[0];
1208 $content{email_customer} =
1209 ( $conf->exists('business-onlinepayment-email_customer')
1210 || $conf->exists('business-onlinepayment-email-override') );
1213 # run transaction(s)
1217 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1218 $self->_bop_options(\%options),
1221 $transaction->reference({ %options });
1223 $transaction->content(
1225 $self->_bop_auth(\%options),
1226 'action' => 'Post Authorization',
1227 'description' => $options{'description'},
1228 'amount' => $cust_pay_pending->paid,
1229 #'invoice_number' => $options{'invnum'},
1230 'customer_id' => $self->custnum,
1231 'reference' => $cust_pay_pending->paypendingnum,
1233 'phone' => $self->daytime || $self->night,
1235 # plus whatever is required for bogus capture avoidance
1238 $transaction->submit();
1241 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1243 if ( $options{'apply'} ) {
1244 my $apply_error = $self->apply_payments_and_credits;
1245 if ( $apply_error ) {
1246 warn "WARNING: error applying payment: $apply_error\n";
1251 bill_error => $error,
1252 session_id => $cust_pay_pending->session_id,
1257 =item default_payment_gateway
1259 DEPRECATED -- use agent->payment_gateway
1263 sub default_payment_gateway {
1264 my( $self, $method ) = @_;
1266 die "Real-time processing not enabled\n"
1267 unless $conf->exists('business-onlinepayment');
1269 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1272 my $bop_config = 'business-onlinepayment';
1273 $bop_config .= '-ach'
1274 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1275 my ( $processor, $login, $password, $action, @bop_options ) =
1276 $conf->config($bop_config);
1277 $action ||= 'normal authorization';
1278 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1279 die "No real-time processor is enabled - ".
1280 "did you set the business-onlinepayment configuration value?\n"
1283 ( $processor, $login, $password, $action, @bop_options )
1286 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1288 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1289 via a Business::OnlinePayment realtime gateway. See
1290 L<http://420.am/business-onlinepayment> for supported gateways.
1292 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1294 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1296 Most gateways require a reference to an original payment transaction to refund,
1297 so you probably need to specify a I<paynum>.
1299 I<amount> defaults to the original amount of the payment if not specified.
1301 I<reason> specifies a reason for the refund.
1303 I<paydate> specifies the expiration date for a credit card overriding the
1304 value from the customer record or the payment record. Specified as yyyy-mm-dd
1306 Implementation note: If I<amount> is unspecified or equal to the amount of the
1307 orignal payment, first an attempt is made to "void" the transaction via
1308 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1309 the normal attempt is made to "refund" ("credit") the transaction via the
1310 gateway is attempted. No attempt to "void" the transaction is made if the
1311 gateway has introspection data and doesn't support void.
1313 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1314 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1315 #if set, will override the value from the customer record.
1317 #If an I<invnum> is specified, this payment (if successful) is applied to the
1318 #specified invoice. If you don't specify an I<invnum> you might want to
1319 #call the B<apply_payments> method.
1323 #some false laziness w/realtime_bop, not enough to make it worth merging
1324 #but some useful small subs should be pulled out
1325 sub realtime_refund_bop {
1328 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1331 if (ref($_[0]) eq 'HASH') {
1332 %options = %{$_[0]};
1336 $options{method} = $method;
1340 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1341 warn " $_ => $options{$_}\n" foreach keys %options;
1345 # look up the original payment and optionally a gateway for that payment
1349 my $amount = $options{'amount'};
1351 my( $processor, $login, $password, @bop_options, $namespace ) ;
1352 my( $auth, $order_number ) = ( '', '', '' );
1353 my $gatewaynum = '';
1355 if ( $options{'paynum'} ) {
1357 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1358 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1359 or return "Unknown paynum $options{'paynum'}";
1360 $amount ||= $cust_pay->paid;
1362 if ( $cust_pay->get('processor') ) {
1363 ($gatewaynum, $processor, $auth, $order_number) =
1365 $cust_pay->gatewaynum,
1366 $cust_pay->processor,
1368 $cust_pay->order_number,
1371 # this payment wasn't upgraded, which probably means this won't work,
1373 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1374 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1375 $cust_pay->paybatch;
1376 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1379 if ( $gatewaynum ) { #gateway for the payment to be refunded
1381 my $payment_gateway =
1382 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1383 die "payment gateway $gatewaynum not found"
1384 unless $payment_gateway;
1386 $processor = $payment_gateway->gateway_module;
1387 $login = $payment_gateway->gateway_username;
1388 $password = $payment_gateway->gateway_password;
1389 $namespace = $payment_gateway->gateway_namespace;
1390 @bop_options = $payment_gateway->options;
1392 } else { #try the default gateway
1395 my $payment_gateway =
1396 $self->agent->payment_gateway('method' => $options{method});
1398 ( $conf_processor, $login, $password, $namespace ) =
1399 map { my $method = "gateway_$_"; $payment_gateway->$method }
1400 qw( module username password namespace );
1402 @bop_options = $payment_gateway->gatewaynum
1403 ? $payment_gateway->options
1404 : @{ $payment_gateway->get('options') };
1406 return "processor of payment $options{'paynum'} $processor does not".
1407 " match default processor $conf_processor"
1408 unless $processor eq $conf_processor;
1413 } else { # didn't specify a paynum, so look for agent gateway overrides
1414 # like a normal transaction
1416 my $payment_gateway =
1417 $self->agent->payment_gateway( 'method' => $options{method},
1418 #'payinfo' => $payinfo,
1420 my( $processor, $login, $password, $namespace ) =
1421 map { my $method = "gateway_$_"; $payment_gateway->$method }
1422 qw( module username password namespace );
1424 my @bop_options = $payment_gateway->gatewaynum
1425 ? $payment_gateway->options
1426 : @{ $payment_gateway->get('options') };
1429 return "neither amount nor paynum specified" unless $amount;
1431 eval "use $namespace";
1435 'type' => $options{method},
1437 'password' => $password,
1438 'order_number' => $order_number,
1439 'amount' => $amount,
1441 $content{authorization} = $auth
1442 if length($auth); #echeck/ACH transactions have an order # but no auth
1443 #(at least with authorize.net)
1445 my $currency = $conf->exists('business-onlinepayment-currency')
1446 && $conf->config('business-onlinepayment-currency');
1447 $content{currency} = $currency if $currency;
1449 my $disable_void_after;
1450 if ($conf->exists('disable_void_after')
1451 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1452 $disable_void_after = $1;
1455 #first try void if applicable
1456 my $void = new Business::OnlinePayment( $processor, @bop_options );
1459 if ($void->can('info')) {
1461 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1462 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1463 my %supported_actions = $void->info('supported_actions');
1465 if ( %supported_actions && $paytype
1466 && defined($supported_actions{$paytype})
1467 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1470 if ( $cust_pay && $cust_pay->paid == $amount
1472 ( not defined($disable_void_after) )
1473 || ( time < ($cust_pay->_date + $disable_void_after ) )
1477 warn " attempting void\n" if $DEBUG > 1;
1478 if ( $void->can('info') ) {
1479 if ( $cust_pay->payby eq 'CARD'
1480 && $void->info('CC_void_requires_card') )
1482 $content{'card_number'} = $cust_pay->payinfo;
1483 } elsif ( $cust_pay->payby eq 'CHEK'
1484 && $void->info('ECHECK_void_requires_account') )
1486 ( $content{'account_number'}, $content{'routing_code'} ) =
1487 split('@', $cust_pay->payinfo);
1488 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1491 $void->content( 'action' => 'void', %content );
1492 $void->test_transaction(1)
1493 if $conf->exists('business-onlinepayment-test_transaction');
1495 if ( $void->is_success ) {
1496 my $error = $cust_pay->void($options{'reason'});
1498 # gah, even with transactions.
1499 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1500 "error voiding payment: $error";
1504 warn " void successful\n" if $DEBUG > 1;
1509 warn " void unsuccessful, trying refund\n"
1513 my $address = $self->address1;
1514 $address .= ", ". $self->address2 if $self->address2;
1516 my($payname, $payfirst, $paylast);
1517 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1518 $payname = $self->payname;
1519 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1520 or return "Illegal payname $payname";
1521 ($payfirst, $paylast) = ($1, $2);
1523 $payfirst = $self->getfield('first');
1524 $paylast = $self->getfield('last');
1525 $payname = "$payfirst $paylast";
1528 my @invoicing_list = $self->invoicing_list_emailonly;
1529 if ( $conf->exists('emailinvoiceautoalways')
1530 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1531 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1532 push @invoicing_list, $self->all_emails;
1535 my $email = ($conf->exists('business-onlinepayment-email-override'))
1536 ? $conf->config('business-onlinepayment-email-override')
1537 : $invoicing_list[0];
1539 my $payip = exists($options{'payip'})
1542 $content{customer_ip} = $payip
1546 if ( $options{method} eq 'CC' ) {
1549 $content{card_number} = $payinfo = $cust_pay->payinfo;
1550 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1551 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1552 ($content{expiration} = "$2/$1"); # where available
1554 $content{card_number} = $payinfo = $self->payinfo;
1555 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1556 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1557 $content{expiration} = "$2/$1";
1560 } elsif ( $options{method} eq 'ECHECK' ) {
1563 $payinfo = $cust_pay->payinfo;
1565 $payinfo = $self->payinfo;
1567 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1568 $content{bank_name} = $self->payname;
1569 $content{account_type} = 'CHECKING';
1570 $content{account_name} = $payname;
1571 $content{customer_org} = $self->company ? 'B' : 'I';
1572 $content{customer_ssn} = $self->ss;
1573 } elsif ( $options{method} eq 'LEC' ) {
1574 $content{phone} = $payinfo = $self->payinfo;
1578 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1579 my %sub_content = $refund->content(
1580 'action' => 'credit',
1581 'customer_id' => $self->custnum,
1582 'last_name' => $paylast,
1583 'first_name' => $payfirst,
1585 'address' => $address,
1586 'city' => $self->city,
1587 'state' => $self->state,
1588 'zip' => $self->zip,
1589 'country' => $self->country,
1591 'phone' => $self->daytime || $self->night,
1594 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1596 $refund->test_transaction(1)
1597 if $conf->exists('business-onlinepayment-test_transaction');
1600 return "$processor error: ". $refund->error_message
1601 unless $refund->is_success();
1603 $order_number = $refund->order_number if $refund->can('order_number');
1605 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1606 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1607 last unless @cust_bill_pay;
1608 my $cust_bill_pay = pop @cust_bill_pay;
1609 my $error = $cust_bill_pay->delete;
1613 my $cust_refund = new FS::cust_refund ( {
1614 'custnum' => $self->custnum,
1615 'paynum' => $options{'paynum'},
1616 'refund' => $amount,
1618 'payby' => $bop_method2payby{$options{method}},
1619 'payinfo' => $payinfo,
1620 'reason' => $options{'reason'} || 'card or ACH refund',
1621 'gatewaynum' => $gatewaynum, # may be null
1622 'processor' => $processor,
1623 'auth' => $refund->authorization,
1624 'order_number' => $order_number,
1626 my $error = $cust_refund->insert;
1628 $cust_refund->paynum(''); #try again with no specific paynum
1629 my $error2 = $cust_refund->insert;
1631 # gah, even with transactions.
1632 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1633 "error inserting refund ($processor): $error2".
1634 " (previously tried insert with paynum #$options{'paynum'}" .
1653 L<FS::cust_main>, L<FS::cust_main::Billing>