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 my %hash = ( 'custnum' => $self->custnum,
178 if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
179 || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
189 sub _payment_gateway {
190 my ($self, $options) = @_;
192 if ( $options->{'selfservice'} ) {
193 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
195 return $options->{payment_gateway} ||=
196 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
200 if ( $options->{'fake_gatewaynum'} ) {
201 $options->{payment_gateway} =
202 qsearchs('payment_gateway',
203 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
207 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
208 unless exists($options->{payment_gateway});
210 $options->{payment_gateway};
214 my ($self, $options) = @_;
217 'login' => $options->{payment_gateway}->gateway_username,
218 'password' => $options->{payment_gateway}->gateway_password,
223 my ($self, $options) = @_;
225 $options->{payment_gateway}->gatewaynum
226 ? $options->{payment_gateway}->options
227 : @{ $options->{payment_gateway}->get('options') };
232 my ($self, $options) = @_;
234 unless ( $options->{'description'} ) {
235 if ( $conf->exists('business-onlinepayment-description') ) {
236 my $dtempl = $conf->config('business-onlinepayment-description');
238 my $agent = $self->agent->agent;
240 $options->{'description'} = eval qq("$dtempl");
242 $options->{'description'} = 'Internet services';
246 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
248 # Default invoice number if the customer has exactly one open invoice.
249 if( ! $options->{'invnum'} ) {
250 $options->{'invnum'} = '';
251 my @open = $self->open_cust_bill;
252 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
255 $options->{payname} = $self->payname unless exists( $options->{payname} );
259 my ($self, $options) = @_;
262 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
263 $content{customer_ip} = $payip if length($payip);
265 $content{invoice_number} = $options->{'invnum'}
266 if exists($options->{'invnum'}) && length($options->{'invnum'});
268 $content{email_customer} =
269 ( $conf->exists('business-onlinepayment-email_customer')
270 || $conf->exists('business-onlinepayment-email-override') );
272 my ($payname, $payfirst, $paylast);
273 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
274 ($payname = $options->{payname}) =~
275 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
276 or return "Illegal payname $payname";
277 ($payfirst, $paylast) = ($1, $2);
279 $payfirst = $self->getfield('first');
280 $paylast = $self->getfield('last');
281 $payname = "$payfirst $paylast";
284 $content{last_name} = $paylast;
285 $content{first_name} = $payfirst;
287 $content{name} = $payname;
289 $content{address} = exists($options->{'address1'})
290 ? $options->{'address1'}
292 my $address2 = exists($options->{'address2'})
293 ? $options->{'address2'}
295 $content{address} .= ", ". $address2 if length($address2);
297 $content{city} = exists($options->{city})
300 $content{state} = exists($options->{state})
303 $content{zip} = exists($options->{zip})
306 $content{country} = exists($options->{country})
307 ? $options->{country}
310 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
311 $content{phone} = $self->daytime || $self->night;
313 my $currency = $conf->exists('business-onlinepayment-currency')
314 && $conf->config('business-onlinepayment-currency');
315 $content{currency} = $currency if $currency;
320 my %bop_method2payby = (
329 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
332 if (ref($_[0]) eq 'HASH') {
335 my ( $method, $amount ) = ( shift, shift );
337 $options{method} = $method;
338 $options{amount} = $amount;
343 # optional credit card surcharge
346 my $cc_surcharge = 0;
347 my $cc_surcharge_pct = 0;
348 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
349 if $conf->config('credit-card-surcharge-percentage');
351 # always add cc surcharge if called from event
352 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
353 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
354 $options{'amount'} += $cc_surcharge;
355 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
357 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
358 # payment screen), so consider the given
359 # amount as post-surcharge
360 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
363 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
364 $options{'cc_surcharge'} = $cc_surcharge;
368 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
369 warn " cc_surcharge = $cc_surcharge\n";
370 warn " $_ => $options{$_}\n" foreach keys %options;
373 return $self->fake_bop(\%options) if $options{'fake'};
375 $self->_bop_defaults(\%options);
378 # set trans_is_recur based on invnum if there is one
381 my $trans_is_recur = 0;
382 if ( $options{'invnum'} ) {
384 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
385 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
391 $cust_bill->cust_bill_pkg;
394 if grep { $_->freq ne '0' } @part_pkg;
402 my $payment_gateway = $self->_payment_gateway( \%options );
403 my $namespace = $payment_gateway->gateway_namespace;
405 eval "use $namespace";
409 # check for banned credit card/ACH
412 my $ban = FS::banned_pay->ban_search(
413 'payby' => $bop_method2payby{$options{method}},
414 'payinfo' => $options{payinfo},
416 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
419 # check for term discount validity
422 my $discount_term = $options{discount_term};
423 if ( $discount_term ) {
424 my $bill = ($self->cust_bill)[-1]
425 or return "Can't apply a term discount to an unbilled customer";
426 my $plan = FS::discount_plan->new(
428 months => $discount_term
429 ) or return "No discount available for term '$discount_term'";
431 if ( $plan->discounted_total != $options{amount} ) {
432 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
440 my $bop_content = $self->_bop_content(\%options);
441 return $bop_content unless ref($bop_content);
443 my @invoicing_list = $self->invoicing_list_emailonly;
444 if ( $conf->exists('emailinvoiceautoalways')
445 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
446 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
447 push @invoicing_list, $self->all_emails;
450 my $email = ($conf->exists('business-onlinepayment-email-override'))
451 ? $conf->config('business-onlinepayment-email-override')
452 : $invoicing_list[0];
457 if ( $namespace eq 'Business::OnlinePayment' ) {
459 if ( $options{method} eq 'CC' ) {
461 $content{card_number} = $options{payinfo};
462 $paydate = exists($options{'paydate'})
463 ? $options{'paydate'}
465 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
466 $content{expiration} = "$2/$1";
468 my $paycvv = exists($options{'paycvv'})
471 $content{cvv2} = $paycvv
474 my $paystart_month = exists($options{'paystart_month'})
475 ? $options{'paystart_month'}
476 : $self->paystart_month;
478 my $paystart_year = exists($options{'paystart_year'})
479 ? $options{'paystart_year'}
480 : $self->paystart_year;
482 $content{card_start} = "$paystart_month/$paystart_year"
483 if $paystart_month && $paystart_year;
485 my $payissue = exists($options{'payissue'})
486 ? $options{'payissue'}
488 $content{issue_number} = $payissue if $payissue;
490 if ( $self->_bop_recurring_billing(
491 'payinfo' => $options{'payinfo'},
492 'trans_is_recur' => $trans_is_recur,
496 $content{recurring_billing} = 'YES';
497 $content{acct_code} = 'rebill'
498 if $conf->exists('credit_card-recurring_billing_acct_code');
501 } elsif ( $options{method} eq 'ECHECK' ){
503 ( $content{account_number}, $content{routing_code} ) =
504 split('@', $options{payinfo});
505 $content{bank_name} = $options{payname};
506 $content{bank_state} = exists($options{'paystate'})
507 ? $options{'paystate'}
508 : $self->getfield('paystate');
509 $content{account_type}=
510 (exists($options{'paytype'}) && $options{'paytype'})
511 ? uc($options{'paytype'})
512 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
513 $content{account_name} = $self->getfield('first'). ' '.
514 $self->getfield('last');
516 $content{customer_org} = $self->company ? 'B' : 'I';
517 $content{state_id} = exists($options{'stateid'})
518 ? $options{'stateid'}
519 : $self->getfield('stateid');
520 $content{state_id_state} = exists($options{'stateid_state'})
521 ? $options{'stateid_state'}
522 : $self->getfield('stateid_state');
523 $content{customer_ssn} = exists($options{'ss'})
527 } elsif ( $options{method} eq 'LEC' ) {
528 $content{phone} = $options{payinfo};
530 die "unknown method ". $options{method};
533 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
536 die "unknown namespace $namespace";
543 my $balance = exists( $options{'balance'} )
544 ? $options{'balance'}
547 $self->select_for_update; #mutex ... just until we get our pending record in
549 #the checks here are intended to catch concurrent payments
550 #double-form-submission prevention is taken care of in cust_pay_pending::check
553 return "The customer's balance has changed; $options{method} transaction aborted."
554 if $self->balance < $balance;
556 #also check and make sure there aren't *other* pending payments for this cust
558 my @pending = qsearch('cust_pay_pending', {
559 'custnum' => $self->custnum,
560 'status' => { op=>'!=', value=>'done' }
563 #for third-party payments only, remove pending payments if they're in the
564 #'thirdparty' (waiting for customer action) state.
565 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
566 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
567 my $error = $_->delete;
568 warn "error deleting unfinished third-party payment ".
569 $_->paypendingnum . ": $error\n"
572 @pending = grep { $_->status ne 'thirdparty' } @pending;
575 return "A payment is already being processed for this customer (".
576 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
577 "); $options{method} transaction aborted."
580 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
582 my $cust_pay_pending = new FS::cust_pay_pending {
583 'custnum' => $self->custnum,
584 'paid' => $options{amount},
586 'payby' => $bop_method2payby{$options{method}},
587 'payinfo' => $options{payinfo},
588 'paydate' => $paydate,
589 'recurring_billing' => $content{recurring_billing},
590 'pkgnum' => $options{'pkgnum'},
592 'gatewaynum' => $payment_gateway->gatewaynum || '',
593 'session_id' => $options{session_id} || '',
594 'jobnum' => $options{depend_jobnum} || '',
596 $cust_pay_pending->payunique( $options{payunique} )
597 if defined($options{payunique}) && length($options{payunique});
598 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
599 return $cpp_new_err if $cpp_new_err;
601 my( $action1, $action2 ) =
602 split( /\s*\,\s*/, $payment_gateway->gateway_action );
604 my $transaction = new $namespace( $payment_gateway->gateway_module,
605 $self->_bop_options(\%options),
608 $transaction->content(
609 'type' => $options{method},
610 $self->_bop_auth(\%options),
611 'action' => $action1,
612 'description' => $options{'description'},
613 'amount' => $options{amount},
614 #'invoice_number' => $options{'invnum'},
615 'customer_id' => $self->custnum,
617 'reference' => $cust_pay_pending->paypendingnum, #for now
618 'callback_url' => $payment_gateway->gateway_callback_url,
623 $cust_pay_pending->status('pending');
624 my $cpp_pending_err = $cust_pay_pending->replace;
625 return $cpp_pending_err if $cpp_pending_err;
627 warn Dumper($transaction) if $DEBUG > 2;
629 unless ( $BOP_TESTING ) {
630 $transaction->test_transaction(1)
631 if $conf->exists('business-onlinepayment-test_transaction');
632 $transaction->submit();
634 if ( $BOP_TESTING_SUCCESS ) {
635 $transaction->is_success(1);
636 $transaction->authorization('fake auth');
638 $transaction->is_success(0);
639 $transaction->error_message('fake failure');
643 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
645 $cust_pay_pending->status('thirdparty');
646 my $cpp_err = $cust_pay_pending->replace;
647 return { error => $cpp_err } if $cpp_err;
648 return { reference => $cust_pay_pending->paypendingnum,
649 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
651 } elsif ( $transaction->is_success() && $action2 ) {
653 $cust_pay_pending->status('authorized');
654 my $cpp_authorized_err = $cust_pay_pending->replace;
655 return $cpp_authorized_err if $cpp_authorized_err;
657 my $auth = $transaction->authorization;
658 my $ordernum = $transaction->can('order_number')
659 ? $transaction->order_number
663 new Business::OnlinePayment( $payment_gateway->gateway_module,
664 $self->_bop_options(\%options),
669 type => $options{method},
671 $self->_bop_auth(\%options),
672 order_number => $ordernum,
673 amount => $options{amount},
674 authorization => $auth,
675 description => $options{'description'},
678 foreach my $field (qw( authorization_source_code returned_ACI
679 transaction_identifier validation_code
680 transaction_sequence_num local_transaction_date
681 local_transaction_time AVS_result_code )) {
682 $capture{$field} = $transaction->$field() if $transaction->can($field);
685 $capture->content( %capture );
687 $capture->test_transaction(1)
688 if $conf->exists('business-onlinepayment-test_transaction');
691 unless ( $capture->is_success ) {
692 my $e = "Authorization successful but capture failed, custnum #".
693 $self->custnum. ': '. $capture->result_code.
694 ": ". $capture->error_message;
702 # remove paycvv after initial transaction
705 #false laziness w/misc/process/payment.cgi - check both to make sure working
707 if ( length($self->paycvv)
708 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
710 my $error = $self->remove_cvv;
712 warn "WARNING: error removing cvv: $error\n";
721 if ( $transaction->can('card_token') && $transaction->card_token ) {
723 $self->card_token($transaction->card_token);
725 if ( $options{'payinfo'} eq $self->payinfo ) {
726 $self->payinfo($transaction->card_token);
727 my $error = $self->replace;
729 warn "WARNING: error storing token: $error, but proceeding anyway\n";
739 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
751 if (ref($_[0]) eq 'HASH') {
754 my ( $method, $amount ) = ( shift, shift );
756 $options{method} = $method;
757 $options{amount} = $amount;
760 if ( $options{'fake_failure'} ) {
761 return "Error: No error; test failure requested with fake_failure";
765 #if ( $payment_gateway->gatewaynum ) { # agent override
766 # $paybatch = $payment_gateway->gatewaynum. '-';
769 #$paybatch .= "$processor:". $transaction->authorization;
771 #$paybatch .= ':'. $transaction->order_number
772 # if $transaction->can('order_number')
773 # && length($transaction->order_number);
775 my $paybatch = 'FakeProcessor:54:32';
777 my $cust_pay = new FS::cust_pay ( {
778 'custnum' => $self->custnum,
779 'invnum' => $options{'invnum'},
780 'paid' => $options{amount},
782 'payby' => $bop_method2payby{$options{method}},
783 #'payinfo' => $payinfo,
784 'payinfo' => '4111111111111111',
785 'paybatch' => $paybatch,
786 #'paydate' => $paydate,
787 'paydate' => '2012-05-01',
789 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
792 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
793 warn " $_ => $options{$_}\n" foreach keys %options;
796 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
799 $cust_pay->invnum(''); #try again with no specific invnum
800 my $error2 = $cust_pay->insert( $options{'manual'} ?
801 ( 'manual' => 1 ) : ()
804 # gah, even with transactions.
805 my $e = 'WARNING: Card/ACH debited but database not updated - '.
806 "error inserting (fake!) payment: $error2".
807 " (previously tried insert with invnum #$options{'invnum'}" .
814 if ( $options{'paynum_ref'} ) {
815 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
823 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
825 # Wraps up processing of a realtime credit card, ACH (electronic check) or
826 # phone bill transaction.
828 sub _realtime_bop_result {
829 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
831 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
834 warn "$me _realtime_bop_result: pending transaction ".
835 $cust_pay_pending->paypendingnum. "\n";
836 warn " $_ => $options{$_}\n" foreach keys %options;
839 my $payment_gateway = $options{payment_gateway}
840 or return "no payment gateway in arguments to _realtime_bop_result";
842 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
843 my $cpp_captured_err = $cust_pay_pending->replace;
844 return $cpp_captured_err if $cpp_captured_err;
846 if ( $transaction->is_success() ) {
849 if ( $payment_gateway->gatewaynum ) { # agent override
850 $paybatch = $payment_gateway->gatewaynum. '-';
853 $paybatch .= $payment_gateway->gateway_module. ":".
854 $transaction->authorization;
856 $paybatch .= ':'. $transaction->order_number
857 if $transaction->can('order_number')
858 && length($transaction->order_number);
860 my $cust_pay = new FS::cust_pay ( {
861 'custnum' => $self->custnum,
862 'invnum' => $options{'invnum'},
863 'paid' => $cust_pay_pending->paid,
865 'payby' => $cust_pay_pending->payby,
866 'payinfo' => $options{'payinfo'},
867 'paybatch' => $paybatch,
868 'paydate' => $cust_pay_pending->paydate,
869 'pkgnum' => $cust_pay_pending->pkgnum,
870 'discount_term' => $options{'discount_term'},
872 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
873 $cust_pay->payunique( $options{payunique} )
874 if defined($options{payunique}) && length($options{payunique});
876 my $oldAutoCommit = $FS::UID::AutoCommit;
877 local $FS::UID::AutoCommit = 0;
880 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
882 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
885 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
886 $cust_pay->invnum(''); #try again with no specific invnum
887 $cust_pay->paynum('');
888 my $error2 = $cust_pay->insert( $options{'manual'} ?
889 ( 'manual' => 1 ) : ()
892 # gah. but at least we have a record of the state we had to abort in
893 # from cust_pay_pending now.
894 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
895 my $e = "WARNING: $options{method} captured but payment not recorded -".
896 " error inserting payment (". $payment_gateway->gateway_module.
898 " (previously tried insert with invnum #$options{'invnum'}" .
899 ": $error ) - pending payment saved as paypendingnum ".
900 $cust_pay_pending->paypendingnum. "\n";
906 my $jobnum = $cust_pay_pending->jobnum;
908 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
910 unless ( $placeholder ) {
911 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
912 my $e = "WARNING: $options{method} captured but job $jobnum not ".
913 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
918 $error = $placeholder->delete;
921 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
922 my $e = "WARNING: $options{method} captured but could not delete ".
923 "job $jobnum for paypendingnum ".
924 $cust_pay_pending->paypendingnum. ": $error\n";
931 if ( $options{'paynum_ref'} ) {
932 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
935 $cust_pay_pending->status('done');
936 $cust_pay_pending->statustext('captured');
937 $cust_pay_pending->paynum($cust_pay->paynum);
938 my $cpp_done_err = $cust_pay_pending->replace;
940 if ( $cpp_done_err ) {
942 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
943 my $e = "WARNING: $options{method} captured but payment not recorded - ".
944 "error updating status for paypendingnum ".
945 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
951 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
953 if ( $options{'apply'} ) {
954 my $apply_error = $self->apply_payments_and_credits;
955 if ( $apply_error ) {
956 warn "WARNING: error applying payment: $apply_error\n";
957 #but we still should return no error cause the payment otherwise went
962 # have a CC surcharge portion --> one-time charge
963 if ( $options{'cc_surcharge'} > 0 ) {
964 # XXX: this whole block needs to be in a transaction?
967 $invnum = $options{'invnum'} if $options{'invnum'};
968 unless ( $invnum ) { # probably from a payment screen
969 # do we have any open invoices? pick earliest
970 # uses the fact that cust_main->cust_bill sorts by date ascending
971 my @open = $self->open_cust_bill;
972 $invnum = $open[0]->invnum if scalar(@open);
975 unless ( $invnum ) { # still nothing? pick last closed invoice
976 # again uses fact that cust_main->cust_bill sorts by date ascending
977 my @closed = $self->cust_bill;
978 $invnum = $closed[$#closed]->invnum if scalar(@closed);
982 # XXX: unlikely case - pre-paying before any invoices generated
983 # what it should do is create a new invoice and pick it
984 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
989 my $charge_error = $self->charge({
990 'amount' => $options{'cc_surcharge'},
991 'pkg' => 'Credit Card Surcharge',
993 'cust_pkg_ref' => \$cust_pkg,
996 warn 'Unable to add CC surcharge cust_pkg';
1000 $cust_pkg->setup(time);
1001 my $cp_error = $cust_pkg->replace;
1003 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1007 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1008 unless ( $cust_bill ) {
1009 warn "race condition + invoice deletion just happened";
1014 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1016 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1020 return ''; #no error
1026 my $perror = $payment_gateway->gateway_module. " error: ".
1027 $transaction->error_message;
1029 my $jobnum = $cust_pay_pending->jobnum;
1031 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1033 if ( $placeholder ) {
1034 my $error = $placeholder->depended_delete;
1035 $error ||= $placeholder->delete;
1036 warn "error removing provisioning jobs after declined paypendingnum ".
1037 $cust_pay_pending->paypendingnum. ": $error\n";
1039 my $e = "error finding job $jobnum for declined paypendingnum ".
1040 $cust_pay_pending->paypendingnum. "\n";
1046 unless ( $transaction->error_message ) {
1049 if ( $transaction->can('response_page') ) {
1051 'page' => ( $transaction->can('response_page')
1052 ? $transaction->response_page
1055 'code' => ( $transaction->can('response_code')
1056 ? $transaction->response_code
1059 'headers' => ( $transaction->can('response_headers')
1060 ? $transaction->response_headers
1066 "No additional debugging information available for ".
1067 $payment_gateway->gateway_module;
1070 $perror .= "No error_message returned from ".
1071 $payment_gateway->gateway_module. " -- ".
1072 ( ref($t_response) ? Dumper($t_response) : $t_response );
1076 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1077 && $conf->exists('emaildecline', $self->agentnum)
1078 && grep { $_ ne 'POST' } $self->invoicing_list
1079 && ! grep { $transaction->error_message =~ /$_/ }
1080 $conf->config('emaildecline-exclude', $self->agentnum)
1083 # Send a decline alert to the customer.
1084 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1087 # include the raw error message in the transaction state
1088 $cust_pay_pending->setfield('error', $transaction->error_message);
1089 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1090 $error = $msg_template->send( 'cust_main' => $self,
1091 'object' => $cust_pay_pending );
1095 my @templ = $conf->config('declinetemplate');
1096 my $template = new Text::Template (
1098 SOURCE => [ map "$_\n", @templ ],
1099 ) or return "($perror) can't create template: $Text::Template::ERROR";
1100 $template->compile()
1101 or return "($perror) can't compile template: $Text::Template::ERROR";
1105 scalar( $conf->config('company_name', $self->agentnum ) ),
1106 'company_address' =>
1107 join("\n", $conf->config('company_address', $self->agentnum ) ),
1108 'error' => $transaction->error_message,
1111 my $error = send_email(
1112 'from' => $conf->config('invoice_from', $self->agentnum ),
1113 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1114 'subject' => 'Your payment could not be processed',
1115 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1119 $perror .= " (also received error sending decline notification: $error)"
1124 $cust_pay_pending->status('done');
1125 $cust_pay_pending->statustext("declined: $perror");
1126 my $cpp_done_err = $cust_pay_pending->replace;
1127 if ( $cpp_done_err ) {
1128 my $e = "WARNING: $options{method} declined but pending payment not ".
1129 "resolved - error updating status for paypendingnum ".
1130 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1132 $perror = "$e ($perror)";
1140 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1142 Verifies successful third party processing of a realtime credit card,
1143 ACH (electronic check) or phone bill transaction via a
1144 Business::OnlineThirdPartyPayment realtime gateway. See
1145 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1147 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1149 The additional options I<payname>, I<city>, I<state>,
1150 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1151 if set, will override the value from the customer record.
1153 I<description> is a free-text field passed to the gateway. It defaults to
1154 "Internet services".
1156 If an I<invnum> is specified, this payment (if successful) is applied to the
1157 specified invoice. If you don't specify an I<invnum> you might want to
1158 call the B<apply_payments> method.
1160 I<quiet> can be set true to surpress email decline notices.
1162 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1163 resulting paynum, if any.
1165 I<payunique> is a unique identifier for this payment.
1167 Returns a hashref containing elements bill_error (which will be undefined
1168 upon success) and session_id of any associated session.
1172 sub realtime_botpp_capture {
1173 my( $self, $cust_pay_pending, %options ) = @_;
1175 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1178 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1179 warn " $_ => $options{$_}\n" foreach keys %options;
1182 eval "use Business::OnlineThirdPartyPayment";
1186 # select the gateway
1189 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1191 my $payment_gateway;
1192 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1193 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1194 { gatewaynum => $gatewaynum }
1196 : $self->agent->payment_gateway( 'method' => $method,
1197 # 'invnum' => $cust_pay_pending->invnum,
1198 # 'payinfo' => $cust_pay_pending->payinfo,
1201 $options{payment_gateway} = $payment_gateway; # for the helper subs
1207 my @invoicing_list = $self->invoicing_list_emailonly;
1208 if ( $conf->exists('emailinvoiceautoalways')
1209 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1210 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1211 push @invoicing_list, $self->all_emails;
1214 my $email = ($conf->exists('business-onlinepayment-email-override'))
1215 ? $conf->config('business-onlinepayment-email-override')
1216 : $invoicing_list[0];
1220 $content{email_customer} =
1221 ( $conf->exists('business-onlinepayment-email_customer')
1222 || $conf->exists('business-onlinepayment-email-override') );
1225 # run transaction(s)
1229 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1230 $self->_bop_options(\%options),
1233 $transaction->reference({ %options });
1235 $transaction->content(
1237 $self->_bop_auth(\%options),
1238 'action' => 'Post Authorization',
1239 'description' => $options{'description'},
1240 'amount' => $cust_pay_pending->paid,
1241 #'invoice_number' => $options{'invnum'},
1242 'customer_id' => $self->custnum,
1243 'referer' => 'http://cleanwhisker.420.am/',
1244 'reference' => $cust_pay_pending->paypendingnum,
1246 'phone' => $self->daytime || $self->night,
1248 # plus whatever is required for bogus capture avoidance
1251 $transaction->submit();
1254 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1256 if ( $options{'apply'} ) {
1257 my $apply_error = $self->apply_payments_and_credits;
1258 if ( $apply_error ) {
1259 warn "WARNING: error applying payment: $apply_error\n";
1264 bill_error => $error,
1265 session_id => $cust_pay_pending->session_id,
1270 =item default_payment_gateway
1272 DEPRECATED -- use agent->payment_gateway
1276 sub default_payment_gateway {
1277 my( $self, $method ) = @_;
1279 die "Real-time processing not enabled\n"
1280 unless $conf->exists('business-onlinepayment');
1282 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1285 my $bop_config = 'business-onlinepayment';
1286 $bop_config .= '-ach'
1287 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1288 my ( $processor, $login, $password, $action, @bop_options ) =
1289 $conf->config($bop_config);
1290 $action ||= 'normal authorization';
1291 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1292 die "No real-time processor is enabled - ".
1293 "did you set the business-onlinepayment configuration value?\n"
1296 ( $processor, $login, $password, $action, @bop_options )
1299 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1301 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1302 via a Business::OnlinePayment realtime gateway. See
1303 L<http://420.am/business-onlinepayment> for supported gateways.
1305 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1307 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1309 Most gateways require a reference to an original payment transaction to refund,
1310 so you probably need to specify a I<paynum>.
1312 I<amount> defaults to the original amount of the payment if not specified.
1314 I<reason> specifies a reason for the refund.
1316 I<paydate> specifies the expiration date for a credit card overriding the
1317 value from the customer record or the payment record. Specified as yyyy-mm-dd
1319 Implementation note: If I<amount> is unspecified or equal to the amount of the
1320 orignal payment, first an attempt is made to "void" the transaction via
1321 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1322 the normal attempt is made to "refund" ("credit") the transaction via the
1323 gateway is attempted. No attempt to "void" the transaction is made if the
1324 gateway has introspection data and doesn't support void.
1326 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1327 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1328 #if set, will override the value from the customer record.
1330 #If an I<invnum> is specified, this payment (if successful) is applied to the
1331 #specified invoice. If you don't specify an I<invnum> you might want to
1332 #call the B<apply_payments> method.
1336 #some false laziness w/realtime_bop, not enough to make it worth merging
1337 #but some useful small subs should be pulled out
1338 sub realtime_refund_bop {
1341 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1344 if (ref($_[0]) eq 'HASH') {
1345 %options = %{$_[0]};
1349 $options{method} = $method;
1353 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1354 warn " $_ => $options{$_}\n" foreach keys %options;
1358 # look up the original payment and optionally a gateway for that payment
1362 my $amount = $options{'amount'};
1364 my( $processor, $login, $password, @bop_options, $namespace ) ;
1365 my( $auth, $order_number ) = ( '', '', '' );
1367 if ( $options{'paynum'} ) {
1369 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1370 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1371 or return "Unknown paynum $options{'paynum'}";
1372 $amount ||= $cust_pay->paid;
1374 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1375 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1376 $cust_pay->paybatch;
1377 my $gatewaynum = '';
1378 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1380 if ( $gatewaynum ) { #gateway for the payment to be refunded
1382 my $payment_gateway =
1383 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1384 die "payment gateway $gatewaynum not found"
1385 unless $payment_gateway;
1387 $processor = $payment_gateway->gateway_module;
1388 $login = $payment_gateway->gateway_username;
1389 $password = $payment_gateway->gateway_password;
1390 $namespace = $payment_gateway->gateway_namespace;
1391 @bop_options = $payment_gateway->options;
1393 } else { #try the default gateway
1396 my $payment_gateway =
1397 $self->agent->payment_gateway('method' => $options{method});
1399 ( $conf_processor, $login, $password, $namespace ) =
1400 map { my $method = "gateway_$_"; $payment_gateway->$method }
1401 qw( module username password namespace );
1403 @bop_options = $payment_gateway->gatewaynum
1404 ? $payment_gateway->options
1405 : @{ $payment_gateway->get('options') };
1407 return "processor of payment $options{'paynum'} $processor does not".
1408 " match default processor $conf_processor"
1409 unless $processor eq $conf_processor;
1414 } else { # didn't specify a paynum, so look for agent gateway overrides
1415 # like a normal transaction
1417 my $payment_gateway =
1418 $self->agent->payment_gateway( 'method' => $options{method},
1419 #'payinfo' => $payinfo,
1421 my( $processor, $login, $password, $namespace ) =
1422 map { my $method = "gateway_$_"; $payment_gateway->$method }
1423 qw( module username password namespace );
1425 my @bop_options = $payment_gateway->gatewaynum
1426 ? $payment_gateway->options
1427 : @{ $payment_gateway->get('options') };
1430 return "neither amount nor paynum specified" unless $amount;
1432 eval "use $namespace";
1436 'type' => $options{method},
1438 'password' => $password,
1439 'order_number' => $order_number,
1440 'amount' => $amount,
1441 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1443 $content{authorization} = $auth
1444 if length($auth); #echeck/ACH transactions have an order # but no auth
1445 #(at least with authorize.net)
1447 my $disable_void_after;
1448 if ($conf->exists('disable_void_after')
1449 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1450 $disable_void_after = $1;
1453 #first try void if applicable
1454 my $void = new Business::OnlinePayment( $processor, @bop_options );
1457 if ($void->can('info')) {
1459 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1460 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1461 my %supported_actions = $void->info('supported_actions');
1463 if ( %supported_actions && $paytype
1464 && defined($supported_actions{$paytype})
1465 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1468 if ( $cust_pay && $cust_pay->paid == $amount
1470 ( not defined($disable_void_after) )
1471 || ( time < ($cust_pay->_date + $disable_void_after ) )
1475 warn " attempting void\n" if $DEBUG > 1;
1476 if ( $void->can('info') ) {
1477 if ( $cust_pay->payby eq 'CARD'
1478 && $void->info('CC_void_requires_card') )
1480 $content{'card_number'} = $cust_pay->payinfo;
1481 } elsif ( $cust_pay->payby eq 'CHEK'
1482 && $void->info('ECHECK_void_requires_account') )
1484 ( $content{'account_number'}, $content{'routing_code'} ) =
1485 split('@', $cust_pay->payinfo);
1486 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1489 $void->content( 'action' => 'void', %content );
1490 $void->test_transaction(1)
1491 if $conf->exists('business-onlinepayment-test_transaction');
1493 if ( $void->is_success ) {
1494 my $error = $cust_pay->void($options{'reason'});
1496 # gah, even with transactions.
1497 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1498 "error voiding payment: $error";
1502 warn " void successful\n" if $DEBUG > 1;
1507 warn " void unsuccessful, trying refund\n"
1511 my $address = $self->address1;
1512 $address .= ", ". $self->address2 if $self->address2;
1514 my($payname, $payfirst, $paylast);
1515 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1516 $payname = $self->payname;
1517 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1518 or return "Illegal payname $payname";
1519 ($payfirst, $paylast) = ($1, $2);
1521 $payfirst = $self->getfield('first');
1522 $paylast = $self->getfield('last');
1523 $payname = "$payfirst $paylast";
1526 my @invoicing_list = $self->invoicing_list_emailonly;
1527 if ( $conf->exists('emailinvoiceautoalways')
1528 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1529 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1530 push @invoicing_list, $self->all_emails;
1533 my $email = ($conf->exists('business-onlinepayment-email-override'))
1534 ? $conf->config('business-onlinepayment-email-override')
1535 : $invoicing_list[0];
1537 my $payip = exists($options{'payip'})
1540 $content{customer_ip} = $payip
1544 if ( $options{method} eq 'CC' ) {
1547 $content{card_number} = $payinfo = $cust_pay->payinfo;
1548 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1549 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1550 ($content{expiration} = "$2/$1"); # where available
1552 $content{card_number} = $payinfo = $self->payinfo;
1553 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1554 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1555 $content{expiration} = "$2/$1";
1558 } elsif ( $options{method} eq 'ECHECK' ) {
1561 $payinfo = $cust_pay->payinfo;
1563 $payinfo = $self->payinfo;
1565 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1566 $content{bank_name} = $self->payname;
1567 $content{account_type} = 'CHECKING';
1568 $content{account_name} = $payname;
1569 $content{customer_org} = $self->company ? 'B' : 'I';
1570 $content{customer_ssn} = $self->ss;
1571 } elsif ( $options{method} eq 'LEC' ) {
1572 $content{phone} = $payinfo = $self->payinfo;
1576 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1577 my %sub_content = $refund->content(
1578 'action' => 'credit',
1579 'customer_id' => $self->custnum,
1580 'last_name' => $paylast,
1581 'first_name' => $payfirst,
1583 'address' => $address,
1584 'city' => $self->city,
1585 'state' => $self->state,
1586 'zip' => $self->zip,
1587 'country' => $self->country,
1589 'phone' => $self->daytime || $self->night,
1592 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1594 $refund->test_transaction(1)
1595 if $conf->exists('business-onlinepayment-test_transaction');
1598 return "$processor error: ". $refund->error_message
1599 unless $refund->is_success();
1601 my $paybatch = "$processor:". $refund->authorization;
1602 $paybatch .= ':'. $refund->order_number
1603 if $refund->can('order_number') && $refund->order_number;
1605 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1606 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1607 last unless @cust_bill_pay;
1608 my $cust_bill_pay = pop @cust_bill_pay;
1609 my $error = $cust_bill_pay->delete;
1613 my $cust_refund = new FS::cust_refund ( {
1614 'custnum' => $self->custnum,
1615 'paynum' => $options{'paynum'},
1616 'refund' => $amount,
1618 'payby' => $bop_method2payby{$options{method}},
1619 'payinfo' => $payinfo,
1620 'paybatch' => $paybatch,
1621 'reason' => $options{'reason'} || 'card or ACH refund',
1623 my $error = $cust_refund->insert;
1625 $cust_refund->paynum(''); #try again with no specific paynum
1626 my $error2 = $cust_refund->insert;
1628 # gah, even with transactions.
1629 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1630 "error inserting refund ($processor): $error2".
1631 " (previously tried insert with paynum #$options{'paynum'}" .
1650 L<FS::cust_main>, L<FS::cust_main::Billing>