1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.28;
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
17 $realtime_bop_decline_quiet = 0;
19 # 1 is mostly method/subroutine entry and options
20 # 2 traces progress of some operations
21 # 3 is even more information including possibly sensitive data
23 $me = '[FS::cust_main::Billing_Realtime]';
26 our $BOP_TESTING_SUCCESS = 1;
28 install_callback FS::UID sub {
30 #yes, need it for stuff below (prolly should be cached)
35 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
41 These methods are available on FS::cust_main objects.
47 =item realtime_collect [ OPTION => VALUE ... ]
49 Attempt to collect the customer's current balance with a realtime credit
50 card, electronic check, or phone bill transaction (see realtime_bop() below).
52 Returns the result of realtime_bop(): nothing, an error message, or a
53 hashref of state information for a third-party transaction.
55 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
57 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
58 then it is deduced from the customer record.
60 If no I<amount> is specified, then the customer balance is used.
62 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
63 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
64 if set, will override the value from the customer record.
66 I<description> is a free-text field passed to the gateway. It defaults to
67 the value defined by the business-onlinepayment-description configuration
68 option, or "Internet services" if that is unset.
70 If an I<invnum> is specified, this payment (if successful) is applied to the
73 I<apply> will automatically apply a resulting payment.
75 I<quiet> can be set true to suppress email decline notices.
77 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
78 resulting paynum, if any.
80 I<payunique> is a unique identifier for this payment.
82 I<session_id> is a session identifier associated with this payment.
84 I<depend_jobnum> allows payment capture to unlock export jobs
88 sub realtime_collect {
89 my( $self, %options ) = @_;
91 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
94 warn "$me realtime_collect:\n";
95 warn " $_ => $options{$_}\n" foreach keys %options;
98 $options{amount} = $self->balance unless exists( $options{amount} );
99 $options{method} = FS::payby->payby2bop($self->payby)
100 unless exists( $options{method} );
102 return $self->realtime_bop({%options});
106 =item realtime_bop { [ ARG => VALUE ... ] }
108 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
109 via a Business::OnlinePayment realtime gateway. See
110 L<http://420.am/business-onlinepayment> for supported gateways.
112 Required arguments in the hashref are I<method>, and I<amount>
114 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
116 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
118 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
119 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
120 if set, will override the value from the customer record.
122 I<description> is a free-text field passed to the gateway. It defaults to
123 the value defined by the business-onlinepayment-description configuration
124 option, or "Internet services" if that is unset.
126 If an I<invnum> is specified, this payment (if successful) is applied to the
127 specified invoice. If the customer has exactly one open invoice, that
128 invoice number will be assumed. If you don't specify an I<invnum> you might
129 want to call the B<apply_payments> method or set the I<apply> option.
131 I<apply> can be set to true to apply a resulting payment.
133 I<quiet> can be set true to surpress email decline notices.
135 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
136 resulting paynum, if any.
138 I<payunique> is a unique identifier for this payment.
140 I<session_id> is a session identifier associated with this payment.
142 I<depend_jobnum> allows payment capture to unlock export jobs
144 I<discount_term> attempts to take a discount by prepaying for discount_term.
145 The payment will fail if I<amount> is incorrect for this discount term.
147 A direct (Business::OnlinePayment) transaction will return nothing on success,
148 or an error message on failure.
150 A third-party transaction will return a hashref containing:
152 - popup_url: the URL to which a browser should be redirected to complete
154 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
155 - reference: a reference ID for the transaction, to show the customer.
157 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
161 # some helper routines
162 sub _bop_recurring_billing {
163 my( $self, %opt ) = @_;
165 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
167 if ( defined($method) && $method eq 'transaction_is_recur' ) {
169 return 1 if $opt{'trans_is_recur'};
173 # return 1 if the payinfo has been used for another payment
174 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
182 sub _payment_gateway {
183 my ($self, $options) = @_;
185 if ( $options->{'selfservice'} ) {
186 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
188 return $options->{payment_gateway} ||=
189 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
193 if ( $options->{'fake_gatewaynum'} ) {
194 $options->{payment_gateway} =
195 qsearchs('payment_gateway',
196 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
200 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
201 unless exists($options->{payment_gateway});
203 $options->{payment_gateway};
207 my ($self, $options) = @_;
210 'login' => $options->{payment_gateway}->gateway_username,
211 'password' => $options->{payment_gateway}->gateway_password,
216 my ($self, $options) = @_;
218 $options->{payment_gateway}->gatewaynum
219 ? $options->{payment_gateway}->options
220 : @{ $options->{payment_gateway}->get('options') };
225 my ($self, $options) = @_;
227 unless ( $options->{'description'} ) {
228 if ( $conf->exists('business-onlinepayment-description') ) {
229 my $dtempl = $conf->config('business-onlinepayment-description');
231 my $agent = $self->agent->agent;
233 $options->{'description'} = eval qq("$dtempl");
235 $options->{'description'} = 'Internet services';
239 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
241 # Default invoice number if the customer has exactly one open invoice.
242 if( ! $options->{'invnum'} ) {
243 $options->{'invnum'} = '';
244 my @open = $self->open_cust_bill;
245 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
248 $options->{payname} = $self->payname unless exists( $options->{payname} );
252 my ($self, $options) = @_;
255 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
256 $content{customer_ip} = $payip if length($payip);
258 $content{invoice_number} = $options->{'invnum'}
259 if exists($options->{'invnum'}) && length($options->{'invnum'});
261 $content{email_customer} =
262 ( $conf->exists('business-onlinepayment-email_customer')
263 || $conf->exists('business-onlinepayment-email-override') );
265 my ($payname, $payfirst, $paylast);
266 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
267 ($payname = $options->{payname}) =~
268 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
269 or return "Illegal payname $payname";
270 ($payfirst, $paylast) = ($1, $2);
272 $payfirst = $self->getfield('first');
273 $paylast = $self->getfield('last');
274 $payname = "$payfirst $paylast";
277 $content{last_name} = $paylast;
278 $content{first_name} = $payfirst;
280 $content{name} = $payname;
282 $content{address} = exists($options->{'address1'})
283 ? $options->{'address1'}
285 my $address2 = exists($options->{'address2'})
286 ? $options->{'address2'}
288 $content{address} .= ", ". $address2 if length($address2);
290 $content{city} = exists($options->{city})
293 $content{state} = exists($options->{state})
296 $content{zip} = exists($options->{zip})
299 $content{country} = exists($options->{country})
300 ? $options->{country}
303 #3.0 is a good a time as any to get rid of this... add a config to pass it
304 # if anyone still needs it
305 #$content{referer} = 'http://cleanwhisker.420.am/';
307 $content{phone} = $self->daytime || $self->night;
309 my $currency = $conf->exists('business-onlinepayment-currency')
310 && $conf->config('business-onlinepayment-currency');
311 $content{currency} = $currency if $currency;
316 my %bop_method2payby = (
326 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
329 if (ref($_[0]) eq 'HASH') {
332 my ( $method, $amount ) = ( shift, shift );
334 $options{method} = $method;
335 $options{amount} = $amount;
340 # optional credit card surcharge
343 my $cc_surcharge = 0;
344 my $cc_surcharge_pct = 0;
345 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
346 if $conf->config('credit-card-surcharge-percentage');
348 # always add cc surcharge if called from event
349 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
350 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
351 $options{'amount'} += $cc_surcharge;
352 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
354 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
355 # payment screen), so consider the given
356 # amount as post-surcharge
357 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
360 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
361 $options{'cc_surcharge'} = $cc_surcharge;
365 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
366 warn " cc_surcharge = $cc_surcharge\n";
367 warn " $_ => $options{$_}\n" foreach keys %options;
370 return $self->fake_bop(\%options) if $options{'fake'};
372 $self->_bop_defaults(\%options);
375 # set trans_is_recur based on invnum if there is one
378 my $trans_is_recur = 0;
379 if ( $options{'invnum'} ) {
381 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
382 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
388 $cust_bill->cust_bill_pkg;
391 if grep { $_->freq ne '0' } @part_pkg;
399 my $payment_gateway = $self->_payment_gateway( \%options );
400 my $namespace = $payment_gateway->gateway_namespace;
402 eval "use $namespace";
406 # check for banned credit card/ACH
409 my $ban = FS::banned_pay->ban_search(
410 'payby' => $bop_method2payby{$options{method}},
411 'payinfo' => $options{payinfo},
413 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
416 # check for term discount validity
419 my $discount_term = $options{discount_term};
420 if ( $discount_term ) {
421 my $bill = ($self->cust_bill)[-1]
422 or return "Can't apply a term discount to an unbilled customer";
423 my $plan = FS::discount_plan->new(
425 months => $discount_term
426 ) or return "No discount available for term '$discount_term'";
428 if ( $plan->discounted_total != $options{amount} ) {
429 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
437 my $bop_content = $self->_bop_content(\%options);
438 return $bop_content unless ref($bop_content);
440 my @invoicing_list = $self->invoicing_list_emailonly;
441 if ( $conf->exists('emailinvoiceautoalways')
442 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
443 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
444 push @invoicing_list, $self->all_emails;
447 my $email = ($conf->exists('business-onlinepayment-email-override'))
448 ? $conf->config('business-onlinepayment-email-override')
449 : $invoicing_list[0];
454 if ( $namespace eq 'Business::OnlinePayment' ) {
456 if ( $options{method} eq 'CC' ) {
458 $content{card_number} = $options{payinfo};
459 $paydate = exists($options{'paydate'})
460 ? $options{'paydate'}
462 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
463 $content{expiration} = "$2/$1";
465 my $paycvv = exists($options{'paycvv'})
468 $content{cvv2} = $paycvv
471 my $paystart_month = exists($options{'paystart_month'})
472 ? $options{'paystart_month'}
473 : $self->paystart_month;
475 my $paystart_year = exists($options{'paystart_year'})
476 ? $options{'paystart_year'}
477 : $self->paystart_year;
479 $content{card_start} = "$paystart_month/$paystart_year"
480 if $paystart_month && $paystart_year;
482 my $payissue = exists($options{'payissue'})
483 ? $options{'payissue'}
485 $content{issue_number} = $payissue if $payissue;
487 if ( $self->_bop_recurring_billing(
488 'payinfo' => $options{'payinfo'},
489 'trans_is_recur' => $trans_is_recur,
493 $content{recurring_billing} = 'YES';
494 $content{acct_code} = 'rebill'
495 if $conf->exists('credit_card-recurring_billing_acct_code');
498 } elsif ( $options{method} eq 'ECHECK' ){
500 ( $content{account_number}, $content{routing_code} ) =
501 split('@', $options{payinfo});
502 $content{bank_name} = $options{payname};
503 $content{bank_state} = exists($options{'paystate'})
504 ? $options{'paystate'}
505 : $self->getfield('paystate');
506 $content{account_type}=
507 (exists($options{'paytype'}) && $options{'paytype'})
508 ? uc($options{'paytype'})
509 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
510 $content{account_name} = $self->getfield('first'). ' '.
511 $self->getfield('last');
513 $content{customer_org} = $self->company ? 'B' : 'I';
514 $content{state_id} = exists($options{'stateid'})
515 ? $options{'stateid'}
516 : $self->getfield('stateid');
517 $content{state_id_state} = exists($options{'stateid_state'})
518 ? $options{'stateid_state'}
519 : $self->getfield('stateid_state');
520 $content{customer_ssn} = exists($options{'ss'})
524 } elsif ( $options{method} eq 'LEC' ) {
525 $content{phone} = $options{payinfo};
527 die "unknown method ". $options{method};
530 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
533 die "unknown namespace $namespace";
540 my $balance = exists( $options{'balance'} )
541 ? $options{'balance'}
544 $self->select_for_update; #mutex ... just until we get our pending record in
546 #the checks here are intended to catch concurrent payments
547 #double-form-submission prevention is taken care of in cust_pay_pending::check
550 return "The customer's balance has changed; $options{method} transaction aborted."
551 if $self->balance < $balance;
553 #also check and make sure there aren't *other* pending payments for this cust
555 my @pending = qsearch('cust_pay_pending', {
556 'custnum' => $self->custnum,
557 'status' => { op=>'!=', value=>'done' }
560 #for third-party payments only, remove pending payments if they're in the
561 #'thirdparty' (waiting for customer action) state.
562 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
563 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
564 my $error = $_->delete;
565 warn "error deleting unfinished third-party payment ".
566 $_->paypendingnum . ": $error\n"
569 @pending = grep { $_->status ne 'thirdparty' } @pending;
572 return "A payment is already being processed for this customer (".
573 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
574 "); $options{method} transaction aborted."
577 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
579 my $cust_pay_pending = new FS::cust_pay_pending {
580 'custnum' => $self->custnum,
581 'paid' => $options{amount},
583 'payby' => $bop_method2payby{$options{method}},
584 'payinfo' => $options{payinfo},
585 'paydate' => $paydate,
586 'recurring_billing' => $content{recurring_billing},
587 'pkgnum' => $options{'pkgnum'},
589 'gatewaynum' => $payment_gateway->gatewaynum || '',
590 'session_id' => $options{session_id} || '',
591 'jobnum' => $options{depend_jobnum} || '',
593 $cust_pay_pending->payunique( $options{payunique} )
594 if defined($options{payunique}) && length($options{payunique});
595 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
596 return $cpp_new_err if $cpp_new_err;
598 my( $action1, $action2 ) =
599 split( /\s*\,\s*/, $payment_gateway->gateway_action );
601 my $transaction = new $namespace( $payment_gateway->gateway_module,
602 $self->_bop_options(\%options),
605 $transaction->content(
606 'type' => $options{method},
607 $self->_bop_auth(\%options),
608 'action' => $action1,
609 'description' => $options{'description'},
610 'amount' => $options{amount},
611 #'invoice_number' => $options{'invnum'},
612 'customer_id' => $self->custnum,
614 'reference' => $cust_pay_pending->paypendingnum, #for now
615 'callback_url' => $payment_gateway->gateway_callback_url,
616 'cancel_url' => $payment_gateway->gateway_cancel_url,
621 $cust_pay_pending->status('pending');
622 my $cpp_pending_err = $cust_pay_pending->replace;
623 return $cpp_pending_err if $cpp_pending_err;
625 warn Dumper($transaction) if $DEBUG > 2;
627 unless ( $BOP_TESTING ) {
628 $transaction->test_transaction(1)
629 if $conf->exists('business-onlinepayment-test_transaction');
630 $transaction->submit();
632 if ( $BOP_TESTING_SUCCESS ) {
633 $transaction->is_success(1);
634 $transaction->authorization('fake auth');
636 $transaction->is_success(0);
637 $transaction->error_message('fake failure');
641 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
643 $cust_pay_pending->status('thirdparty');
644 my $cpp_err = $cust_pay_pending->replace;
645 return { error => $cpp_err } if $cpp_err;
646 return { reference => $cust_pay_pending->paypendingnum,
647 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
649 } elsif ( $transaction->is_success() && $action2 ) {
651 $cust_pay_pending->status('authorized');
652 my $cpp_authorized_err = $cust_pay_pending->replace;
653 return $cpp_authorized_err if $cpp_authorized_err;
655 my $auth = $transaction->authorization;
656 my $ordernum = $transaction->can('order_number')
657 ? $transaction->order_number
661 new Business::OnlinePayment( $payment_gateway->gateway_module,
662 $self->_bop_options(\%options),
667 type => $options{method},
669 $self->_bop_auth(\%options),
670 order_number => $ordernum,
671 amount => $options{amount},
672 authorization => $auth,
673 description => $options{'description'},
676 foreach my $field (qw( authorization_source_code returned_ACI
677 transaction_identifier validation_code
678 transaction_sequence_num local_transaction_date
679 local_transaction_time AVS_result_code )) {
680 $capture{$field} = $transaction->$field() if $transaction->can($field);
683 $capture->content( %capture );
685 $capture->test_transaction(1)
686 if $conf->exists('business-onlinepayment-test_transaction');
689 unless ( $capture->is_success ) {
690 my $e = "Authorization successful but capture failed, custnum #".
691 $self->custnum. ': '. $capture->result_code.
692 ": ". $capture->error_message;
700 # remove paycvv after initial transaction
703 #false laziness w/misc/process/payment.cgi - check both to make sure working
705 if ( length($self->paycvv)
706 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
708 my $error = $self->remove_cvv;
710 warn "WARNING: error removing cvv: $error\n";
719 if ( $transaction->can('card_token') && $transaction->card_token ) {
721 $self->card_token($transaction->card_token);
723 if ( $options{'payinfo'} eq $self->payinfo ) {
724 $self->payinfo($transaction->card_token);
725 my $error = $self->replace;
727 warn "WARNING: error storing token: $error, but proceeding anyway\n";
737 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
749 if (ref($_[0]) eq 'HASH') {
752 my ( $method, $amount ) = ( shift, shift );
754 $options{method} = $method;
755 $options{amount} = $amount;
758 if ( $options{'fake_failure'} ) {
759 return "Error: No error; test failure requested with fake_failure";
762 my $cust_pay = new FS::cust_pay ( {
763 'custnum' => $self->custnum,
764 'invnum' => $options{'invnum'},
765 'paid' => $options{amount},
767 'payby' => $bop_method2payby{$options{method}},
768 #'payinfo' => $payinfo,
769 'payinfo' => '4111111111111111',
770 #'paydate' => $paydate,
771 'paydate' => '2012-05-01',
772 'processor' => 'FakeProcessor',
774 'order_number' => '32',
776 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
779 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
780 warn " $_ => $options{$_}\n" foreach keys %options;
783 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
786 $cust_pay->invnum(''); #try again with no specific invnum
787 my $error2 = $cust_pay->insert( $options{'manual'} ?
788 ( 'manual' => 1 ) : ()
791 # gah, even with transactions.
792 my $e = 'WARNING: Card/ACH debited but database not updated - '.
793 "error inserting (fake!) payment: $error2".
794 " (previously tried insert with invnum #$options{'invnum'}" .
801 if ( $options{'paynum_ref'} ) {
802 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
810 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
812 # Wraps up processing of a realtime credit card, ACH (electronic check) or
813 # phone bill transaction.
815 sub _realtime_bop_result {
816 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
818 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
821 warn "$me _realtime_bop_result: pending transaction ".
822 $cust_pay_pending->paypendingnum. "\n";
823 warn " $_ => $options{$_}\n" foreach keys %options;
826 my $payment_gateway = $options{payment_gateway}
827 or return "no payment gateway in arguments to _realtime_bop_result";
829 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
830 my $cpp_captured_err = $cust_pay_pending->replace;
831 return $cpp_captured_err if $cpp_captured_err;
833 if ( $transaction->is_success() ) {
835 my $order_number = $transaction->order_number
836 if $transaction->can('order_number');
838 my $cust_pay = new FS::cust_pay ( {
839 'custnum' => $self->custnum,
840 'invnum' => $options{'invnum'},
841 'paid' => $cust_pay_pending->paid,
843 'payby' => $cust_pay_pending->payby,
844 'payinfo' => $options{'payinfo'},
845 'paydate' => $cust_pay_pending->paydate,
846 'pkgnum' => $cust_pay_pending->pkgnum,
847 'discount_term' => $options{'discount_term'},
848 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
849 'processor' => $payment_gateway->gateway_module,
850 'auth' => $transaction->authorization,
851 'order_number' => $order_number || '',
854 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
855 $cust_pay->payunique( $options{payunique} )
856 if defined($options{payunique}) && length($options{payunique});
858 my $oldAutoCommit = $FS::UID::AutoCommit;
859 local $FS::UID::AutoCommit = 0;
862 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
864 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
867 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
868 $cust_pay->invnum(''); #try again with no specific invnum
869 $cust_pay->paynum('');
870 my $error2 = $cust_pay->insert( $options{'manual'} ?
871 ( 'manual' => 1 ) : ()
874 # gah. but at least we have a record of the state we had to abort in
875 # from cust_pay_pending now.
876 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
877 my $e = "WARNING: $options{method} captured but payment not recorded -".
878 " error inserting payment (". $payment_gateway->gateway_module.
880 " (previously tried insert with invnum #$options{'invnum'}" .
881 ": $error ) - pending payment saved as paypendingnum ".
882 $cust_pay_pending->paypendingnum. "\n";
888 my $jobnum = $cust_pay_pending->jobnum;
890 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
892 unless ( $placeholder ) {
893 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
894 my $e = "WARNING: $options{method} captured but job $jobnum not ".
895 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
900 $error = $placeholder->delete;
903 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
904 my $e = "WARNING: $options{method} captured but could not delete ".
905 "job $jobnum for paypendingnum ".
906 $cust_pay_pending->paypendingnum. ": $error\n";
913 if ( $options{'paynum_ref'} ) {
914 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
917 $cust_pay_pending->status('done');
918 $cust_pay_pending->statustext('captured');
919 $cust_pay_pending->paynum($cust_pay->paynum);
920 my $cpp_done_err = $cust_pay_pending->replace;
922 if ( $cpp_done_err ) {
924 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
925 my $e = "WARNING: $options{method} captured but payment not recorded - ".
926 "error updating status for paypendingnum ".
927 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
933 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
935 if ( $options{'apply'} ) {
936 my $apply_error = $self->apply_payments_and_credits;
937 if ( $apply_error ) {
938 warn "WARNING: error applying payment: $apply_error\n";
939 #but we still should return no error cause the payment otherwise went
944 # have a CC surcharge portion --> one-time charge
945 if ( $options{'cc_surcharge'} > 0 ) {
946 # XXX: this whole block needs to be in a transaction?
949 $invnum = $options{'invnum'} if $options{'invnum'};
950 unless ( $invnum ) { # probably from a payment screen
951 # do we have any open invoices? pick earliest
952 # uses the fact that cust_main->cust_bill sorts by date ascending
953 my @open = $self->open_cust_bill;
954 $invnum = $open[0]->invnum if scalar(@open);
957 unless ( $invnum ) { # still nothing? pick last closed invoice
958 # again uses fact that cust_main->cust_bill sorts by date ascending
959 my @closed = $self->cust_bill;
960 $invnum = $closed[$#closed]->invnum if scalar(@closed);
964 # XXX: unlikely case - pre-paying before any invoices generated
965 # what it should do is create a new invoice and pick it
966 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
971 my $charge_error = $self->charge({
972 'amount' => $options{'cc_surcharge'},
973 'pkg' => 'Credit Card Surcharge',
975 'cust_pkg_ref' => \$cust_pkg,
978 warn 'Unable to add CC surcharge cust_pkg';
982 $cust_pkg->setup(time);
983 my $cp_error = $cust_pkg->replace;
985 warn 'Unable to set setup time on cust_pkg for cc surcharge';
989 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
990 unless ( $cust_bill ) {
991 warn "race condition + invoice deletion just happened";
996 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
998 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1002 return ''; #no error
1008 my $perror = $payment_gateway->gateway_module. " error: ".
1009 $transaction->error_message;
1011 my $jobnum = $cust_pay_pending->jobnum;
1013 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1015 if ( $placeholder ) {
1016 my $error = $placeholder->depended_delete;
1017 $error ||= $placeholder->delete;
1018 warn "error removing provisioning jobs after declined paypendingnum ".
1019 $cust_pay_pending->paypendingnum. ": $error\n";
1021 my $e = "error finding job $jobnum for declined paypendingnum ".
1022 $cust_pay_pending->paypendingnum. "\n";
1028 unless ( $transaction->error_message ) {
1031 if ( $transaction->can('response_page') ) {
1033 'page' => ( $transaction->can('response_page')
1034 ? $transaction->response_page
1037 'code' => ( $transaction->can('response_code')
1038 ? $transaction->response_code
1041 'headers' => ( $transaction->can('response_headers')
1042 ? $transaction->response_headers
1048 "No additional debugging information available for ".
1049 $payment_gateway->gateway_module;
1052 $perror .= "No error_message returned from ".
1053 $payment_gateway->gateway_module. " -- ".
1054 ( ref($t_response) ? Dumper($t_response) : $t_response );
1058 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1059 && $conf->exists('emaildecline', $self->agentnum)
1060 && grep { $_ ne 'POST' } $self->invoicing_list
1061 && ! grep { $transaction->error_message =~ /$_/ }
1062 $conf->config('emaildecline-exclude', $self->agentnum)
1065 # Send a decline alert to the customer.
1066 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1069 # include the raw error message in the transaction state
1070 $cust_pay_pending->setfield('error', $transaction->error_message);
1071 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1072 $error = $msg_template->send( 'cust_main' => $self,
1073 'object' => $cust_pay_pending );
1077 my @templ = $conf->config('declinetemplate');
1078 my $template = new Text::Template (
1080 SOURCE => [ map "$_\n", @templ ],
1081 ) or return "($perror) can't create template: $Text::Template::ERROR";
1082 $template->compile()
1083 or return "($perror) can't compile template: $Text::Template::ERROR";
1087 scalar( $conf->config('company_name', $self->agentnum ) ),
1088 'company_address' =>
1089 join("\n", $conf->config('company_address', $self->agentnum ) ),
1090 'error' => $transaction->error_message,
1093 my $error = send_email(
1094 'from' => $conf->config('invoice_from', $self->agentnum ),
1095 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1096 'subject' => 'Your payment could not be processed',
1097 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1101 $perror .= " (also received error sending decline notification: $error)"
1106 $cust_pay_pending->status('done');
1107 $cust_pay_pending->statustext("declined: $perror");
1108 my $cpp_done_err = $cust_pay_pending->replace;
1109 if ( $cpp_done_err ) {
1110 my $e = "WARNING: $options{method} declined but pending payment not ".
1111 "resolved - error updating status for paypendingnum ".
1112 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1114 $perror = "$e ($perror)";
1122 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1124 Verifies successful third party processing of a realtime credit card,
1125 ACH (electronic check) or phone bill transaction via a
1126 Business::OnlineThirdPartyPayment realtime gateway. See
1127 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1129 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1131 The additional options I<payname>, I<city>, I<state>,
1132 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1133 if set, will override the value from the customer record.
1135 I<description> is a free-text field passed to the gateway. It defaults to
1136 "Internet services".
1138 If an I<invnum> is specified, this payment (if successful) is applied to the
1139 specified invoice. If you don't specify an I<invnum> you might want to
1140 call the B<apply_payments> method.
1142 I<quiet> can be set true to surpress email decline notices.
1144 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1145 resulting paynum, if any.
1147 I<payunique> is a unique identifier for this payment.
1149 Returns a hashref containing elements bill_error (which will be undefined
1150 upon success) and session_id of any associated session.
1154 sub realtime_botpp_capture {
1155 my( $self, $cust_pay_pending, %options ) = @_;
1157 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1160 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1161 warn " $_ => $options{$_}\n" foreach keys %options;
1164 eval "use Business::OnlineThirdPartyPayment";
1168 # select the gateway
1171 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1173 my $payment_gateway;
1174 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1175 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1176 { gatewaynum => $gatewaynum }
1178 : $self->agent->payment_gateway( 'method' => $method,
1179 # 'invnum' => $cust_pay_pending->invnum,
1180 # 'payinfo' => $cust_pay_pending->payinfo,
1183 $options{payment_gateway} = $payment_gateway; # for the helper subs
1189 my @invoicing_list = $self->invoicing_list_emailonly;
1190 if ( $conf->exists('emailinvoiceautoalways')
1191 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1192 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1193 push @invoicing_list, $self->all_emails;
1196 my $email = ($conf->exists('business-onlinepayment-email-override'))
1197 ? $conf->config('business-onlinepayment-email-override')
1198 : $invoicing_list[0];
1202 $content{email_customer} =
1203 ( $conf->exists('business-onlinepayment-email_customer')
1204 || $conf->exists('business-onlinepayment-email-override') );
1207 # run transaction(s)
1211 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1212 $self->_bop_options(\%options),
1215 $transaction->reference({ %options });
1217 $transaction->content(
1219 $self->_bop_auth(\%options),
1220 'action' => 'Post Authorization',
1221 'description' => $options{'description'},
1222 'amount' => $cust_pay_pending->paid,
1223 #'invoice_number' => $options{'invnum'},
1224 'customer_id' => $self->custnum,
1226 #3.0 is a good a time as any to get rid of this... add a config to pass it
1227 # if anyone still needs it
1228 #'referer' => 'http://cleanwhisker.420.am/',
1230 'reference' => $cust_pay_pending->paypendingnum,
1232 'phone' => $self->daytime || $self->night,
1234 # plus whatever is required for bogus capture avoidance
1237 $transaction->submit();
1240 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1242 if ( $options{'apply'} ) {
1243 my $apply_error = $self->apply_payments_and_credits;
1244 if ( $apply_error ) {
1245 warn "WARNING: error applying payment: $apply_error\n";
1250 bill_error => $error,
1251 session_id => $cust_pay_pending->session_id,
1256 =item default_payment_gateway
1258 DEPRECATED -- use agent->payment_gateway
1262 sub default_payment_gateway {
1263 my( $self, $method ) = @_;
1265 die "Real-time processing not enabled\n"
1266 unless $conf->exists('business-onlinepayment');
1268 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1271 my $bop_config = 'business-onlinepayment';
1272 $bop_config .= '-ach'
1273 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1274 my ( $processor, $login, $password, $action, @bop_options ) =
1275 $conf->config($bop_config);
1276 $action ||= 'normal authorization';
1277 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1278 die "No real-time processor is enabled - ".
1279 "did you set the business-onlinepayment configuration value?\n"
1282 ( $processor, $login, $password, $action, @bop_options )
1285 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1287 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1288 via a Business::OnlinePayment realtime gateway. See
1289 L<http://420.am/business-onlinepayment> for supported gateways.
1291 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1293 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1295 Most gateways require a reference to an original payment transaction to refund,
1296 so you probably need to specify a I<paynum>.
1298 I<amount> defaults to the original amount of the payment if not specified.
1300 I<reason> specifies a reason for the refund.
1302 I<paydate> specifies the expiration date for a credit card overriding the
1303 value from the customer record or the payment record. Specified as yyyy-mm-dd
1305 Implementation note: If I<amount> is unspecified or equal to the amount of the
1306 orignal payment, first an attempt is made to "void" the transaction via
1307 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1308 the normal attempt is made to "refund" ("credit") the transaction via the
1309 gateway is attempted. No attempt to "void" the transaction is made if the
1310 gateway has introspection data and doesn't support void.
1312 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1313 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1314 #if set, will override the value from the customer record.
1316 #If an I<invnum> is specified, this payment (if successful) is applied to the
1317 #specified invoice. If you don't specify an I<invnum> you might want to
1318 #call the B<apply_payments> method.
1322 #some false laziness w/realtime_bop, not enough to make it worth merging
1323 #but some useful small subs should be pulled out
1324 sub realtime_refund_bop {
1327 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1330 if (ref($_[0]) eq 'HASH') {
1331 %options = %{$_[0]};
1335 $options{method} = $method;
1339 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1340 warn " $_ => $options{$_}\n" foreach keys %options;
1344 # look up the original payment and optionally a gateway for that payment
1348 my $amount = $options{'amount'};
1350 my( $processor, $login, $password, @bop_options, $namespace ) ;
1351 my( $auth, $order_number ) = ( '', '', '' );
1352 my $gatewaynum = '';
1354 if ( $options{'paynum'} ) {
1356 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1357 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1358 or return "Unknown paynum $options{'paynum'}";
1359 $amount ||= $cust_pay->paid;
1361 if ( $cust_pay->get('processor') ) {
1362 ($gatewaynum, $processor, $auth, $order_number) =
1364 $cust_pay->gatewaynum,
1365 $cust_pay->processor,
1367 $cust_pay->order_number,
1370 # this payment wasn't upgraded, which probably means this won't work,
1372 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1373 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1374 $cust_pay->paybatch;
1375 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1378 if ( $gatewaynum ) { #gateway for the payment to be refunded
1380 my $payment_gateway =
1381 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1382 die "payment gateway $gatewaynum not found"
1383 unless $payment_gateway;
1385 $processor = $payment_gateway->gateway_module;
1386 $login = $payment_gateway->gateway_username;
1387 $password = $payment_gateway->gateway_password;
1388 $namespace = $payment_gateway->gateway_namespace;
1389 @bop_options = $payment_gateway->options;
1391 } else { #try the default gateway
1394 my $payment_gateway =
1395 $self->agent->payment_gateway('method' => $options{method});
1397 ( $conf_processor, $login, $password, $namespace ) =
1398 map { my $method = "gateway_$_"; $payment_gateway->$method }
1399 qw( module username password namespace );
1401 @bop_options = $payment_gateway->gatewaynum
1402 ? $payment_gateway->options
1403 : @{ $payment_gateway->get('options') };
1405 return "processor of payment $options{'paynum'} $processor does not".
1406 " match default processor $conf_processor"
1407 unless $processor eq $conf_processor;
1412 } else { # didn't specify a paynum, so look for agent gateway overrides
1413 # like a normal transaction
1415 my $payment_gateway =
1416 $self->agent->payment_gateway( 'method' => $options{method},
1417 #'payinfo' => $payinfo,
1419 my( $processor, $login, $password, $namespace ) =
1420 map { my $method = "gateway_$_"; $payment_gateway->$method }
1421 qw( module username password namespace );
1423 my @bop_options = $payment_gateway->gatewaynum
1424 ? $payment_gateway->options
1425 : @{ $payment_gateway->get('options') };
1428 return "neither amount nor paynum specified" unless $amount;
1430 eval "use $namespace";
1434 'type' => $options{method},
1436 'password' => $password,
1437 'order_number' => $order_number,
1438 'amount' => $amount,
1440 #3.0 is a good a time as any to get rid of this... add a config to pass it
1441 # if anyone still needs it
1442 #'referer' => 'http://cleanwhisker.420.am/',
1444 $content{authorization} = $auth
1445 if length($auth); #echeck/ACH transactions have an order # but no auth
1446 #(at least with authorize.net)
1448 my $currency = $conf->exists('business-onlinepayment-currency')
1449 && $conf->config('business-onlinepayment-currency');
1450 $content{currency} = $currency if $currency;
1452 my $disable_void_after;
1453 if ($conf->exists('disable_void_after')
1454 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1455 $disable_void_after = $1;
1458 #first try void if applicable
1459 my $void = new Business::OnlinePayment( $processor, @bop_options );
1462 if ($void->can('info')) {
1464 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1465 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1466 my %supported_actions = $void->info('supported_actions');
1468 if ( %supported_actions && $paytype
1469 && defined($supported_actions{$paytype})
1470 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1473 if ( $cust_pay && $cust_pay->paid == $amount
1475 ( not defined($disable_void_after) )
1476 || ( time < ($cust_pay->_date + $disable_void_after ) )
1480 warn " attempting void\n" if $DEBUG > 1;
1481 if ( $void->can('info') ) {
1482 if ( $cust_pay->payby eq 'CARD'
1483 && $void->info('CC_void_requires_card') )
1485 $content{'card_number'} = $cust_pay->payinfo;
1486 } elsif ( $cust_pay->payby eq 'CHEK'
1487 && $void->info('ECHECK_void_requires_account') )
1489 ( $content{'account_number'}, $content{'routing_code'} ) =
1490 split('@', $cust_pay->payinfo);
1491 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1494 $void->content( 'action' => 'void', %content );
1495 $void->test_transaction(1)
1496 if $conf->exists('business-onlinepayment-test_transaction');
1498 if ( $void->is_success ) {
1499 my $error = $cust_pay->void($options{'reason'});
1501 # gah, even with transactions.
1502 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1503 "error voiding payment: $error";
1507 warn " void successful\n" if $DEBUG > 1;
1512 warn " void unsuccessful, trying refund\n"
1516 my $address = $self->address1;
1517 $address .= ", ". $self->address2 if $self->address2;
1519 my($payname, $payfirst, $paylast);
1520 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1521 $payname = $self->payname;
1522 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1523 or return "Illegal payname $payname";
1524 ($payfirst, $paylast) = ($1, $2);
1526 $payfirst = $self->getfield('first');
1527 $paylast = $self->getfield('last');
1528 $payname = "$payfirst $paylast";
1531 my @invoicing_list = $self->invoicing_list_emailonly;
1532 if ( $conf->exists('emailinvoiceautoalways')
1533 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1534 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1535 push @invoicing_list, $self->all_emails;
1538 my $email = ($conf->exists('business-onlinepayment-email-override'))
1539 ? $conf->config('business-onlinepayment-email-override')
1540 : $invoicing_list[0];
1542 my $payip = exists($options{'payip'})
1545 $content{customer_ip} = $payip
1549 if ( $options{method} eq 'CC' ) {
1552 $content{card_number} = $payinfo = $cust_pay->payinfo;
1553 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1554 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1555 ($content{expiration} = "$2/$1"); # where available
1557 $content{card_number} = $payinfo = $self->payinfo;
1558 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1559 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1560 $content{expiration} = "$2/$1";
1563 } elsif ( $options{method} eq 'ECHECK' ) {
1566 $payinfo = $cust_pay->payinfo;
1568 $payinfo = $self->payinfo;
1570 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1571 $content{bank_name} = $self->payname;
1572 $content{account_type} = 'CHECKING';
1573 $content{account_name} = $payname;
1574 $content{customer_org} = $self->company ? 'B' : 'I';
1575 $content{customer_ssn} = $self->ss;
1576 } elsif ( $options{method} eq 'LEC' ) {
1577 $content{phone} = $payinfo = $self->payinfo;
1581 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1582 my %sub_content = $refund->content(
1583 'action' => 'credit',
1584 'customer_id' => $self->custnum,
1585 'last_name' => $paylast,
1586 'first_name' => $payfirst,
1588 'address' => $address,
1589 'city' => $self->city,
1590 'state' => $self->state,
1591 'zip' => $self->zip,
1592 'country' => $self->country,
1594 'phone' => $self->daytime || $self->night,
1597 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1599 $refund->test_transaction(1)
1600 if $conf->exists('business-onlinepayment-test_transaction');
1603 return "$processor error: ". $refund->error_message
1604 unless $refund->is_success();
1606 $order_number = $refund->order_number if $refund->can('order_number');
1608 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1609 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1610 last unless @cust_bill_pay;
1611 my $cust_bill_pay = pop @cust_bill_pay;
1612 my $error = $cust_bill_pay->delete;
1616 my $cust_refund = new FS::cust_refund ( {
1617 'custnum' => $self->custnum,
1618 'paynum' => $options{'paynum'},
1619 'refund' => $amount,
1621 'payby' => $bop_method2payby{$options{method}},
1622 'payinfo' => $payinfo,
1623 'reason' => $options{'reason'} || 'card or ACH refund',
1624 'gatewaynum' => $gatewaynum, # may be null
1625 'processor' => $processor,
1626 'auth' => $refund->authorization,
1627 'order_number' => $order_number,
1629 my $error = $cust_refund->insert;
1631 $cust_refund->paynum(''); #try again with no specific paynum
1632 my $error2 = $cust_refund->insert;
1634 # gah, even with transactions.
1635 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1636 "error inserting refund ($processor): $error2".
1637 " (previously tried insert with paynum #$options{'paynum'}" .
1656 L<FS::cust_main>, L<FS::cust_main::Billing>