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> and I<LEC>
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 = (
325 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
328 if (ref($_[0]) eq 'HASH') {
331 my ( $method, $amount ) = ( shift, shift );
333 $options{method} = $method;
334 $options{amount} = $amount;
339 # optional credit card surcharge
342 my $cc_surcharge = 0;
343 my $cc_surcharge_pct = 0;
344 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
345 if $conf->config('credit-card-surcharge-percentage');
347 # always add cc surcharge if called from event
348 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
349 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
350 $options{'amount'} += $cc_surcharge;
351 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
353 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
354 # payment screen), so consider the given
355 # amount as post-surcharge
356 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
359 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
360 $options{'cc_surcharge'} = $cc_surcharge;
364 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
365 warn " cc_surcharge = $cc_surcharge\n";
366 warn " $_ => $options{$_}\n" foreach keys %options;
369 return $self->fake_bop(\%options) if $options{'fake'};
371 $self->_bop_defaults(\%options);
374 # set trans_is_recur based on invnum if there is one
377 my $trans_is_recur = 0;
378 if ( $options{'invnum'} ) {
380 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
381 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
387 $cust_bill->cust_bill_pkg;
390 if grep { $_->freq ne '0' } @part_pkg;
398 my $payment_gateway = $self->_payment_gateway( \%options );
399 my $namespace = $payment_gateway->gateway_namespace;
401 eval "use $namespace";
405 # check for banned credit card/ACH
408 my $ban = FS::banned_pay->ban_search(
409 'payby' => $bop_method2payby{$options{method}},
410 'payinfo' => $options{payinfo},
412 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
415 # check for term discount validity
418 my $discount_term = $options{discount_term};
419 if ( $discount_term ) {
420 my $bill = ($self->cust_bill)[-1]
421 or return "Can't apply a term discount to an unbilled customer";
422 my $plan = FS::discount_plan->new(
424 months => $discount_term
425 ) or return "No discount available for term '$discount_term'";
427 if ( $plan->discounted_total != $options{amount} ) {
428 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
436 my $bop_content = $self->_bop_content(\%options);
437 return $bop_content unless ref($bop_content);
439 my @invoicing_list = $self->invoicing_list_emailonly;
440 if ( $conf->exists('emailinvoiceautoalways')
441 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
442 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
443 push @invoicing_list, $self->all_emails;
446 my $email = ($conf->exists('business-onlinepayment-email-override'))
447 ? $conf->config('business-onlinepayment-email-override')
448 : $invoicing_list[0];
453 if ( $namespace eq 'Business::OnlinePayment' ) {
455 if ( $options{method} eq 'CC' ) {
457 $content{card_number} = $options{payinfo};
458 $paydate = exists($options{'paydate'})
459 ? $options{'paydate'}
461 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
462 $content{expiration} = "$2/$1";
464 my $paycvv = exists($options{'paycvv'})
467 $content{cvv2} = $paycvv
470 my $paystart_month = exists($options{'paystart_month'})
471 ? $options{'paystart_month'}
472 : $self->paystart_month;
474 my $paystart_year = exists($options{'paystart_year'})
475 ? $options{'paystart_year'}
476 : $self->paystart_year;
478 $content{card_start} = "$paystart_month/$paystart_year"
479 if $paystart_month && $paystart_year;
481 my $payissue = exists($options{'payissue'})
482 ? $options{'payissue'}
484 $content{issue_number} = $payissue if $payissue;
486 if ( $self->_bop_recurring_billing(
487 'payinfo' => $options{'payinfo'},
488 'trans_is_recur' => $trans_is_recur,
492 $content{recurring_billing} = 'YES';
493 $content{acct_code} = 'rebill'
494 if $conf->exists('credit_card-recurring_billing_acct_code');
497 } elsif ( $options{method} eq 'ECHECK' ){
499 ( $content{account_number}, $content{routing_code} ) =
500 split('@', $options{payinfo});
501 $content{bank_name} = $options{payname};
502 $content{bank_state} = exists($options{'paystate'})
503 ? $options{'paystate'}
504 : $self->getfield('paystate');
505 $content{account_type}=
506 (exists($options{'paytype'}) && $options{'paytype'})
507 ? uc($options{'paytype'})
508 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
509 $content{account_name} = $self->getfield('first'). ' '.
510 $self->getfield('last');
512 $content{customer_org} = $self->company ? 'B' : 'I';
513 $content{state_id} = exists($options{'stateid'})
514 ? $options{'stateid'}
515 : $self->getfield('stateid');
516 $content{state_id_state} = exists($options{'stateid_state'})
517 ? $options{'stateid_state'}
518 : $self->getfield('stateid_state');
519 $content{customer_ssn} = exists($options{'ss'})
523 } elsif ( $options{method} eq 'LEC' ) {
524 $content{phone} = $options{payinfo};
526 die "unknown method ". $options{method};
529 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
532 die "unknown namespace $namespace";
539 my $balance = exists( $options{'balance'} )
540 ? $options{'balance'}
543 $self->select_for_update; #mutex ... just until we get our pending record in
545 #the checks here are intended to catch concurrent payments
546 #double-form-submission prevention is taken care of in cust_pay_pending::check
549 return "The customer's balance has changed; $options{method} transaction aborted."
550 if $self->balance < $balance;
552 #also check and make sure there aren't *other* pending payments for this cust
554 my @pending = qsearch('cust_pay_pending', {
555 'custnum' => $self->custnum,
556 'status' => { op=>'!=', value=>'done' }
559 #for third-party payments only, remove pending payments if they're in the
560 #'thirdparty' (waiting for customer action) state.
561 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
562 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
563 my $error = $_->delete;
564 warn "error deleting unfinished third-party payment ".
565 $_->paypendingnum . ": $error\n"
568 @pending = grep { $_->status ne 'thirdparty' } @pending;
571 return "A payment is already being processed for this customer (".
572 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
573 "); $options{method} transaction aborted."
576 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
578 my $cust_pay_pending = new FS::cust_pay_pending {
579 'custnum' => $self->custnum,
580 'paid' => $options{amount},
582 'payby' => $bop_method2payby{$options{method}},
583 'payinfo' => $options{payinfo},
584 'paydate' => $paydate,
585 'recurring_billing' => $content{recurring_billing},
586 'pkgnum' => $options{'pkgnum'},
588 'gatewaynum' => $payment_gateway->gatewaynum || '',
589 'session_id' => $options{session_id} || '',
590 'jobnum' => $options{depend_jobnum} || '',
592 $cust_pay_pending->payunique( $options{payunique} )
593 if defined($options{payunique}) && length($options{payunique});
594 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
595 return $cpp_new_err if $cpp_new_err;
597 my( $action1, $action2 ) =
598 split( /\s*\,\s*/, $payment_gateway->gateway_action );
600 my $transaction = new $namespace( $payment_gateway->gateway_module,
601 $self->_bop_options(\%options),
604 $transaction->content(
605 'type' => $options{method},
606 $self->_bop_auth(\%options),
607 'action' => $action1,
608 'description' => $options{'description'},
609 'amount' => $options{amount},
610 #'invoice_number' => $options{'invnum'},
611 'customer_id' => $self->custnum,
613 'reference' => $cust_pay_pending->paypendingnum, #for now
614 'callback_url' => $payment_gateway->gateway_callback_url,
619 $cust_pay_pending->status('pending');
620 my $cpp_pending_err = $cust_pay_pending->replace;
621 return $cpp_pending_err if $cpp_pending_err;
623 warn Dumper($transaction) if $DEBUG > 2;
625 unless ( $BOP_TESTING ) {
626 $transaction->test_transaction(1)
627 if $conf->exists('business-onlinepayment-test_transaction');
628 $transaction->submit();
630 if ( $BOP_TESTING_SUCCESS ) {
631 $transaction->is_success(1);
632 $transaction->authorization('fake auth');
634 $transaction->is_success(0);
635 $transaction->error_message('fake failure');
639 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
641 $cust_pay_pending->status('thirdparty');
642 my $cpp_err = $cust_pay_pending->replace;
643 return { error => $cpp_err } if $cpp_err;
644 return { reference => $cust_pay_pending->paypendingnum,
645 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
647 } elsif ( $transaction->is_success() && $action2 ) {
649 $cust_pay_pending->status('authorized');
650 my $cpp_authorized_err = $cust_pay_pending->replace;
651 return $cpp_authorized_err if $cpp_authorized_err;
653 my $auth = $transaction->authorization;
654 my $ordernum = $transaction->can('order_number')
655 ? $transaction->order_number
659 new Business::OnlinePayment( $payment_gateway->gateway_module,
660 $self->_bop_options(\%options),
665 type => $options{method},
667 $self->_bop_auth(\%options),
668 order_number => $ordernum,
669 amount => $options{amount},
670 authorization => $auth,
671 description => $options{'description'},
674 foreach my $field (qw( authorization_source_code returned_ACI
675 transaction_identifier validation_code
676 transaction_sequence_num local_transaction_date
677 local_transaction_time AVS_result_code )) {
678 $capture{$field} = $transaction->$field() if $transaction->can($field);
681 $capture->content( %capture );
683 $capture->test_transaction(1)
684 if $conf->exists('business-onlinepayment-test_transaction');
687 unless ( $capture->is_success ) {
688 my $e = "Authorization successful but capture failed, custnum #".
689 $self->custnum. ': '. $capture->result_code.
690 ": ". $capture->error_message;
698 # remove paycvv after initial transaction
701 #false laziness w/misc/process/payment.cgi - check both to make sure working
703 if ( length($self->paycvv)
704 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
706 my $error = $self->remove_cvv;
708 warn "WARNING: error removing cvv: $error\n";
717 if ( $transaction->can('card_token') && $transaction->card_token ) {
719 $self->card_token($transaction->card_token);
721 if ( $options{'payinfo'} eq $self->payinfo ) {
722 $self->payinfo($transaction->card_token);
723 my $error = $self->replace;
725 warn "WARNING: error storing token: $error, but proceeding anyway\n";
735 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
747 if (ref($_[0]) eq 'HASH') {
750 my ( $method, $amount ) = ( shift, shift );
752 $options{method} = $method;
753 $options{amount} = $amount;
756 if ( $options{'fake_failure'} ) {
757 return "Error: No error; test failure requested with fake_failure";
760 my $cust_pay = new FS::cust_pay ( {
761 'custnum' => $self->custnum,
762 'invnum' => $options{'invnum'},
763 'paid' => $options{amount},
765 'payby' => $bop_method2payby{$options{method}},
766 #'payinfo' => $payinfo,
767 'payinfo' => '4111111111111111',
768 #'paydate' => $paydate,
769 'paydate' => '2012-05-01',
770 'processor' => 'FakeProcessor',
772 'order_number' => '32',
774 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
777 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
778 warn " $_ => $options{$_}\n" foreach keys %options;
781 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
784 $cust_pay->invnum(''); #try again with no specific invnum
785 my $error2 = $cust_pay->insert( $options{'manual'} ?
786 ( 'manual' => 1 ) : ()
789 # gah, even with transactions.
790 my $e = 'WARNING: Card/ACH debited but database not updated - '.
791 "error inserting (fake!) payment: $error2".
792 " (previously tried insert with invnum #$options{'invnum'}" .
799 if ( $options{'paynum_ref'} ) {
800 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
808 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
810 # Wraps up processing of a realtime credit card, ACH (electronic check) or
811 # phone bill transaction.
813 sub _realtime_bop_result {
814 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
816 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
819 warn "$me _realtime_bop_result: pending transaction ".
820 $cust_pay_pending->paypendingnum. "\n";
821 warn " $_ => $options{$_}\n" foreach keys %options;
824 my $payment_gateway = $options{payment_gateway}
825 or return "no payment gateway in arguments to _realtime_bop_result";
827 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
828 my $cpp_captured_err = $cust_pay_pending->replace;
829 return $cpp_captured_err if $cpp_captured_err;
831 if ( $transaction->is_success() ) {
833 my $order_number = $transaction->order_number
834 if $transaction->can('order_number');
836 my $cust_pay = new FS::cust_pay ( {
837 'custnum' => $self->custnum,
838 'invnum' => $options{'invnum'},
839 'paid' => $cust_pay_pending->paid,
841 'payby' => $cust_pay_pending->payby,
842 'payinfo' => $options{'payinfo'},
843 'paydate' => $cust_pay_pending->paydate,
844 'pkgnum' => $cust_pay_pending->pkgnum,
845 'discount_term' => $options{'discount_term'},
846 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
847 'processor' => $payment_gateway->gateway_module,
848 'auth' => $transaction->authorization,
849 'order_number' => $order_number || '',
852 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
853 $cust_pay->payunique( $options{payunique} )
854 if defined($options{payunique}) && length($options{payunique});
856 my $oldAutoCommit = $FS::UID::AutoCommit;
857 local $FS::UID::AutoCommit = 0;
860 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
862 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
865 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
866 $cust_pay->invnum(''); #try again with no specific invnum
867 $cust_pay->paynum('');
868 my $error2 = $cust_pay->insert( $options{'manual'} ?
869 ( 'manual' => 1 ) : ()
872 # gah. but at least we have a record of the state we had to abort in
873 # from cust_pay_pending now.
874 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
875 my $e = "WARNING: $options{method} captured but payment not recorded -".
876 " error inserting payment (". $payment_gateway->gateway_module.
878 " (previously tried insert with invnum #$options{'invnum'}" .
879 ": $error ) - pending payment saved as paypendingnum ".
880 $cust_pay_pending->paypendingnum. "\n";
886 my $jobnum = $cust_pay_pending->jobnum;
888 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
890 unless ( $placeholder ) {
891 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
892 my $e = "WARNING: $options{method} captured but job $jobnum not ".
893 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
898 $error = $placeholder->delete;
901 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
902 my $e = "WARNING: $options{method} captured but could not delete ".
903 "job $jobnum for paypendingnum ".
904 $cust_pay_pending->paypendingnum. ": $error\n";
911 if ( $options{'paynum_ref'} ) {
912 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
915 $cust_pay_pending->status('done');
916 $cust_pay_pending->statustext('captured');
917 $cust_pay_pending->paynum($cust_pay->paynum);
918 my $cpp_done_err = $cust_pay_pending->replace;
920 if ( $cpp_done_err ) {
922 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
923 my $e = "WARNING: $options{method} captured but payment not recorded - ".
924 "error updating status for paypendingnum ".
925 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
931 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
933 if ( $options{'apply'} ) {
934 my $apply_error = $self->apply_payments_and_credits;
935 if ( $apply_error ) {
936 warn "WARNING: error applying payment: $apply_error\n";
937 #but we still should return no error cause the payment otherwise went
942 # have a CC surcharge portion --> one-time charge
943 if ( $options{'cc_surcharge'} > 0 ) {
944 # XXX: this whole block needs to be in a transaction?
947 $invnum = $options{'invnum'} if $options{'invnum'};
948 unless ( $invnum ) { # probably from a payment screen
949 # do we have any open invoices? pick earliest
950 # uses the fact that cust_main->cust_bill sorts by date ascending
951 my @open = $self->open_cust_bill;
952 $invnum = $open[0]->invnum if scalar(@open);
955 unless ( $invnum ) { # still nothing? pick last closed invoice
956 # again uses fact that cust_main->cust_bill sorts by date ascending
957 my @closed = $self->cust_bill;
958 $invnum = $closed[$#closed]->invnum if scalar(@closed);
962 # XXX: unlikely case - pre-paying before any invoices generated
963 # what it should do is create a new invoice and pick it
964 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
969 my $charge_error = $self->charge({
970 'amount' => $options{'cc_surcharge'},
971 'pkg' => 'Credit Card Surcharge',
973 'cust_pkg_ref' => \$cust_pkg,
976 warn 'Unable to add CC surcharge cust_pkg';
980 $cust_pkg->setup(time);
981 my $cp_error = $cust_pkg->replace;
983 warn 'Unable to set setup time on cust_pkg for cc surcharge';
987 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
988 unless ( $cust_bill ) {
989 warn "race condition + invoice deletion just happened";
994 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
996 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1000 return ''; #no error
1006 my $perror = $payment_gateway->gateway_module. " error: ".
1007 $transaction->error_message;
1009 my $jobnum = $cust_pay_pending->jobnum;
1011 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1013 if ( $placeholder ) {
1014 my $error = $placeholder->depended_delete;
1015 $error ||= $placeholder->delete;
1016 warn "error removing provisioning jobs after declined paypendingnum ".
1017 $cust_pay_pending->paypendingnum. ": $error\n";
1019 my $e = "error finding job $jobnum for declined paypendingnum ".
1020 $cust_pay_pending->paypendingnum. "\n";
1026 unless ( $transaction->error_message ) {
1029 if ( $transaction->can('response_page') ) {
1031 'page' => ( $transaction->can('response_page')
1032 ? $transaction->response_page
1035 'code' => ( $transaction->can('response_code')
1036 ? $transaction->response_code
1039 'headers' => ( $transaction->can('response_headers')
1040 ? $transaction->response_headers
1046 "No additional debugging information available for ".
1047 $payment_gateway->gateway_module;
1050 $perror .= "No error_message returned from ".
1051 $payment_gateway->gateway_module. " -- ".
1052 ( ref($t_response) ? Dumper($t_response) : $t_response );
1056 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1057 && $conf->exists('emaildecline', $self->agentnum)
1058 && grep { $_ ne 'POST' } $self->invoicing_list
1059 && ! grep { $transaction->error_message =~ /$_/ }
1060 $conf->config('emaildecline-exclude', $self->agentnum)
1063 # Send a decline alert to the customer.
1064 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1067 # include the raw error message in the transaction state
1068 $cust_pay_pending->setfield('error', $transaction->error_message);
1069 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1070 $error = $msg_template->send( 'cust_main' => $self,
1071 'object' => $cust_pay_pending );
1075 my @templ = $conf->config('declinetemplate');
1076 my $template = new Text::Template (
1078 SOURCE => [ map "$_\n", @templ ],
1079 ) or return "($perror) can't create template: $Text::Template::ERROR";
1080 $template->compile()
1081 or return "($perror) can't compile template: $Text::Template::ERROR";
1085 scalar( $conf->config('company_name', $self->agentnum ) ),
1086 'company_address' =>
1087 join("\n", $conf->config('company_address', $self->agentnum ) ),
1088 'error' => $transaction->error_message,
1091 my $error = send_email(
1092 'from' => $conf->config('invoice_from', $self->agentnum ),
1093 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1094 'subject' => 'Your payment could not be processed',
1095 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1099 $perror .= " (also received error sending decline notification: $error)"
1104 $cust_pay_pending->status('done');
1105 $cust_pay_pending->statustext("declined: $perror");
1106 my $cpp_done_err = $cust_pay_pending->replace;
1107 if ( $cpp_done_err ) {
1108 my $e = "WARNING: $options{method} declined but pending payment not ".
1109 "resolved - error updating status for paypendingnum ".
1110 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1112 $perror = "$e ($perror)";
1120 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1122 Verifies successful third party processing of a realtime credit card,
1123 ACH (electronic check) or phone bill transaction via a
1124 Business::OnlineThirdPartyPayment realtime gateway. See
1125 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1127 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1129 The additional options I<payname>, I<city>, I<state>,
1130 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1131 if set, will override the value from the customer record.
1133 I<description> is a free-text field passed to the gateway. It defaults to
1134 "Internet services".
1136 If an I<invnum> is specified, this payment (if successful) is applied to the
1137 specified invoice. If you don't specify an I<invnum> you might want to
1138 call the B<apply_payments> method.
1140 I<quiet> can be set true to surpress email decline notices.
1142 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1143 resulting paynum, if any.
1145 I<payunique> is a unique identifier for this payment.
1147 Returns a hashref containing elements bill_error (which will be undefined
1148 upon success) and session_id of any associated session.
1152 sub realtime_botpp_capture {
1153 my( $self, $cust_pay_pending, %options ) = @_;
1155 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1158 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1159 warn " $_ => $options{$_}\n" foreach keys %options;
1162 eval "use Business::OnlineThirdPartyPayment";
1166 # select the gateway
1169 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1171 my $payment_gateway;
1172 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1173 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1174 { gatewaynum => $gatewaynum }
1176 : $self->agent->payment_gateway( 'method' => $method,
1177 # 'invnum' => $cust_pay_pending->invnum,
1178 # 'payinfo' => $cust_pay_pending->payinfo,
1181 $options{payment_gateway} = $payment_gateway; # for the helper subs
1187 my @invoicing_list = $self->invoicing_list_emailonly;
1188 if ( $conf->exists('emailinvoiceautoalways')
1189 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1190 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1191 push @invoicing_list, $self->all_emails;
1194 my $email = ($conf->exists('business-onlinepayment-email-override'))
1195 ? $conf->config('business-onlinepayment-email-override')
1196 : $invoicing_list[0];
1200 $content{email_customer} =
1201 ( $conf->exists('business-onlinepayment-email_customer')
1202 || $conf->exists('business-onlinepayment-email-override') );
1205 # run transaction(s)
1209 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1210 $self->_bop_options(\%options),
1213 $transaction->reference({ %options });
1215 $transaction->content(
1217 $self->_bop_auth(\%options),
1218 'action' => 'Post Authorization',
1219 'description' => $options{'description'},
1220 'amount' => $cust_pay_pending->paid,
1221 #'invoice_number' => $options{'invnum'},
1222 'customer_id' => $self->custnum,
1224 #3.0 is a good a time as any to get rid of this... add a config to pass it
1225 # if anyone still needs it
1226 #'referer' => 'http://cleanwhisker.420.am/',
1228 'reference' => $cust_pay_pending->paypendingnum,
1230 'phone' => $self->daytime || $self->night,
1232 # plus whatever is required for bogus capture avoidance
1235 $transaction->submit();
1238 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1240 if ( $options{'apply'} ) {
1241 my $apply_error = $self->apply_payments_and_credits;
1242 if ( $apply_error ) {
1243 warn "WARNING: error applying payment: $apply_error\n";
1248 bill_error => $error,
1249 session_id => $cust_pay_pending->session_id,
1254 =item default_payment_gateway
1256 DEPRECATED -- use agent->payment_gateway
1260 sub default_payment_gateway {
1261 my( $self, $method ) = @_;
1263 die "Real-time processing not enabled\n"
1264 unless $conf->exists('business-onlinepayment');
1266 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1269 my $bop_config = 'business-onlinepayment';
1270 $bop_config .= '-ach'
1271 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1272 my ( $processor, $login, $password, $action, @bop_options ) =
1273 $conf->config($bop_config);
1274 $action ||= 'normal authorization';
1275 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1276 die "No real-time processor is enabled - ".
1277 "did you set the business-onlinepayment configuration value?\n"
1280 ( $processor, $login, $password, $action, @bop_options )
1283 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1285 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1286 via a Business::OnlinePayment realtime gateway. See
1287 L<http://420.am/business-onlinepayment> for supported gateways.
1289 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1291 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1293 Most gateways require a reference to an original payment transaction to refund,
1294 so you probably need to specify a I<paynum>.
1296 I<amount> defaults to the original amount of the payment if not specified.
1298 I<reason> specifies a reason for the refund.
1300 I<paydate> specifies the expiration date for a credit card overriding the
1301 value from the customer record or the payment record. Specified as yyyy-mm-dd
1303 Implementation note: If I<amount> is unspecified or equal to the amount of the
1304 orignal payment, first an attempt is made to "void" the transaction via
1305 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1306 the normal attempt is made to "refund" ("credit") the transaction via the
1307 gateway is attempted. No attempt to "void" the transaction is made if the
1308 gateway has introspection data and doesn't support void.
1310 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1311 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1312 #if set, will override the value from the customer record.
1314 #If an I<invnum> is specified, this payment (if successful) is applied to the
1315 #specified invoice. If you don't specify an I<invnum> you might want to
1316 #call the B<apply_payments> method.
1320 #some false laziness w/realtime_bop, not enough to make it worth merging
1321 #but some useful small subs should be pulled out
1322 sub realtime_refund_bop {
1325 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1328 if (ref($_[0]) eq 'HASH') {
1329 %options = %{$_[0]};
1333 $options{method} = $method;
1337 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1338 warn " $_ => $options{$_}\n" foreach keys %options;
1342 # look up the original payment and optionally a gateway for that payment
1346 my $amount = $options{'amount'};
1348 my( $processor, $login, $password, @bop_options, $namespace ) ;
1349 my( $auth, $order_number ) = ( '', '', '' );
1350 my $gatewaynum = '';
1352 if ( $options{'paynum'} ) {
1354 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1355 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1356 or return "Unknown paynum $options{'paynum'}";
1357 $amount ||= $cust_pay->paid;
1359 if ( $cust_pay->get('processor') ) {
1360 ($gatewaynum, $processor, $auth, $order_number) =
1362 $cust_pay->gatewaynum,
1363 $cust_pay->processor,
1365 $cust_pay->order_number,
1368 # this payment wasn't upgraded, which probably means this won't work,
1370 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1371 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1372 $cust_pay->paybatch;
1373 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1376 if ( $gatewaynum ) { #gateway for the payment to be refunded
1378 my $payment_gateway =
1379 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1380 die "payment gateway $gatewaynum not found"
1381 unless $payment_gateway;
1383 $processor = $payment_gateway->gateway_module;
1384 $login = $payment_gateway->gateway_username;
1385 $password = $payment_gateway->gateway_password;
1386 $namespace = $payment_gateway->gateway_namespace;
1387 @bop_options = $payment_gateway->options;
1389 } else { #try the default gateway
1392 my $payment_gateway =
1393 $self->agent->payment_gateway('method' => $options{method});
1395 ( $conf_processor, $login, $password, $namespace ) =
1396 map { my $method = "gateway_$_"; $payment_gateway->$method }
1397 qw( module username password namespace );
1399 @bop_options = $payment_gateway->gatewaynum
1400 ? $payment_gateway->options
1401 : @{ $payment_gateway->get('options') };
1403 return "processor of payment $options{'paynum'} $processor does not".
1404 " match default processor $conf_processor"
1405 unless $processor eq $conf_processor;
1410 } else { # didn't specify a paynum, so look for agent gateway overrides
1411 # like a normal transaction
1413 my $payment_gateway =
1414 $self->agent->payment_gateway( 'method' => $options{method},
1415 #'payinfo' => $payinfo,
1417 my( $processor, $login, $password, $namespace ) =
1418 map { my $method = "gateway_$_"; $payment_gateway->$method }
1419 qw( module username password namespace );
1421 my @bop_options = $payment_gateway->gatewaynum
1422 ? $payment_gateway->options
1423 : @{ $payment_gateway->get('options') };
1426 return "neither amount nor paynum specified" unless $amount;
1428 eval "use $namespace";
1432 'type' => $options{method},
1434 'password' => $password,
1435 'order_number' => $order_number,
1436 'amount' => $amount,
1438 #3.0 is a good a time as any to get rid of this... add a config to pass it
1439 # if anyone still needs it
1440 #'referer' => 'http://cleanwhisker.420.am/',
1442 $content{authorization} = $auth
1443 if length($auth); #echeck/ACH transactions have an order # but no auth
1444 #(at least with authorize.net)
1446 my $currency = $conf->exists('business-onlinepayment-currency')
1447 && $conf->config('business-onlinepayment-currency');
1448 $content{currency} = $currency if $currency;
1450 my $disable_void_after;
1451 if ($conf->exists('disable_void_after')
1452 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1453 $disable_void_after = $1;
1456 #first try void if applicable
1457 my $void = new Business::OnlinePayment( $processor, @bop_options );
1460 if ($void->can('info')) {
1462 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1463 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1464 my %supported_actions = $void->info('supported_actions');
1466 if ( %supported_actions && $paytype
1467 && defined($supported_actions{$paytype})
1468 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1471 if ( $cust_pay && $cust_pay->paid == $amount
1473 ( not defined($disable_void_after) )
1474 || ( time < ($cust_pay->_date + $disable_void_after ) )
1478 warn " attempting void\n" if $DEBUG > 1;
1479 if ( $void->can('info') ) {
1480 if ( $cust_pay->payby eq 'CARD'
1481 && $void->info('CC_void_requires_card') )
1483 $content{'card_number'} = $cust_pay->payinfo;
1484 } elsif ( $cust_pay->payby eq 'CHEK'
1485 && $void->info('ECHECK_void_requires_account') )
1487 ( $content{'account_number'}, $content{'routing_code'} ) =
1488 split('@', $cust_pay->payinfo);
1489 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1492 $void->content( 'action' => 'void', %content );
1493 $void->test_transaction(1)
1494 if $conf->exists('business-onlinepayment-test_transaction');
1496 if ( $void->is_success ) {
1497 my $error = $cust_pay->void($options{'reason'});
1499 # gah, even with transactions.
1500 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1501 "error voiding payment: $error";
1505 warn " void successful\n" if $DEBUG > 1;
1510 warn " void unsuccessful, trying refund\n"
1514 my $address = $self->address1;
1515 $address .= ", ". $self->address2 if $self->address2;
1517 my($payname, $payfirst, $paylast);
1518 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1519 $payname = $self->payname;
1520 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1521 or return "Illegal payname $payname";
1522 ($payfirst, $paylast) = ($1, $2);
1524 $payfirst = $self->getfield('first');
1525 $paylast = $self->getfield('last');
1526 $payname = "$payfirst $paylast";
1529 my @invoicing_list = $self->invoicing_list_emailonly;
1530 if ( $conf->exists('emailinvoiceautoalways')
1531 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1532 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1533 push @invoicing_list, $self->all_emails;
1536 my $email = ($conf->exists('business-onlinepayment-email-override'))
1537 ? $conf->config('business-onlinepayment-email-override')
1538 : $invoicing_list[0];
1540 my $payip = exists($options{'payip'})
1543 $content{customer_ip} = $payip
1547 if ( $options{method} eq 'CC' ) {
1550 $content{card_number} = $payinfo = $cust_pay->payinfo;
1551 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1552 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1553 ($content{expiration} = "$2/$1"); # where available
1555 $content{card_number} = $payinfo = $self->payinfo;
1556 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1557 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1558 $content{expiration} = "$2/$1";
1561 } elsif ( $options{method} eq 'ECHECK' ) {
1564 $payinfo = $cust_pay->payinfo;
1566 $payinfo = $self->payinfo;
1568 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1569 $content{bank_name} = $self->payname;
1570 $content{account_type} = 'CHECKING';
1571 $content{account_name} = $payname;
1572 $content{customer_org} = $self->company ? 'B' : 'I';
1573 $content{customer_ssn} = $self->ss;
1574 } elsif ( $options{method} eq 'LEC' ) {
1575 $content{phone} = $payinfo = $self->payinfo;
1579 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1580 my %sub_content = $refund->content(
1581 'action' => 'credit',
1582 'customer_id' => $self->custnum,
1583 'last_name' => $paylast,
1584 'first_name' => $payfirst,
1586 'address' => $address,
1587 'city' => $self->city,
1588 'state' => $self->state,
1589 'zip' => $self->zip,
1590 'country' => $self->country,
1592 'phone' => $self->daytime || $self->night,
1595 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1597 $refund->test_transaction(1)
1598 if $conf->exists('business-onlinepayment-test_transaction');
1601 return "$processor error: ". $refund->error_message
1602 unless $refund->is_success();
1604 $order_number = $refund->order_number if $refund->can('order_number');
1606 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1607 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1608 last unless @cust_bill_pay;
1609 my $cust_bill_pay = pop @cust_bill_pay;
1610 my $error = $cust_bill_pay->delete;
1614 my $cust_refund = new FS::cust_refund ( {
1615 'custnum' => $self->custnum,
1616 'paynum' => $options{'paynum'},
1617 'refund' => $amount,
1619 'payby' => $bop_method2payby{$options{method}},
1620 'payinfo' => $payinfo,
1621 'reason' => $options{'reason'} || 'card or ACH refund',
1622 'gatewaynum' => $gatewaynum, # may be null
1623 'processor' => $processor,
1624 'auth' => $refund->authorization,
1625 'order_number' => $order_number,
1627 my $error = $cust_refund->insert;
1629 $cust_refund->paynum(''); #try again with no specific paynum
1630 my $error2 = $cust_refund->insert;
1632 # gah, even with transactions.
1633 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1634 "error inserting refund ($processor): $error2".
1635 " (previously tried insert with paynum #$options{'paynum'}" .
1654 L<FS::cust_main>, L<FS::cust_main::Billing>