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 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
304 $content{phone} = $self->daytime || $self->night;
306 my $currency = $conf->exists('business-onlinepayment-currency')
307 && $conf->config('business-onlinepayment-currency');
308 $content{currency} = $currency if $currency;
313 my %bop_method2payby = (
322 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
325 if (ref($_[0]) eq 'HASH') {
328 my ( $method, $amount ) = ( shift, shift );
330 $options{method} = $method;
331 $options{amount} = $amount;
336 # optional credit card surcharge
339 my $cc_surcharge = 0;
340 my $cc_surcharge_pct = 0;
341 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
342 if $conf->config('credit-card-surcharge-percentage');
344 # always add cc surcharge if called from event
345 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
346 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
347 $options{'amount'} += $cc_surcharge;
348 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
350 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
351 # payment screen), so consider the given
352 # amount as post-surcharge
353 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
356 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
357 $options{'cc_surcharge'} = $cc_surcharge;
361 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
362 warn " cc_surcharge = $cc_surcharge\n";
363 warn " $_ => $options{$_}\n" foreach keys %options;
366 return $self->fake_bop(\%options) if $options{'fake'};
368 $self->_bop_defaults(\%options);
371 # set trans_is_recur based on invnum if there is one
374 my $trans_is_recur = 0;
375 if ( $options{'invnum'} ) {
377 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
378 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
384 $cust_bill->cust_bill_pkg;
387 if grep { $_->freq ne '0' } @part_pkg;
395 my $payment_gateway = $self->_payment_gateway( \%options );
396 my $namespace = $payment_gateway->gateway_namespace;
398 eval "use $namespace";
402 # check for banned credit card/ACH
405 my $ban = FS::banned_pay->ban_search(
406 'payby' => $bop_method2payby{$options{method}},
407 'payinfo' => $options{payinfo},
409 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
412 # check for term discount validity
415 my $discount_term = $options{discount_term};
416 if ( $discount_term ) {
417 my $bill = ($self->cust_bill)[-1]
418 or return "Can't apply a term discount to an unbilled customer";
419 my $plan = FS::discount_plan->new(
421 months => $discount_term
422 ) or return "No discount available for term '$discount_term'";
424 if ( $plan->discounted_total != $options{amount} ) {
425 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
433 my $bop_content = $self->_bop_content(\%options);
434 return $bop_content unless ref($bop_content);
436 my @invoicing_list = $self->invoicing_list_emailonly;
437 if ( $conf->exists('emailinvoiceautoalways')
438 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
439 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
440 push @invoicing_list, $self->all_emails;
443 my $email = ($conf->exists('business-onlinepayment-email-override'))
444 ? $conf->config('business-onlinepayment-email-override')
445 : $invoicing_list[0];
450 if ( $namespace eq 'Business::OnlinePayment' ) {
452 if ( $options{method} eq 'CC' ) {
454 $content{card_number} = $options{payinfo};
455 $paydate = exists($options{'paydate'})
456 ? $options{'paydate'}
458 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
459 $content{expiration} = "$2/$1";
461 my $paycvv = exists($options{'paycvv'})
464 $content{cvv2} = $paycvv
467 my $paystart_month = exists($options{'paystart_month'})
468 ? $options{'paystart_month'}
469 : $self->paystart_month;
471 my $paystart_year = exists($options{'paystart_year'})
472 ? $options{'paystart_year'}
473 : $self->paystart_year;
475 $content{card_start} = "$paystart_month/$paystart_year"
476 if $paystart_month && $paystart_year;
478 my $payissue = exists($options{'payissue'})
479 ? $options{'payissue'}
481 $content{issue_number} = $payissue if $payissue;
483 if ( $self->_bop_recurring_billing(
484 'payinfo' => $options{'payinfo'},
485 'trans_is_recur' => $trans_is_recur,
489 $content{recurring_billing} = 'YES';
490 $content{acct_code} = 'rebill'
491 if $conf->exists('credit_card-recurring_billing_acct_code');
494 } elsif ( $options{method} eq 'ECHECK' ){
496 ( $content{account_number}, $content{routing_code} ) =
497 split('@', $options{payinfo});
498 $content{bank_name} = $options{payname};
499 $content{bank_state} = exists($options{'paystate'})
500 ? $options{'paystate'}
501 : $self->getfield('paystate');
502 $content{account_type}=
503 (exists($options{'paytype'}) && $options{'paytype'})
504 ? uc($options{'paytype'})
505 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
506 $content{account_name} = $self->getfield('first'). ' '.
507 $self->getfield('last');
509 $content{customer_org} = $self->company ? 'B' : 'I';
510 $content{state_id} = exists($options{'stateid'})
511 ? $options{'stateid'}
512 : $self->getfield('stateid');
513 $content{state_id_state} = exists($options{'stateid_state'})
514 ? $options{'stateid_state'}
515 : $self->getfield('stateid_state');
516 $content{customer_ssn} = exists($options{'ss'})
520 } elsif ( $options{method} eq 'LEC' ) {
521 $content{phone} = $options{payinfo};
523 die "unknown method ". $options{method};
526 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
529 die "unknown namespace $namespace";
536 my $balance = exists( $options{'balance'} )
537 ? $options{'balance'}
540 $self->select_for_update; #mutex ... just until we get our pending record in
542 #the checks here are intended to catch concurrent payments
543 #double-form-submission prevention is taken care of in cust_pay_pending::check
546 return "The customer's balance has changed; $options{method} transaction aborted."
547 if $self->balance < $balance;
549 #also check and make sure there aren't *other* pending payments for this cust
551 my @pending = qsearch('cust_pay_pending', {
552 'custnum' => $self->custnum,
553 'status' => { op=>'!=', value=>'done' }
556 #for third-party payments only, remove pending payments if they're in the
557 #'thirdparty' (waiting for customer action) state.
558 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
559 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
560 my $error = $_->delete;
561 warn "error deleting unfinished third-party payment ".
562 $_->paypendingnum . ": $error\n"
565 @pending = grep { $_->status ne 'thirdparty' } @pending;
568 return "A payment is already being processed for this customer (".
569 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
570 "); $options{method} transaction aborted."
573 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
575 my $cust_pay_pending = new FS::cust_pay_pending {
576 'custnum' => $self->custnum,
577 'paid' => $options{amount},
579 'payby' => $bop_method2payby{$options{method}},
580 'payinfo' => $options{payinfo},
581 'paydate' => $paydate,
582 'recurring_billing' => $content{recurring_billing},
583 'pkgnum' => $options{'pkgnum'},
585 'gatewaynum' => $payment_gateway->gatewaynum || '',
586 'session_id' => $options{session_id} || '',
587 'jobnum' => $options{depend_jobnum} || '',
589 $cust_pay_pending->payunique( $options{payunique} )
590 if defined($options{payunique}) && length($options{payunique});
591 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
592 return $cpp_new_err if $cpp_new_err;
594 my( $action1, $action2 ) =
595 split( /\s*\,\s*/, $payment_gateway->gateway_action );
597 my $transaction = new $namespace( $payment_gateway->gateway_module,
598 $self->_bop_options(\%options),
601 $transaction->content(
602 'type' => $options{method},
603 $self->_bop_auth(\%options),
604 'action' => $action1,
605 'description' => $options{'description'},
606 'amount' => $options{amount},
607 #'invoice_number' => $options{'invnum'},
608 'customer_id' => $self->custnum,
610 'reference' => $cust_pay_pending->paypendingnum, #for now
611 'callback_url' => $payment_gateway->gateway_callback_url,
616 $cust_pay_pending->status('pending');
617 my $cpp_pending_err = $cust_pay_pending->replace;
618 return $cpp_pending_err if $cpp_pending_err;
620 warn Dumper($transaction) if $DEBUG > 2;
622 unless ( $BOP_TESTING ) {
623 $transaction->test_transaction(1)
624 if $conf->exists('business-onlinepayment-test_transaction');
625 $transaction->submit();
627 if ( $BOP_TESTING_SUCCESS ) {
628 $transaction->is_success(1);
629 $transaction->authorization('fake auth');
631 $transaction->is_success(0);
632 $transaction->error_message('fake failure');
636 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
638 $cust_pay_pending->status('thirdparty');
639 my $cpp_err = $cust_pay_pending->replace;
640 return { error => $cpp_err } if $cpp_err;
641 return { reference => $cust_pay_pending->paypendingnum,
642 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
644 } elsif ( $transaction->is_success() && $action2 ) {
646 $cust_pay_pending->status('authorized');
647 my $cpp_authorized_err = $cust_pay_pending->replace;
648 return $cpp_authorized_err if $cpp_authorized_err;
650 my $auth = $transaction->authorization;
651 my $ordernum = $transaction->can('order_number')
652 ? $transaction->order_number
656 new Business::OnlinePayment( $payment_gateway->gateway_module,
657 $self->_bop_options(\%options),
662 type => $options{method},
664 $self->_bop_auth(\%options),
665 order_number => $ordernum,
666 amount => $options{amount},
667 authorization => $auth,
668 description => $options{'description'},
671 foreach my $field (qw( authorization_source_code returned_ACI
672 transaction_identifier validation_code
673 transaction_sequence_num local_transaction_date
674 local_transaction_time AVS_result_code )) {
675 $capture{$field} = $transaction->$field() if $transaction->can($field);
678 $capture->content( %capture );
680 $capture->test_transaction(1)
681 if $conf->exists('business-onlinepayment-test_transaction');
684 unless ( $capture->is_success ) {
685 my $e = "Authorization successful but capture failed, custnum #".
686 $self->custnum. ': '. $capture->result_code.
687 ": ". $capture->error_message;
695 # remove paycvv after initial transaction
698 #false laziness w/misc/process/payment.cgi - check both to make sure working
700 if ( length($self->paycvv)
701 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
703 my $error = $self->remove_cvv;
705 warn "WARNING: error removing cvv: $error\n";
714 if ( $transaction->can('card_token') && $transaction->card_token ) {
716 $self->card_token($transaction->card_token);
718 if ( $options{'payinfo'} eq $self->payinfo ) {
719 $self->payinfo($transaction->card_token);
720 my $error = $self->replace;
722 warn "WARNING: error storing token: $error, but proceeding anyway\n";
732 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
744 if (ref($_[0]) eq 'HASH') {
747 my ( $method, $amount ) = ( shift, shift );
749 $options{method} = $method;
750 $options{amount} = $amount;
753 if ( $options{'fake_failure'} ) {
754 return "Error: No error; test failure requested with fake_failure";
758 #if ( $payment_gateway->gatewaynum ) { # agent override
759 # $paybatch = $payment_gateway->gatewaynum. '-';
762 #$paybatch .= "$processor:". $transaction->authorization;
764 #$paybatch .= ':'. $transaction->order_number
765 # if $transaction->can('order_number')
766 # && length($transaction->order_number);
768 my $paybatch = 'FakeProcessor:54:32';
770 my $cust_pay = new FS::cust_pay ( {
771 'custnum' => $self->custnum,
772 'invnum' => $options{'invnum'},
773 'paid' => $options{amount},
775 'payby' => $bop_method2payby{$options{method}},
776 #'payinfo' => $payinfo,
777 'payinfo' => '4111111111111111',
778 'paybatch' => $paybatch,
779 #'paydate' => $paydate,
780 'paydate' => '2012-05-01',
782 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
785 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
786 warn " $_ => $options{$_}\n" foreach keys %options;
789 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
792 $cust_pay->invnum(''); #try again with no specific invnum
793 my $error2 = $cust_pay->insert( $options{'manual'} ?
794 ( 'manual' => 1 ) : ()
797 # gah, even with transactions.
798 my $e = 'WARNING: Card/ACH debited but database not updated - '.
799 "error inserting (fake!) payment: $error2".
800 " (previously tried insert with invnum #$options{'invnum'}" .
807 if ( $options{'paynum_ref'} ) {
808 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
816 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
818 # Wraps up processing of a realtime credit card, ACH (electronic check) or
819 # phone bill transaction.
821 sub _realtime_bop_result {
822 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
824 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
827 warn "$me _realtime_bop_result: pending transaction ".
828 $cust_pay_pending->paypendingnum. "\n";
829 warn " $_ => $options{$_}\n" foreach keys %options;
832 my $payment_gateway = $options{payment_gateway}
833 or return "no payment gateway in arguments to _realtime_bop_result";
835 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
836 my $cpp_captured_err = $cust_pay_pending->replace;
837 return $cpp_captured_err if $cpp_captured_err;
839 if ( $transaction->is_success() ) {
842 if ( $payment_gateway->gatewaynum ) { # agent override
843 $paybatch = $payment_gateway->gatewaynum. '-';
846 $paybatch .= $payment_gateway->gateway_module. ":".
847 $transaction->authorization;
849 $paybatch .= ':'. $transaction->order_number
850 if $transaction->can('order_number')
851 && length($transaction->order_number);
853 my $cust_pay = new FS::cust_pay ( {
854 'custnum' => $self->custnum,
855 'invnum' => $options{'invnum'},
856 'paid' => $cust_pay_pending->paid,
858 'payby' => $cust_pay_pending->payby,
859 'payinfo' => $options{'payinfo'},
860 'paybatch' => $paybatch,
861 'paydate' => $cust_pay_pending->paydate,
862 'pkgnum' => $cust_pay_pending->pkgnum,
863 'discount_term' => $options{'discount_term'},
865 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
866 $cust_pay->payunique( $options{payunique} )
867 if defined($options{payunique}) && length($options{payunique});
869 my $oldAutoCommit = $FS::UID::AutoCommit;
870 local $FS::UID::AutoCommit = 0;
873 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
875 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
878 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
879 $cust_pay->invnum(''); #try again with no specific invnum
880 $cust_pay->paynum('');
881 my $error2 = $cust_pay->insert( $options{'manual'} ?
882 ( 'manual' => 1 ) : ()
885 # gah. but at least we have a record of the state we had to abort in
886 # from cust_pay_pending now.
887 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
888 my $e = "WARNING: $options{method} captured but payment not recorded -".
889 " error inserting payment (". $payment_gateway->gateway_module.
891 " (previously tried insert with invnum #$options{'invnum'}" .
892 ": $error ) - pending payment saved as paypendingnum ".
893 $cust_pay_pending->paypendingnum. "\n";
899 my $jobnum = $cust_pay_pending->jobnum;
901 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
903 unless ( $placeholder ) {
904 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
905 my $e = "WARNING: $options{method} captured but job $jobnum not ".
906 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
911 $error = $placeholder->delete;
914 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
915 my $e = "WARNING: $options{method} captured but could not delete ".
916 "job $jobnum for paypendingnum ".
917 $cust_pay_pending->paypendingnum. ": $error\n";
924 if ( $options{'paynum_ref'} ) {
925 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
928 $cust_pay_pending->status('done');
929 $cust_pay_pending->statustext('captured');
930 $cust_pay_pending->paynum($cust_pay->paynum);
931 my $cpp_done_err = $cust_pay_pending->replace;
933 if ( $cpp_done_err ) {
935 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
936 my $e = "WARNING: $options{method} captured but payment not recorded - ".
937 "error updating status for paypendingnum ".
938 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
944 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
946 if ( $options{'apply'} ) {
947 my $apply_error = $self->apply_payments_and_credits;
948 if ( $apply_error ) {
949 warn "WARNING: error applying payment: $apply_error\n";
950 #but we still should return no error cause the payment otherwise went
955 # have a CC surcharge portion --> one-time charge
956 if ( $options{'cc_surcharge'} > 0 ) {
957 # XXX: this whole block needs to be in a transaction?
960 $invnum = $options{'invnum'} if $options{'invnum'};
961 unless ( $invnum ) { # probably from a payment screen
962 # do we have any open invoices? pick earliest
963 # uses the fact that cust_main->cust_bill sorts by date ascending
964 my @open = $self->open_cust_bill;
965 $invnum = $open[0]->invnum if scalar(@open);
968 unless ( $invnum ) { # still nothing? pick last closed invoice
969 # again uses fact that cust_main->cust_bill sorts by date ascending
970 my @closed = $self->cust_bill;
971 $invnum = $closed[$#closed]->invnum if scalar(@closed);
975 # XXX: unlikely case - pre-paying before any invoices generated
976 # what it should do is create a new invoice and pick it
977 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
982 my $charge_error = $self->charge({
983 'amount' => $options{'cc_surcharge'},
984 'pkg' => 'Credit Card Surcharge',
986 'cust_pkg_ref' => \$cust_pkg,
989 warn 'Unable to add CC surcharge cust_pkg';
993 $cust_pkg->setup(time);
994 my $cp_error = $cust_pkg->replace;
996 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1000 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1001 unless ( $cust_bill ) {
1002 warn "race condition + invoice deletion just happened";
1007 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1009 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1013 return ''; #no error
1019 my $perror = $payment_gateway->gateway_module. " error: ".
1020 $transaction->error_message;
1022 my $jobnum = $cust_pay_pending->jobnum;
1024 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1026 if ( $placeholder ) {
1027 my $error = $placeholder->depended_delete;
1028 $error ||= $placeholder->delete;
1029 warn "error removing provisioning jobs after declined paypendingnum ".
1030 $cust_pay_pending->paypendingnum. ": $error\n";
1032 my $e = "error finding job $jobnum for declined paypendingnum ".
1033 $cust_pay_pending->paypendingnum. "\n";
1039 unless ( $transaction->error_message ) {
1042 if ( $transaction->can('response_page') ) {
1044 'page' => ( $transaction->can('response_page')
1045 ? $transaction->response_page
1048 'code' => ( $transaction->can('response_code')
1049 ? $transaction->response_code
1052 'headers' => ( $transaction->can('response_headers')
1053 ? $transaction->response_headers
1059 "No additional debugging information available for ".
1060 $payment_gateway->gateway_module;
1063 $perror .= "No error_message returned from ".
1064 $payment_gateway->gateway_module. " -- ".
1065 ( ref($t_response) ? Dumper($t_response) : $t_response );
1069 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1070 && $conf->exists('emaildecline', $self->agentnum)
1071 && grep { $_ ne 'POST' } $self->invoicing_list
1072 && ! grep { $transaction->error_message =~ /$_/ }
1073 $conf->config('emaildecline-exclude', $self->agentnum)
1076 # Send a decline alert to the customer.
1077 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1080 # include the raw error message in the transaction state
1081 $cust_pay_pending->setfield('error', $transaction->error_message);
1082 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1083 $error = $msg_template->send( 'cust_main' => $self,
1084 'object' => $cust_pay_pending );
1088 my @templ = $conf->config('declinetemplate');
1089 my $template = new Text::Template (
1091 SOURCE => [ map "$_\n", @templ ],
1092 ) or return "($perror) can't create template: $Text::Template::ERROR";
1093 $template->compile()
1094 or return "($perror) can't compile template: $Text::Template::ERROR";
1098 scalar( $conf->config('company_name', $self->agentnum ) ),
1099 'company_address' =>
1100 join("\n", $conf->config('company_address', $self->agentnum ) ),
1101 'error' => $transaction->error_message,
1104 my $error = send_email(
1105 'from' => $conf->config('invoice_from', $self->agentnum ),
1106 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1107 'subject' => 'Your payment could not be processed',
1108 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1112 $perror .= " (also received error sending decline notification: $error)"
1117 $cust_pay_pending->status('done');
1118 $cust_pay_pending->statustext("declined: $perror");
1119 my $cpp_done_err = $cust_pay_pending->replace;
1120 if ( $cpp_done_err ) {
1121 my $e = "WARNING: $options{method} declined but pending payment not ".
1122 "resolved - error updating status for paypendingnum ".
1123 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1125 $perror = "$e ($perror)";
1133 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1135 Verifies successful third party processing of a realtime credit card,
1136 ACH (electronic check) or phone bill transaction via a
1137 Business::OnlineThirdPartyPayment realtime gateway. See
1138 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1140 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1142 The additional options I<payname>, I<city>, I<state>,
1143 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1144 if set, will override the value from the customer record.
1146 I<description> is a free-text field passed to the gateway. It defaults to
1147 "Internet services".
1149 If an I<invnum> is specified, this payment (if successful) is applied to the
1150 specified invoice. If you don't specify an I<invnum> you might want to
1151 call the B<apply_payments> method.
1153 I<quiet> can be set true to surpress email decline notices.
1155 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1156 resulting paynum, if any.
1158 I<payunique> is a unique identifier for this payment.
1160 Returns a hashref containing elements bill_error (which will be undefined
1161 upon success) and session_id of any associated session.
1165 sub realtime_botpp_capture {
1166 my( $self, $cust_pay_pending, %options ) = @_;
1168 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1171 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1172 warn " $_ => $options{$_}\n" foreach keys %options;
1175 eval "use Business::OnlineThirdPartyPayment";
1179 # select the gateway
1182 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1184 my $payment_gateway;
1185 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1186 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1187 { gatewaynum => $gatewaynum }
1189 : $self->agent->payment_gateway( 'method' => $method,
1190 # 'invnum' => $cust_pay_pending->invnum,
1191 # 'payinfo' => $cust_pay_pending->payinfo,
1194 $options{payment_gateway} = $payment_gateway; # for the helper subs
1200 my @invoicing_list = $self->invoicing_list_emailonly;
1201 if ( $conf->exists('emailinvoiceautoalways')
1202 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1203 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1204 push @invoicing_list, $self->all_emails;
1207 my $email = ($conf->exists('business-onlinepayment-email-override'))
1208 ? $conf->config('business-onlinepayment-email-override')
1209 : $invoicing_list[0];
1213 $content{email_customer} =
1214 ( $conf->exists('business-onlinepayment-email_customer')
1215 || $conf->exists('business-onlinepayment-email-override') );
1218 # run transaction(s)
1222 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1223 $self->_bop_options(\%options),
1226 $transaction->reference({ %options });
1228 $transaction->content(
1230 $self->_bop_auth(\%options),
1231 'action' => 'Post Authorization',
1232 'description' => $options{'description'},
1233 'amount' => $cust_pay_pending->paid,
1234 #'invoice_number' => $options{'invnum'},
1235 'customer_id' => $self->custnum,
1236 'referer' => 'http://cleanwhisker.420.am/',
1237 'reference' => $cust_pay_pending->paypendingnum,
1239 'phone' => $self->daytime || $self->night,
1241 # plus whatever is required for bogus capture avoidance
1244 $transaction->submit();
1247 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1249 if ( $options{'apply'} ) {
1250 my $apply_error = $self->apply_payments_and_credits;
1251 if ( $apply_error ) {
1252 warn "WARNING: error applying payment: $apply_error\n";
1257 bill_error => $error,
1258 session_id => $cust_pay_pending->session_id,
1263 =item default_payment_gateway
1265 DEPRECATED -- use agent->payment_gateway
1269 sub default_payment_gateway {
1270 my( $self, $method ) = @_;
1272 die "Real-time processing not enabled\n"
1273 unless $conf->exists('business-onlinepayment');
1275 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1278 my $bop_config = 'business-onlinepayment';
1279 $bop_config .= '-ach'
1280 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1281 my ( $processor, $login, $password, $action, @bop_options ) =
1282 $conf->config($bop_config);
1283 $action ||= 'normal authorization';
1284 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1285 die "No real-time processor is enabled - ".
1286 "did you set the business-onlinepayment configuration value?\n"
1289 ( $processor, $login, $password, $action, @bop_options )
1292 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1294 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1295 via a Business::OnlinePayment realtime gateway. See
1296 L<http://420.am/business-onlinepayment> for supported gateways.
1298 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1300 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1302 Most gateways require a reference to an original payment transaction to refund,
1303 so you probably need to specify a I<paynum>.
1305 I<amount> defaults to the original amount of the payment if not specified.
1307 I<reason> specifies a reason for the refund.
1309 I<paydate> specifies the expiration date for a credit card overriding the
1310 value from the customer record or the payment record. Specified as yyyy-mm-dd
1312 Implementation note: If I<amount> is unspecified or equal to the amount of the
1313 orignal payment, first an attempt is made to "void" the transaction via
1314 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1315 the normal attempt is made to "refund" ("credit") the transaction via the
1316 gateway is attempted. No attempt to "void" the transaction is made if the
1317 gateway has introspection data and doesn't support void.
1319 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1320 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1321 #if set, will override the value from the customer record.
1323 #If an I<invnum> is specified, this payment (if successful) is applied to the
1324 #specified invoice. If you don't specify an I<invnum> you might want to
1325 #call the B<apply_payments> method.
1329 #some false laziness w/realtime_bop, not enough to make it worth merging
1330 #but some useful small subs should be pulled out
1331 sub realtime_refund_bop {
1334 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1337 if (ref($_[0]) eq 'HASH') {
1338 %options = %{$_[0]};
1342 $options{method} = $method;
1346 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1347 warn " $_ => $options{$_}\n" foreach keys %options;
1351 # look up the original payment and optionally a gateway for that payment
1355 my $amount = $options{'amount'};
1357 my( $processor, $login, $password, @bop_options, $namespace ) ;
1358 my( $auth, $order_number ) = ( '', '', '' );
1360 if ( $options{'paynum'} ) {
1362 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1363 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1364 or return "Unknown paynum $options{'paynum'}";
1365 $amount ||= $cust_pay->paid;
1367 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1368 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1369 $cust_pay->paybatch;
1370 my $gatewaynum = '';
1371 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1373 if ( $gatewaynum ) { #gateway for the payment to be refunded
1375 my $payment_gateway =
1376 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1377 die "payment gateway $gatewaynum not found"
1378 unless $payment_gateway;
1380 $processor = $payment_gateway->gateway_module;
1381 $login = $payment_gateway->gateway_username;
1382 $password = $payment_gateway->gateway_password;
1383 $namespace = $payment_gateway->gateway_namespace;
1384 @bop_options = $payment_gateway->options;
1386 } else { #try the default gateway
1389 my $payment_gateway =
1390 $self->agent->payment_gateway('method' => $options{method});
1392 ( $conf_processor, $login, $password, $namespace ) =
1393 map { my $method = "gateway_$_"; $payment_gateway->$method }
1394 qw( module username password namespace );
1396 @bop_options = $payment_gateway->gatewaynum
1397 ? $payment_gateway->options
1398 : @{ $payment_gateway->get('options') };
1400 return "processor of payment $options{'paynum'} $processor does not".
1401 " match default processor $conf_processor"
1402 unless $processor eq $conf_processor;
1407 } else { # didn't specify a paynum, so look for agent gateway overrides
1408 # like a normal transaction
1410 my $payment_gateway =
1411 $self->agent->payment_gateway( 'method' => $options{method},
1412 #'payinfo' => $payinfo,
1414 my( $processor, $login, $password, $namespace ) =
1415 map { my $method = "gateway_$_"; $payment_gateway->$method }
1416 qw( module username password namespace );
1418 my @bop_options = $payment_gateway->gatewaynum
1419 ? $payment_gateway->options
1420 : @{ $payment_gateway->get('options') };
1423 return "neither amount nor paynum specified" unless $amount;
1425 eval "use $namespace";
1429 'type' => $options{method},
1431 'password' => $password,
1432 'order_number' => $order_number,
1433 'amount' => $amount,
1434 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1436 $content{authorization} = $auth
1437 if length($auth); #echeck/ACH transactions have an order # but no auth
1438 #(at least with authorize.net)
1440 my $disable_void_after;
1441 if ($conf->exists('disable_void_after')
1442 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1443 $disable_void_after = $1;
1446 #first try void if applicable
1447 my $void = new Business::OnlinePayment( $processor, @bop_options );
1450 if ($void->can('info')) {
1452 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1453 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1454 my %supported_actions = $void->info('supported_actions');
1456 if ( %supported_actions && $paytype
1457 && defined($supported_actions{$paytype})
1458 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1461 if ( $cust_pay && $cust_pay->paid == $amount
1463 ( not defined($disable_void_after) )
1464 || ( time < ($cust_pay->_date + $disable_void_after ) )
1468 warn " attempting void\n" if $DEBUG > 1;
1469 if ( $void->can('info') ) {
1470 if ( $cust_pay->payby eq 'CARD'
1471 && $void->info('CC_void_requires_card') )
1473 $content{'card_number'} = $cust_pay->payinfo;
1474 } elsif ( $cust_pay->payby eq 'CHEK'
1475 && $void->info('ECHECK_void_requires_account') )
1477 ( $content{'account_number'}, $content{'routing_code'} ) =
1478 split('@', $cust_pay->payinfo);
1479 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1482 $void->content( 'action' => 'void', %content );
1483 $void->test_transaction(1)
1484 if $conf->exists('business-onlinepayment-test_transaction');
1486 if ( $void->is_success ) {
1487 my $error = $cust_pay->void($options{'reason'});
1489 # gah, even with transactions.
1490 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1491 "error voiding payment: $error";
1495 warn " void successful\n" if $DEBUG > 1;
1500 warn " void unsuccessful, trying refund\n"
1504 my $address = $self->address1;
1505 $address .= ", ". $self->address2 if $self->address2;
1507 my($payname, $payfirst, $paylast);
1508 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1509 $payname = $self->payname;
1510 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1511 or return "Illegal payname $payname";
1512 ($payfirst, $paylast) = ($1, $2);
1514 $payfirst = $self->getfield('first');
1515 $paylast = $self->getfield('last');
1516 $payname = "$payfirst $paylast";
1519 my @invoicing_list = $self->invoicing_list_emailonly;
1520 if ( $conf->exists('emailinvoiceautoalways')
1521 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1522 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1523 push @invoicing_list, $self->all_emails;
1526 my $email = ($conf->exists('business-onlinepayment-email-override'))
1527 ? $conf->config('business-onlinepayment-email-override')
1528 : $invoicing_list[0];
1530 my $payip = exists($options{'payip'})
1533 $content{customer_ip} = $payip
1537 if ( $options{method} eq 'CC' ) {
1540 $content{card_number} = $payinfo = $cust_pay->payinfo;
1541 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1542 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1543 ($content{expiration} = "$2/$1"); # where available
1545 $content{card_number} = $payinfo = $self->payinfo;
1546 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1547 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1548 $content{expiration} = "$2/$1";
1551 } elsif ( $options{method} eq 'ECHECK' ) {
1554 $payinfo = $cust_pay->payinfo;
1556 $payinfo = $self->payinfo;
1558 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1559 $content{bank_name} = $self->payname;
1560 $content{account_type} = 'CHECKING';
1561 $content{account_name} = $payname;
1562 $content{customer_org} = $self->company ? 'B' : 'I';
1563 $content{customer_ssn} = $self->ss;
1564 } elsif ( $options{method} eq 'LEC' ) {
1565 $content{phone} = $payinfo = $self->payinfo;
1569 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1570 my %sub_content = $refund->content(
1571 'action' => 'credit',
1572 'customer_id' => $self->custnum,
1573 'last_name' => $paylast,
1574 'first_name' => $payfirst,
1576 'address' => $address,
1577 'city' => $self->city,
1578 'state' => $self->state,
1579 'zip' => $self->zip,
1580 'country' => $self->country,
1582 'phone' => $self->daytime || $self->night,
1585 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1587 $refund->test_transaction(1)
1588 if $conf->exists('business-onlinepayment-test_transaction');
1591 return "$processor error: ". $refund->error_message
1592 unless $refund->is_success();
1594 my $paybatch = "$processor:". $refund->authorization;
1595 $paybatch .= ':'. $refund->order_number
1596 if $refund->can('order_number') && $refund->order_number;
1598 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1599 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1600 last unless @cust_bill_pay;
1601 my $cust_bill_pay = pop @cust_bill_pay;
1602 my $error = $cust_bill_pay->delete;
1606 my $cust_refund = new FS::cust_refund ( {
1607 'custnum' => $self->custnum,
1608 'paynum' => $options{'paynum'},
1609 'refund' => $amount,
1611 'payby' => $bop_method2payby{$options{method}},
1612 'payinfo' => $payinfo,
1613 'paybatch' => $paybatch,
1614 'reason' => $options{'reason'} || 'card or ACH refund',
1616 my $error = $cust_refund->insert;
1618 $cust_refund->paynum(''); #try again with no specific paynum
1619 my $error2 = $cust_refund->insert;
1621 # gah, even with transactions.
1622 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1623 "error inserting refund ($processor): $error2".
1624 " (previously tried insert with paynum #$options{'paynum'}" .
1643 L<FS::cust_main>, L<FS::cust_main::Billing>