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]';
25 install_callback FS::UID sub {
27 #yes, need it for stuff below (prolly should be cached)
32 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
38 These methods are available on FS::cust_main objects.
44 =item realtime_collect [ OPTION => VALUE ... ]
46 Attempt to collect the customer's current balance with a realtime credit
47 card, electronic check, or phone bill transaction (see realtime_bop() below).
49 Returns the result of realtime_bop(): nothing, an error message, or a
50 hashref of state information for a third-party transaction.
52 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
54 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
55 then it is deduced from the customer record.
57 If no I<amount> is specified, then the customer balance is used.
59 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
60 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
61 if set, will override the value from the customer record.
63 I<description> is a free-text field passed to the gateway. It defaults to
64 the value defined by the business-onlinepayment-description configuration
65 option, or "Internet services" if that is unset.
67 If an I<invnum> is specified, this payment (if successful) is applied to the
70 I<apply> will automatically apply a resulting payment.
72 I<quiet> can be set true to suppress email decline notices.
74 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
75 resulting paynum, if any.
77 I<payunique> is a unique identifier for this payment.
79 I<session_id> is a session identifier associated with this payment.
81 I<depend_jobnum> allows payment capture to unlock export jobs
85 sub realtime_collect {
86 my( $self, %options ) = @_;
88 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
91 warn "$me realtime_collect:\n";
92 warn " $_ => $options{$_}\n" foreach keys %options;
95 $options{amount} = $self->balance unless exists( $options{amount} );
96 $options{method} = FS::payby->payby2bop($self->payby)
97 unless exists( $options{method} );
99 return $self->realtime_bop({%options});
103 =item realtime_bop { [ ARG => VALUE ... ] }
105 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
106 via a Business::OnlinePayment realtime gateway. See
107 L<http://420.am/business-onlinepayment> for supported gateways.
109 Required arguments in the hashref are I<method>, and I<amount>
111 Available methods are: I<CC>, I<ECHECK> and I<LEC>
113 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
115 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
116 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
117 if set, will override the value from the customer record.
119 I<description> is a free-text field passed to the gateway. It defaults to
120 the value defined by the business-onlinepayment-description configuration
121 option, or "Internet services" if that is unset.
123 If an I<invnum> is specified, this payment (if successful) is applied to the
124 specified invoice. If the customer has exactly one open invoice, that
125 invoice number will be assumed. If you don't specify an I<invnum> you might
126 want to call the B<apply_payments> method or set the I<apply> option.
128 I<apply> can be set to true to apply a resulting payment.
130 I<quiet> can be set true to surpress email decline notices.
132 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
133 resulting paynum, if any.
135 I<payunique> is a unique identifier for this payment.
137 I<session_id> is a session identifier associated with this payment.
139 I<depend_jobnum> allows payment capture to unlock export jobs
141 I<discount_term> attempts to take a discount by prepaying for discount_term.
142 The payment will fail if I<amount> is incorrect for this discount term.
144 A direct (Business::OnlinePayment) transaction will return nothing on success,
145 or an error message on failure.
147 A third-party transaction will return a hashref containing:
149 - popup_url: the URL to which a browser should be redirected to complete
151 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
152 - reference: a reference ID for the transaction, to show the customer.
154 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
158 # some helper routines
159 sub _bop_recurring_billing {
160 my( $self, %opt ) = @_;
162 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
164 if ( defined($method) && $method eq 'transaction_is_recur' ) {
166 return 1 if $opt{'trans_is_recur'};
170 my %hash = ( 'custnum' => $self->custnum,
175 if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
176 || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
186 sub _payment_gateway {
187 my ($self, $options) = @_;
189 if ( $options->{'selfservice'} ) {
190 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
192 return $options->{payment_gateway} ||=
193 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
197 if ( $options->{'fake_gatewaynum'} ) {
198 $options->{payment_gateway} =
199 qsearchs('payment_gateway',
200 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
204 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
205 unless exists($options->{payment_gateway});
207 $options->{payment_gateway};
211 my ($self, $options) = @_;
214 'login' => $options->{payment_gateway}->gateway_username,
215 'password' => $options->{payment_gateway}->gateway_password,
220 my ($self, $options) = @_;
222 $options->{payment_gateway}->gatewaynum
223 ? $options->{payment_gateway}->options
224 : @{ $options->{payment_gateway}->get('options') };
229 my ($self, $options) = @_;
231 unless ( $options->{'description'} ) {
232 if ( $conf->exists('business-onlinepayment-description') ) {
233 my $dtempl = $conf->config('business-onlinepayment-description');
235 my $agent = $self->agent->agent;
237 $options->{'description'} = eval qq("$dtempl");
239 $options->{'description'} = 'Internet services';
243 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
245 # Default invoice number if the customer has exactly one open invoice.
246 if( ! $options->{'invnum'} ) {
247 $options->{'invnum'} = '';
248 my @open = $self->open_cust_bill;
249 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
252 $options->{payname} = $self->payname unless exists( $options->{payname} );
256 my ($self, $options) = @_;
259 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
260 $content{customer_ip} = $payip if length($payip);
262 $content{invoice_number} = $options->{'invnum'}
263 if exists($options->{'invnum'}) && length($options->{'invnum'});
265 $content{email_customer} =
266 ( $conf->exists('business-onlinepayment-email_customer')
267 || $conf->exists('business-onlinepayment-email-override') );
269 my ($payname, $payfirst, $paylast);
270 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
271 ($payname = $options->{payname}) =~
272 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
273 or return "Illegal payname $payname";
274 ($payfirst, $paylast) = ($1, $2);
276 $payfirst = $self->getfield('first');
277 $paylast = $self->getfield('last');
278 $payname = "$payfirst $paylast";
281 $content{last_name} = $paylast;
282 $content{first_name} = $payfirst;
284 $content{name} = $payname;
286 $content{address} = exists($options->{'address1'})
287 ? $options->{'address1'}
289 my $address2 = exists($options->{'address2'})
290 ? $options->{'address2'}
292 $content{address} .= ", ". $address2 if length($address2);
294 $content{city} = exists($options->{city})
297 $content{state} = exists($options->{state})
300 $content{zip} = exists($options->{zip})
303 $content{country} = exists($options->{country})
304 ? $options->{country}
307 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
308 $content{phone} = $self->daytime || $self->night;
310 my $currency = $conf->exists('business-onlinepayment-currency')
311 && $conf->config('business-onlinepayment-currency');
312 $content{currency} = $currency if $currency;
317 my %bop_method2payby = (
326 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
329 if (ref($_[0]) eq 'HASH') {
332 my ( $method, $amount ) = ( shift, shift );
334 $options{method} = $method;
335 $options{amount} = $amount;
340 # optional credit card surcharge
343 my $cc_surcharge = 0;
344 my $cc_surcharge_pct = 0;
345 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
346 if $conf->config('credit-card-surcharge-percentage');
348 # always add cc surcharge if called from event
349 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
350 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
351 $options{'amount'} += $cc_surcharge;
352 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
354 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
355 # payment screen), so consider the given
356 # amount as post-surcharge
357 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
360 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
361 $options{'cc_surcharge'} = $cc_surcharge;
365 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
366 warn " cc_surcharge = $cc_surcharge\n";
367 warn " $_ => $options{$_}\n" foreach keys %options;
370 return $self->fake_bop(\%options) if $options{'fake'};
372 $self->_bop_defaults(\%options);
375 # set trans_is_recur based on invnum if there is one
378 my $trans_is_recur = 0;
379 if ( $options{'invnum'} ) {
381 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
382 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
388 $cust_bill->cust_bill_pkg;
391 if grep { $_->freq ne '0' } @part_pkg;
399 my $payment_gateway = $self->_payment_gateway( \%options );
400 my $namespace = $payment_gateway->gateway_namespace;
402 eval "use $namespace";
406 # check for banned credit card/ACH
409 my $ban = FS::banned_pay->ban_search(
410 'payby' => $bop_method2payby{$options{method}},
411 'payinfo' => $options{payinfo},
413 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
416 # check for term discount validity
419 my $discount_term = $options{discount_term};
420 if ( $discount_term ) {
421 my $bill = ($self->cust_bill)[-1]
422 or return "Can't apply a term discount to an unbilled customer";
423 my $plan = FS::discount_plan->new(
425 months => $discount_term
426 ) or return "No discount available for term '$discount_term'";
428 if ( $plan->discounted_total != $options{amount} ) {
429 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
437 my $bop_content = $self->_bop_content(\%options);
438 return $bop_content unless ref($bop_content);
440 my @invoicing_list = $self->invoicing_list_emailonly;
441 if ( $conf->exists('emailinvoiceautoalways')
442 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
443 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
444 push @invoicing_list, $self->all_emails;
447 my $email = ($conf->exists('business-onlinepayment-email-override'))
448 ? $conf->config('business-onlinepayment-email-override')
449 : $invoicing_list[0];
453 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
455 $content{card_number} = $options{payinfo};
456 $paydate = exists($options{'paydate'})
457 ? $options{'paydate'}
459 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
460 $content{expiration} = "$2/$1";
462 my $paycvv = exists($options{'paycvv'})
465 $content{cvv2} = $paycvv
468 my $paystart_month = exists($options{'paystart_month'})
469 ? $options{'paystart_month'}
470 : $self->paystart_month;
472 my $paystart_year = exists($options{'paystart_year'})
473 ? $options{'paystart_year'}
474 : $self->paystart_year;
476 $content{card_start} = "$paystart_month/$paystart_year"
477 if $paystart_month && $paystart_year;
479 my $payissue = exists($options{'payissue'})
480 ? $options{'payissue'}
482 $content{issue_number} = $payissue if $payissue;
484 if ( $self->_bop_recurring_billing( '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 ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
495 ( $content{account_number}, $content{routing_code} ) =
496 split('@', $options{payinfo});
497 $content{bank_name} = $options{payname};
498 $content{bank_state} = exists($options{'paystate'})
499 ? $options{'paystate'}
500 : $self->getfield('paystate');
501 $content{account_type}= (exists($options{'paytype'}) && $options{'paytype'})
502 ? uc($options{'paytype'})
503 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
504 $content{account_name} = $self->getfield('first'). ' '.
505 $self->getfield('last');
507 $content{customer_org} = $self->company ? 'B' : 'I';
508 $content{state_id} = exists($options{'stateid'})
509 ? $options{'stateid'}
510 : $self->getfield('stateid');
511 $content{state_id_state} = exists($options{'stateid_state'})
512 ? $options{'stateid_state'}
513 : $self->getfield('stateid_state');
514 $content{customer_ssn} = exists($options{'ss'})
517 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
518 $content{phone} = $options{payinfo};
519 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
529 my $balance = exists( $options{'balance'} )
530 ? $options{'balance'}
533 $self->select_for_update; #mutex ... just until we get our pending record in
535 #the checks here are intended to catch concurrent payments
536 #double-form-submission prevention is taken care of in cust_pay_pending::check
539 return "The customer's balance has changed; $options{method} transaction aborted."
540 if $self->balance < $balance;
542 #also check and make sure there aren't *other* pending payments for this cust
544 my @pending = qsearch('cust_pay_pending', {
545 'custnum' => $self->custnum,
546 'status' => { op=>'!=', value=>'done' }
549 #for third-party payments only, remove pending payments if they're in the
550 #'thirdparty' (waiting for customer action) state.
551 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
552 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
553 my $error = $_->delete;
554 warn "error deleting unfinished third-party payment ".
555 $_->paypendingnum . ": $error\n"
558 @pending = grep { $_->status ne 'thirdparty' } @pending;
561 return "A payment is already being processed for this customer (".
562 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
563 "); $options{method} transaction aborted."
566 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
568 my $cust_pay_pending = new FS::cust_pay_pending {
569 'custnum' => $self->custnum,
570 'paid' => $options{amount},
572 'payby' => $bop_method2payby{$options{method}},
573 'payinfo' => $options{payinfo},
574 'paydate' => $paydate,
575 'recurring_billing' => $content{recurring_billing},
576 'pkgnum' => $options{'pkgnum'},
578 'gatewaynum' => $payment_gateway->gatewaynum || '',
579 'session_id' => $options{session_id} || '',
580 'jobnum' => $options{depend_jobnum} || '',
582 $cust_pay_pending->payunique( $options{payunique} )
583 if defined($options{payunique}) && length($options{payunique});
584 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
585 return $cpp_new_err if $cpp_new_err;
587 my( $action1, $action2 ) =
588 split( /\s*\,\s*/, $payment_gateway->gateway_action );
590 my $transaction = new $namespace( $payment_gateway->gateway_module,
591 $self->_bop_options(\%options),
594 $transaction->content(
595 'type' => $options{method},
596 $self->_bop_auth(\%options),
597 'action' => $action1,
598 'description' => $options{'description'},
599 'amount' => $options{amount},
600 #'invoice_number' => $options{'invnum'},
601 'customer_id' => $self->custnum,
603 'reference' => $cust_pay_pending->paypendingnum, #for now
604 'callback_url' => $payment_gateway->gateway_callback_url,
609 $cust_pay_pending->status('pending');
610 my $cpp_pending_err = $cust_pay_pending->replace;
611 return $cpp_pending_err if $cpp_pending_err;
615 my $BOP_TESTING_SUCCESS = 1;
617 unless ( $BOP_TESTING ) {
618 $transaction->test_transaction(1)
619 if $conf->exists('business-onlinepayment-test_transaction');
620 $transaction->submit();
622 if ( $BOP_TESTING_SUCCESS ) {
623 $transaction->is_success(1);
624 $transaction->authorization('fake auth');
626 $transaction->is_success(0);
627 $transaction->error_message('fake failure');
631 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
633 $cust_pay_pending->status('thirdparty');
634 my $cpp_err = $cust_pay_pending->replace;
635 return { error => $cpp_err } if $cpp_err;
636 return { reference => $cust_pay_pending->paypendingnum,
637 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
639 } elsif ( $transaction->is_success() && $action2 ) {
641 $cust_pay_pending->status('authorized');
642 my $cpp_authorized_err = $cust_pay_pending->replace;
643 return $cpp_authorized_err if $cpp_authorized_err;
645 my $auth = $transaction->authorization;
646 my $ordernum = $transaction->can('order_number')
647 ? $transaction->order_number
651 new Business::OnlinePayment( $payment_gateway->gateway_module,
652 $self->_bop_options(\%options),
657 type => $options{method},
659 $self->_bop_auth(\%options),
660 order_number => $ordernum,
661 amount => $options{amount},
662 authorization => $auth,
663 description => $options{'description'},
666 foreach my $field (qw( authorization_source_code returned_ACI
667 transaction_identifier validation_code
668 transaction_sequence_num local_transaction_date
669 local_transaction_time AVS_result_code )) {
670 $capture{$field} = $transaction->$field() if $transaction->can($field);
673 $capture->content( %capture );
675 $capture->test_transaction(1)
676 if $conf->exists('business-onlinepayment-test_transaction');
679 unless ( $capture->is_success ) {
680 my $e = "Authorization successful but capture failed, custnum #".
681 $self->custnum. ': '. $capture->result_code.
682 ": ". $capture->error_message;
690 # remove paycvv after initial transaction
693 #false laziness w/misc/process/payment.cgi - check both to make sure working
695 if ( length($self->paycvv)
696 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
698 my $error = $self->remove_cvv;
700 warn "WARNING: error removing cvv: $error\n";
709 if ( $transaction->can('card_token') && $transaction->card_token ) {
711 $self->card_token($transaction->card_token);
713 if ( $options{'payinfo'} eq $self->payinfo ) {
714 $self->payinfo($transaction->card_token);
715 my $error = $self->replace;
717 warn "WARNING: error storing token: $error, but proceeding anyway\n";
727 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
739 if (ref($_[0]) eq 'HASH') {
742 my ( $method, $amount ) = ( shift, shift );
744 $options{method} = $method;
745 $options{amount} = $amount;
748 if ( $options{'fake_failure'} ) {
749 return "Error: No error; test failure requested with fake_failure";
753 #if ( $payment_gateway->gatewaynum ) { # agent override
754 # $paybatch = $payment_gateway->gatewaynum. '-';
757 #$paybatch .= "$processor:". $transaction->authorization;
759 #$paybatch .= ':'. $transaction->order_number
760 # if $transaction->can('order_number')
761 # && length($transaction->order_number);
763 my $paybatch = 'FakeProcessor:54:32';
765 my $cust_pay = new FS::cust_pay ( {
766 'custnum' => $self->custnum,
767 'invnum' => $options{'invnum'},
768 'paid' => $options{amount},
770 'payby' => $bop_method2payby{$options{method}},
771 #'payinfo' => $payinfo,
772 'payinfo' => '4111111111111111',
773 'paybatch' => $paybatch,
774 #'paydate' => $paydate,
775 'paydate' => '2012-05-01',
777 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
780 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
781 warn " $_ => $options{$_}\n" foreach keys %options;
784 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
787 $cust_pay->invnum(''); #try again with no specific invnum
788 my $error2 = $cust_pay->insert( $options{'manual'} ?
789 ( 'manual' => 1 ) : ()
792 # gah, even with transactions.
793 my $e = 'WARNING: Card/ACH debited but database not updated - '.
794 "error inserting (fake!) payment: $error2".
795 " (previously tried insert with invnum #$options{'invnum'}" .
802 if ( $options{'paynum_ref'} ) {
803 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
811 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
813 # Wraps up processing of a realtime credit card, ACH (electronic check) or
814 # phone bill transaction.
816 sub _realtime_bop_result {
817 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
819 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
822 warn "$me _realtime_bop_result: pending transaction ".
823 $cust_pay_pending->paypendingnum. "\n";
824 warn " $_ => $options{$_}\n" foreach keys %options;
827 my $payment_gateway = $options{payment_gateway}
828 or return "no payment gateway in arguments to _realtime_bop_result";
830 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
831 my $cpp_captured_err = $cust_pay_pending->replace;
832 return $cpp_captured_err if $cpp_captured_err;
834 if ( $transaction->is_success() ) {
837 if ( $payment_gateway->gatewaynum ) { # agent override
838 $paybatch = $payment_gateway->gatewaynum. '-';
841 $paybatch .= $payment_gateway->gateway_module. ":".
842 $transaction->authorization;
844 $paybatch .= ':'. $transaction->order_number
845 if $transaction->can('order_number')
846 && length($transaction->order_number);
848 my $cust_pay = new FS::cust_pay ( {
849 'custnum' => $self->custnum,
850 'invnum' => $options{'invnum'},
851 'paid' => $cust_pay_pending->paid,
853 'payby' => $cust_pay_pending->payby,
854 'payinfo' => $options{'payinfo'},
855 'paybatch' => $paybatch,
856 'paydate' => $cust_pay_pending->paydate,
857 'pkgnum' => $cust_pay_pending->pkgnum,
858 'discount_term' => $options{'discount_term'},
860 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
861 $cust_pay->payunique( $options{payunique} )
862 if defined($options{payunique}) && length($options{payunique});
864 my $oldAutoCommit = $FS::UID::AutoCommit;
865 local $FS::UID::AutoCommit = 0;
868 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
870 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
873 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
874 $cust_pay->invnum(''); #try again with no specific invnum
875 $cust_pay->paynum('');
876 my $error2 = $cust_pay->insert( $options{'manual'} ?
877 ( 'manual' => 1 ) : ()
880 # gah. but at least we have a record of the state we had to abort in
881 # from cust_pay_pending now.
882 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
883 my $e = "WARNING: $options{method} captured but payment not recorded -".
884 " error inserting payment (". $payment_gateway->gateway_module.
886 " (previously tried insert with invnum #$options{'invnum'}" .
887 ": $error ) - pending payment saved as paypendingnum ".
888 $cust_pay_pending->paypendingnum. "\n";
894 my $jobnum = $cust_pay_pending->jobnum;
896 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
898 unless ( $placeholder ) {
899 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
900 my $e = "WARNING: $options{method} captured but job $jobnum not ".
901 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
906 $error = $placeholder->delete;
909 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
910 my $e = "WARNING: $options{method} captured but could not delete ".
911 "job $jobnum for paypendingnum ".
912 $cust_pay_pending->paypendingnum. ": $error\n";
919 if ( $options{'paynum_ref'} ) {
920 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
923 $cust_pay_pending->status('done');
924 $cust_pay_pending->statustext('captured');
925 $cust_pay_pending->paynum($cust_pay->paynum);
926 my $cpp_done_err = $cust_pay_pending->replace;
928 if ( $cpp_done_err ) {
930 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
931 my $e = "WARNING: $options{method} captured but payment not recorded - ".
932 "error updating status for paypendingnum ".
933 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
939 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
941 if ( $options{'apply'} ) {
942 my $apply_error = $self->apply_payments_and_credits;
943 if ( $apply_error ) {
944 warn "WARNING: error applying payment: $apply_error\n";
945 #but we still should return no error cause the payment otherwise went
950 # have a CC surcharge portion --> one-time charge
951 if ( $options{'cc_surcharge'} > 0 ) {
952 # XXX: this whole block needs to be in a transaction?
955 $invnum = $options{'invnum'} if $options{'invnum'};
956 unless ( $invnum ) { # probably from a payment screen
957 # do we have any open invoices? pick earliest
958 # uses the fact that cust_main->cust_bill sorts by date ascending
959 my @open = $self->open_cust_bill;
960 $invnum = $open[0]->invnum if scalar(@open);
963 unless ( $invnum ) { # still nothing? pick last closed invoice
964 # again uses fact that cust_main->cust_bill sorts by date ascending
965 my @closed = $self->cust_bill;
966 $invnum = $closed[$#closed]->invnum if scalar(@closed);
970 # XXX: unlikely case - pre-paying before any invoices generated
971 # what it should do is create a new invoice and pick it
972 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
977 my $charge_error = $self->charge({
978 'amount' => $options{'cc_surcharge'},
979 'pkg' => 'Credit Card Surcharge',
981 'cust_pkg_ref' => \$cust_pkg,
984 warn 'Unable to add CC surcharge cust_pkg';
988 $cust_pkg->setup(time);
989 my $cp_error = $cust_pkg->replace;
991 warn 'Unable to set setup time on cust_pkg for cc surcharge';
995 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
996 unless ( $cust_bill ) {
997 warn "race condition + invoice deletion just happened";
1002 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1004 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1008 return ''; #no error
1014 my $perror = $payment_gateway->gateway_module. " error: ".
1015 $transaction->error_message;
1017 my $jobnum = $cust_pay_pending->jobnum;
1019 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1021 if ( $placeholder ) {
1022 my $error = $placeholder->depended_delete;
1023 $error ||= $placeholder->delete;
1024 warn "error removing provisioning jobs after declined paypendingnum ".
1025 $cust_pay_pending->paypendingnum. ": $error\n";
1027 my $e = "error finding job $jobnum for declined paypendingnum ".
1028 $cust_pay_pending->paypendingnum. "\n";
1034 unless ( $transaction->error_message ) {
1037 if ( $transaction->can('response_page') ) {
1039 'page' => ( $transaction->can('response_page')
1040 ? $transaction->response_page
1043 'code' => ( $transaction->can('response_code')
1044 ? $transaction->response_code
1047 'headers' => ( $transaction->can('response_headers')
1048 ? $transaction->response_headers
1054 "No additional debugging information available for ".
1055 $payment_gateway->gateway_module;
1058 $perror .= "No error_message returned from ".
1059 $payment_gateway->gateway_module. " -- ".
1060 ( ref($t_response) ? Dumper($t_response) : $t_response );
1064 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1065 && $conf->exists('emaildecline', $self->agentnum)
1066 && grep { $_ ne 'POST' } $self->invoicing_list
1067 && ! grep { $transaction->error_message =~ /$_/ }
1068 $conf->config('emaildecline-exclude', $self->agentnum)
1071 # Send a decline alert to the customer.
1072 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1075 # include the raw error message in the transaction state
1076 $cust_pay_pending->setfield('error', $transaction->error_message);
1077 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1078 $error = $msg_template->send( 'cust_main' => $self,
1079 'object' => $cust_pay_pending );
1083 my @templ = $conf->config('declinetemplate');
1084 my $template = new Text::Template (
1086 SOURCE => [ map "$_\n", @templ ],
1087 ) or return "($perror) can't create template: $Text::Template::ERROR";
1088 $template->compile()
1089 or return "($perror) can't compile template: $Text::Template::ERROR";
1093 scalar( $conf->config('company_name', $self->agentnum ) ),
1094 'company_address' =>
1095 join("\n", $conf->config('company_address', $self->agentnum ) ),
1096 'error' => $transaction->error_message,
1099 my $error = send_email(
1100 'from' => $conf->config('invoice_from', $self->agentnum ),
1101 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1102 'subject' => 'Your payment could not be processed',
1103 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1107 $perror .= " (also received error sending decline notification: $error)"
1112 $cust_pay_pending->status('done');
1113 $cust_pay_pending->statustext("declined: $perror");
1114 my $cpp_done_err = $cust_pay_pending->replace;
1115 if ( $cpp_done_err ) {
1116 my $e = "WARNING: $options{method} declined but pending payment not ".
1117 "resolved - error updating status for paypendingnum ".
1118 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1120 $perror = "$e ($perror)";
1128 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1130 Verifies successful third party processing of a realtime credit card,
1131 ACH (electronic check) or phone bill transaction via a
1132 Business::OnlineThirdPartyPayment realtime gateway. See
1133 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1135 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1137 The additional options I<payname>, I<city>, I<state>,
1138 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1139 if set, will override the value from the customer record.
1141 I<description> is a free-text field passed to the gateway. It defaults to
1142 "Internet services".
1144 If an I<invnum> is specified, this payment (if successful) is applied to the
1145 specified invoice. If you don't specify an I<invnum> you might want to
1146 call the B<apply_payments> method.
1148 I<quiet> can be set true to surpress email decline notices.
1150 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1151 resulting paynum, if any.
1153 I<payunique> is a unique identifier for this payment.
1155 Returns a hashref containing elements bill_error (which will be undefined
1156 upon success) and session_id of any associated session.
1160 sub realtime_botpp_capture {
1161 my( $self, $cust_pay_pending, %options ) = @_;
1163 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1166 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1167 warn " $_ => $options{$_}\n" foreach keys %options;
1170 eval "use Business::OnlineThirdPartyPayment";
1174 # select the gateway
1177 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1179 my $payment_gateway;
1180 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1181 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1182 { gatewaynum => $gatewaynum }
1184 : $self->agent->payment_gateway( 'method' => $method,
1185 # 'invnum' => $cust_pay_pending->invnum,
1186 # 'payinfo' => $cust_pay_pending->payinfo,
1189 $options{payment_gateway} = $payment_gateway; # for the helper subs
1195 my @invoicing_list = $self->invoicing_list_emailonly;
1196 if ( $conf->exists('emailinvoiceautoalways')
1197 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1198 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1199 push @invoicing_list, $self->all_emails;
1202 my $email = ($conf->exists('business-onlinepayment-email-override'))
1203 ? $conf->config('business-onlinepayment-email-override')
1204 : $invoicing_list[0];
1208 $content{email_customer} =
1209 ( $conf->exists('business-onlinepayment-email_customer')
1210 || $conf->exists('business-onlinepayment-email-override') );
1213 # run transaction(s)
1217 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1218 $self->_bop_options(\%options),
1221 $transaction->reference({ %options });
1223 $transaction->content(
1225 $self->_bop_auth(\%options),
1226 'action' => 'Post Authorization',
1227 'description' => $options{'description'},
1228 'amount' => $cust_pay_pending->paid,
1229 #'invoice_number' => $options{'invnum'},
1230 'customer_id' => $self->custnum,
1231 'referer' => 'http://cleanwhisker.420.am/',
1232 'reference' => $cust_pay_pending->paypendingnum,
1234 'phone' => $self->daytime || $self->night,
1236 # plus whatever is required for bogus capture avoidance
1239 $transaction->submit();
1242 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1244 if ( $options{'apply'} ) {
1245 my $apply_error = $self->apply_payments_and_credits;
1246 if ( $apply_error ) {
1247 warn "WARNING: error applying payment: $apply_error\n";
1252 bill_error => $error,
1253 session_id => $cust_pay_pending->session_id,
1258 =item default_payment_gateway
1260 DEPRECATED -- use agent->payment_gateway
1264 sub default_payment_gateway {
1265 my( $self, $method ) = @_;
1267 die "Real-time processing not enabled\n"
1268 unless $conf->exists('business-onlinepayment');
1270 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1273 my $bop_config = 'business-onlinepayment';
1274 $bop_config .= '-ach'
1275 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1276 my ( $processor, $login, $password, $action, @bop_options ) =
1277 $conf->config($bop_config);
1278 $action ||= 'normal authorization';
1279 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1280 die "No real-time processor is enabled - ".
1281 "did you set the business-onlinepayment configuration value?\n"
1284 ( $processor, $login, $password, $action, @bop_options )
1287 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1289 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1290 via a Business::OnlinePayment realtime gateway. See
1291 L<http://420.am/business-onlinepayment> for supported gateways.
1293 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1295 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1297 Most gateways require a reference to an original payment transaction to refund,
1298 so you probably need to specify a I<paynum>.
1300 I<amount> defaults to the original amount of the payment if not specified.
1302 I<reason> specifies a reason for the refund.
1304 I<paydate> specifies the expiration date for a credit card overriding the
1305 value from the customer record or the payment record. Specified as yyyy-mm-dd
1307 Implementation note: If I<amount> is unspecified or equal to the amount of the
1308 orignal payment, first an attempt is made to "void" the transaction via
1309 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1310 the normal attempt is made to "refund" ("credit") the transaction via the
1311 gateway is attempted. No attempt to "void" the transaction is made if the
1312 gateway has introspection data and doesn't support void.
1314 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1315 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1316 #if set, will override the value from the customer record.
1318 #If an I<invnum> is specified, this payment (if successful) is applied to the
1319 #specified invoice. If you don't specify an I<invnum> you might want to
1320 #call the B<apply_payments> method.
1324 #some false laziness w/realtime_bop, not enough to make it worth merging
1325 #but some useful small subs should be pulled out
1326 sub realtime_refund_bop {
1329 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1332 if (ref($_[0]) eq 'HASH') {
1333 %options = %{$_[0]};
1337 $options{method} = $method;
1341 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1342 warn " $_ => $options{$_}\n" foreach keys %options;
1346 # look up the original payment and optionally a gateway for that payment
1350 my $amount = $options{'amount'};
1352 my( $processor, $login, $password, @bop_options, $namespace ) ;
1353 my( $auth, $order_number ) = ( '', '', '' );
1355 if ( $options{'paynum'} ) {
1357 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1358 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1359 or return "Unknown paynum $options{'paynum'}";
1360 $amount ||= $cust_pay->paid;
1362 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1363 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1364 $cust_pay->paybatch;
1365 my $gatewaynum = '';
1366 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1368 if ( $gatewaynum ) { #gateway for the payment to be refunded
1370 my $payment_gateway =
1371 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1372 die "payment gateway $gatewaynum not found"
1373 unless $payment_gateway;
1375 $processor = $payment_gateway->gateway_module;
1376 $login = $payment_gateway->gateway_username;
1377 $password = $payment_gateway->gateway_password;
1378 $namespace = $payment_gateway->gateway_namespace;
1379 @bop_options = $payment_gateway->options;
1381 } else { #try the default gateway
1384 my $payment_gateway =
1385 $self->agent->payment_gateway('method' => $options{method});
1387 ( $conf_processor, $login, $password, $namespace ) =
1388 map { my $method = "gateway_$_"; $payment_gateway->$method }
1389 qw( module username password namespace );
1391 @bop_options = $payment_gateway->gatewaynum
1392 ? $payment_gateway->options
1393 : @{ $payment_gateway->get('options') };
1395 return "processor of payment $options{'paynum'} $processor does not".
1396 " match default processor $conf_processor"
1397 unless $processor eq $conf_processor;
1402 } else { # didn't specify a paynum, so look for agent gateway overrides
1403 # like a normal transaction
1405 my $payment_gateway =
1406 $self->agent->payment_gateway( 'method' => $options{method},
1407 #'payinfo' => $payinfo,
1409 my( $processor, $login, $password, $namespace ) =
1410 map { my $method = "gateway_$_"; $payment_gateway->$method }
1411 qw( module username password namespace );
1413 my @bop_options = $payment_gateway->gatewaynum
1414 ? $payment_gateway->options
1415 : @{ $payment_gateway->get('options') };
1418 return "neither amount nor paynum specified" unless $amount;
1420 eval "use $namespace";
1424 'type' => $options{method},
1426 'password' => $password,
1427 'order_number' => $order_number,
1428 'amount' => $amount,
1429 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1431 $content{authorization} = $auth
1432 if length($auth); #echeck/ACH transactions have an order # but no auth
1433 #(at least with authorize.net)
1435 my $disable_void_after;
1436 if ($conf->exists('disable_void_after')
1437 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1438 $disable_void_after = $1;
1441 #first try void if applicable
1442 my $void = new Business::OnlinePayment( $processor, @bop_options );
1445 if ($void->can('info')) {
1447 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1448 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1449 my %supported_actions = $void->info('supported_actions');
1451 if ( %supported_actions && $paytype
1452 && defined($supported_actions{$paytype})
1453 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1456 if ( $cust_pay && $cust_pay->paid == $amount
1458 ( not defined($disable_void_after) )
1459 || ( time < ($cust_pay->_date + $disable_void_after ) )
1463 warn " attempting void\n" if $DEBUG > 1;
1464 if ( $void->can('info') ) {
1465 if ( $cust_pay->payby eq 'CARD'
1466 && $void->info('CC_void_requires_card') )
1468 $content{'card_number'} = $cust_pay->payinfo;
1469 } elsif ( $cust_pay->payby eq 'CHEK'
1470 && $void->info('ECHECK_void_requires_account') )
1472 ( $content{'account_number'}, $content{'routing_code'} ) =
1473 split('@', $cust_pay->payinfo);
1474 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1477 $void->content( 'action' => 'void', %content );
1478 $void->test_transaction(1)
1479 if $conf->exists('business-onlinepayment-test_transaction');
1481 if ( $void->is_success ) {
1482 my $error = $cust_pay->void($options{'reason'});
1484 # gah, even with transactions.
1485 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1486 "error voiding payment: $error";
1490 warn " void successful\n" if $DEBUG > 1;
1495 warn " void unsuccessful, trying refund\n"
1499 my $address = $self->address1;
1500 $address .= ", ". $self->address2 if $self->address2;
1502 my($payname, $payfirst, $paylast);
1503 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1504 $payname = $self->payname;
1505 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1506 or return "Illegal payname $payname";
1507 ($payfirst, $paylast) = ($1, $2);
1509 $payfirst = $self->getfield('first');
1510 $paylast = $self->getfield('last');
1511 $payname = "$payfirst $paylast";
1514 my @invoicing_list = $self->invoicing_list_emailonly;
1515 if ( $conf->exists('emailinvoiceautoalways')
1516 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1517 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1518 push @invoicing_list, $self->all_emails;
1521 my $email = ($conf->exists('business-onlinepayment-email-override'))
1522 ? $conf->config('business-onlinepayment-email-override')
1523 : $invoicing_list[0];
1525 my $payip = exists($options{'payip'})
1528 $content{customer_ip} = $payip
1532 if ( $options{method} eq 'CC' ) {
1535 $content{card_number} = $payinfo = $cust_pay->payinfo;
1536 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1537 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1538 ($content{expiration} = "$2/$1"); # where available
1540 $content{card_number} = $payinfo = $self->payinfo;
1541 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1542 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1543 $content{expiration} = "$2/$1";
1546 } elsif ( $options{method} eq 'ECHECK' ) {
1549 $payinfo = $cust_pay->payinfo;
1551 $payinfo = $self->payinfo;
1553 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1554 $content{bank_name} = $self->payname;
1555 $content{account_type} = 'CHECKING';
1556 $content{account_name} = $payname;
1557 $content{customer_org} = $self->company ? 'B' : 'I';
1558 $content{customer_ssn} = $self->ss;
1559 } elsif ( $options{method} eq 'LEC' ) {
1560 $content{phone} = $payinfo = $self->payinfo;
1564 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1565 my %sub_content = $refund->content(
1566 'action' => 'credit',
1567 'customer_id' => $self->custnum,
1568 'last_name' => $paylast,
1569 'first_name' => $payfirst,
1571 'address' => $address,
1572 'city' => $self->city,
1573 'state' => $self->state,
1574 'zip' => $self->zip,
1575 'country' => $self->country,
1577 'phone' => $self->daytime || $self->night,
1580 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1582 $refund->test_transaction(1)
1583 if $conf->exists('business-onlinepayment-test_transaction');
1586 return "$processor error: ". $refund->error_message
1587 unless $refund->is_success();
1589 my $paybatch = "$processor:". $refund->authorization;
1590 $paybatch .= ':'. $refund->order_number
1591 if $refund->can('order_number') && $refund->order_number;
1593 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1594 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1595 last unless @cust_bill_pay;
1596 my $cust_bill_pay = pop @cust_bill_pay;
1597 my $error = $cust_bill_pay->delete;
1601 my $cust_refund = new FS::cust_refund ( {
1602 'custnum' => $self->custnum,
1603 'paynum' => $options{'paynum'},
1604 'refund' => $amount,
1606 'payby' => $bop_method2payby{$options{method}},
1607 'payinfo' => $payinfo,
1608 'paybatch' => $paybatch,
1609 'reason' => $options{'reason'} || 'card or ACH refund',
1611 my $error = $cust_refund->insert;
1613 $cust_refund->paynum(''); #try again with no specific paynum
1614 my $error2 = $cust_refund->insert;
1616 # gah, even with transactions.
1617 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1618 "error inserting refund ($processor): $error2".
1619 " (previously tried insert with paynum #$options{'paynum'}" .
1638 L<FS::cust_main>, L<FS::cust_main::Billing>