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 # return 1 if the payinfo has been used for another payment
171 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
179 sub _payment_gateway {
180 my ($self, $options) = @_;
182 if ( $options->{'selfservice'} ) {
183 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
185 return $options->{payment_gateway} ||=
186 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
190 if ( $options->{'fake_gatewaynum'} ) {
191 $options->{payment_gateway} =
192 qsearchs('payment_gateway',
193 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
197 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
198 unless exists($options->{payment_gateway});
200 $options->{payment_gateway};
204 my ($self, $options) = @_;
207 'login' => $options->{payment_gateway}->gateway_username,
208 'password' => $options->{payment_gateway}->gateway_password,
213 my ($self, $options) = @_;
215 $options->{payment_gateway}->gatewaynum
216 ? $options->{payment_gateway}->options
217 : @{ $options->{payment_gateway}->get('options') };
222 my ($self, $options) = @_;
224 unless ( $options->{'description'} ) {
225 if ( $conf->exists('business-onlinepayment-description') ) {
226 my $dtempl = $conf->config('business-onlinepayment-description');
228 my $agent = $self->agent->agent;
230 $options->{'description'} = eval qq("$dtempl");
232 $options->{'description'} = 'Internet services';
236 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
238 # Default invoice number if the customer has exactly one open invoice.
239 if( ! $options->{'invnum'} ) {
240 $options->{'invnum'} = '';
241 my @open = $self->open_cust_bill;
242 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
245 $options->{payname} = $self->payname unless exists( $options->{payname} );
249 my ($self, $options) = @_;
252 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
253 $content{customer_ip} = $payip if length($payip);
255 $content{invoice_number} = $options->{'invnum'}
256 if exists($options->{'invnum'}) && length($options->{'invnum'});
258 $content{email_customer} =
259 ( $conf->exists('business-onlinepayment-email_customer')
260 || $conf->exists('business-onlinepayment-email-override') );
262 my ($payname, $payfirst, $paylast);
263 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
264 ($payname = $options->{payname}) =~
265 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
266 or return "Illegal payname $payname";
267 ($payfirst, $paylast) = ($1, $2);
269 $payfirst = $self->getfield('first');
270 $paylast = $self->getfield('last');
271 $payname = "$payfirst $paylast";
274 $content{last_name} = $paylast;
275 $content{first_name} = $payfirst;
277 $content{name} = $payname;
279 $content{address} = exists($options->{'address1'})
280 ? $options->{'address1'}
282 my $address2 = exists($options->{'address2'})
283 ? $options->{'address2'}
285 $content{address} .= ", ". $address2 if length($address2);
287 $content{city} = exists($options->{city})
290 $content{state} = exists($options->{state})
293 $content{zip} = exists($options->{zip})
296 $content{country} = exists($options->{country})
297 ? $options->{country}
300 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
301 $content{phone} = $self->daytime || $self->night;
303 my $currency = $conf->exists('business-onlinepayment-currency')
304 && $conf->config('business-onlinepayment-currency');
305 $content{currency} = $currency if $currency;
310 my %bop_method2payby = (
319 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
322 if (ref($_[0]) eq 'HASH') {
325 my ( $method, $amount ) = ( shift, shift );
327 $options{method} = $method;
328 $options{amount} = $amount;
333 # optional credit card surcharge
336 my $cc_surcharge = 0;
337 my $cc_surcharge_pct = 0;
338 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
339 if $conf->config('credit-card-surcharge-percentage');
341 # always add cc surcharge if called from event
342 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
343 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
344 $options{'amount'} += $cc_surcharge;
345 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
347 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
348 # payment screen), so consider the given
349 # amount as post-surcharge
350 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
353 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
354 $options{'cc_surcharge'} = $cc_surcharge;
358 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
359 warn " cc_surcharge = $cc_surcharge\n";
360 warn " $_ => $options{$_}\n" foreach keys %options;
363 return $self->fake_bop(\%options) if $options{'fake'};
365 $self->_bop_defaults(\%options);
368 # set trans_is_recur based on invnum if there is one
371 my $trans_is_recur = 0;
372 if ( $options{'invnum'} ) {
374 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
375 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
381 $cust_bill->cust_bill_pkg;
384 if grep { $_->freq ne '0' } @part_pkg;
392 my $payment_gateway = $self->_payment_gateway( \%options );
393 my $namespace = $payment_gateway->gateway_namespace;
395 eval "use $namespace";
399 # check for banned credit card/ACH
402 my $ban = FS::banned_pay->ban_search(
403 'payby' => $bop_method2payby{$options{method}},
404 'payinfo' => $options{payinfo},
406 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
409 # check for term discount validity
412 my $discount_term = $options{discount_term};
413 if ( $discount_term ) {
414 my $bill = ($self->cust_bill)[-1]
415 or return "Can't apply a term discount to an unbilled customer";
416 my $plan = FS::discount_plan->new(
418 months => $discount_term
419 ) or return "No discount available for term '$discount_term'";
421 if ( $plan->discounted_total != $options{amount} ) {
422 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
430 my $bop_content = $self->_bop_content(\%options);
431 return $bop_content unless ref($bop_content);
433 my @invoicing_list = $self->invoicing_list_emailonly;
434 if ( $conf->exists('emailinvoiceautoalways')
435 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
436 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
437 push @invoicing_list, $self->all_emails;
440 my $email = ($conf->exists('business-onlinepayment-email-override'))
441 ? $conf->config('business-onlinepayment-email-override')
442 : $invoicing_list[0];
446 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
448 $content{card_number} = $options{payinfo};
449 $paydate = exists($options{'paydate'})
450 ? $options{'paydate'}
452 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
453 $content{expiration} = "$2/$1";
455 my $paycvv = exists($options{'paycvv'})
458 $content{cvv2} = $paycvv
461 my $paystart_month = exists($options{'paystart_month'})
462 ? $options{'paystart_month'}
463 : $self->paystart_month;
465 my $paystart_year = exists($options{'paystart_year'})
466 ? $options{'paystart_year'}
467 : $self->paystart_year;
469 $content{card_start} = "$paystart_month/$paystart_year"
470 if $paystart_month && $paystart_year;
472 my $payissue = exists($options{'payissue'})
473 ? $options{'payissue'}
475 $content{issue_number} = $payissue if $payissue;
477 if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
478 'trans_is_recur' => $trans_is_recur,
482 $content{recurring_billing} = 'YES';
483 $content{acct_code} = 'rebill'
484 if $conf->exists('credit_card-recurring_billing_acct_code');
487 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
488 ( $content{account_number}, $content{routing_code} ) =
489 split('@', $options{payinfo});
490 $content{bank_name} = $options{payname};
491 $content{bank_state} = exists($options{'paystate'})
492 ? $options{'paystate'}
493 : $self->getfield('paystate');
494 $content{account_type}= (exists($options{'paytype'}) && $options{'paytype'})
495 ? uc($options{'paytype'})
496 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
497 $content{account_name} = $self->getfield('first'). ' '.
498 $self->getfield('last');
500 $content{customer_org} = $self->company ? 'B' : 'I';
501 $content{state_id} = exists($options{'stateid'})
502 ? $options{'stateid'}
503 : $self->getfield('stateid');
504 $content{state_id_state} = exists($options{'stateid_state'})
505 ? $options{'stateid_state'}
506 : $self->getfield('stateid_state');
507 $content{customer_ssn} = exists($options{'ss'})
510 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
511 $content{phone} = $options{payinfo};
512 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
522 my $balance = exists( $options{'balance'} )
523 ? $options{'balance'}
526 $self->select_for_update; #mutex ... just until we get our pending record in
528 #the checks here are intended to catch concurrent payments
529 #double-form-submission prevention is taken care of in cust_pay_pending::check
532 return "The customer's balance has changed; $options{method} transaction aborted."
533 if $self->balance < $balance;
535 #also check and make sure there aren't *other* pending payments for this cust
537 my @pending = qsearch('cust_pay_pending', {
538 'custnum' => $self->custnum,
539 'status' => { op=>'!=', value=>'done' }
542 #for third-party payments only, remove pending payments if they're in the
543 #'thirdparty' (waiting for customer action) state.
544 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
545 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
546 my $error = $_->delete;
547 warn "error deleting unfinished third-party payment ".
548 $_->paypendingnum . ": $error\n"
551 @pending = grep { $_->status ne 'thirdparty' } @pending;
554 return "A payment is already being processed for this customer (".
555 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
556 "); $options{method} transaction aborted."
559 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
561 my $cust_pay_pending = new FS::cust_pay_pending {
562 'custnum' => $self->custnum,
563 'paid' => $options{amount},
565 'payby' => $bop_method2payby{$options{method}},
566 'payinfo' => $options{payinfo},
567 'paydate' => $paydate,
568 'recurring_billing' => $content{recurring_billing},
569 'pkgnum' => $options{'pkgnum'},
571 'gatewaynum' => $payment_gateway->gatewaynum || '',
572 'session_id' => $options{session_id} || '',
573 'jobnum' => $options{depend_jobnum} || '',
575 $cust_pay_pending->payunique( $options{payunique} )
576 if defined($options{payunique}) && length($options{payunique});
577 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
578 return $cpp_new_err if $cpp_new_err;
580 my( $action1, $action2 ) =
581 split( /\s*\,\s*/, $payment_gateway->gateway_action );
583 my $transaction = new $namespace( $payment_gateway->gateway_module,
584 $self->_bop_options(\%options),
587 $transaction->content(
588 'type' => $options{method},
589 $self->_bop_auth(\%options),
590 'action' => $action1,
591 'description' => $options{'description'},
592 'amount' => $options{amount},
593 #'invoice_number' => $options{'invnum'},
594 'customer_id' => $self->custnum,
596 'reference' => $cust_pay_pending->paypendingnum, #for now
597 'callback_url' => $payment_gateway->gateway_callback_url,
602 $cust_pay_pending->status('pending');
603 my $cpp_pending_err = $cust_pay_pending->replace;
604 return $cpp_pending_err if $cpp_pending_err;
608 my $BOP_TESTING_SUCCESS = 1;
610 unless ( $BOP_TESTING ) {
611 $transaction->test_transaction(1)
612 if $conf->exists('business-onlinepayment-test_transaction');
613 $transaction->submit();
615 if ( $BOP_TESTING_SUCCESS ) {
616 $transaction->is_success(1);
617 $transaction->authorization('fake auth');
619 $transaction->is_success(0);
620 $transaction->error_message('fake failure');
624 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
626 $cust_pay_pending->status('thirdparty');
627 my $cpp_err = $cust_pay_pending->replace;
628 return { error => $cpp_err } if $cpp_err;
629 return { reference => $cust_pay_pending->paypendingnum,
630 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
632 } elsif ( $transaction->is_success() && $action2 ) {
634 $cust_pay_pending->status('authorized');
635 my $cpp_authorized_err = $cust_pay_pending->replace;
636 return $cpp_authorized_err if $cpp_authorized_err;
638 my $auth = $transaction->authorization;
639 my $ordernum = $transaction->can('order_number')
640 ? $transaction->order_number
644 new Business::OnlinePayment( $payment_gateway->gateway_module,
645 $self->_bop_options(\%options),
650 type => $options{method},
652 $self->_bop_auth(\%options),
653 order_number => $ordernum,
654 amount => $options{amount},
655 authorization => $auth,
656 description => $options{'description'},
659 foreach my $field (qw( authorization_source_code returned_ACI
660 transaction_identifier validation_code
661 transaction_sequence_num local_transaction_date
662 local_transaction_time AVS_result_code )) {
663 $capture{$field} = $transaction->$field() if $transaction->can($field);
666 $capture->content( %capture );
668 $capture->test_transaction(1)
669 if $conf->exists('business-onlinepayment-test_transaction');
672 unless ( $capture->is_success ) {
673 my $e = "Authorization successful but capture failed, custnum #".
674 $self->custnum. ': '. $capture->result_code.
675 ": ". $capture->error_message;
683 # remove paycvv after initial transaction
686 #false laziness w/misc/process/payment.cgi - check both to make sure working
688 if ( length($self->paycvv)
689 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
691 my $error = $self->remove_cvv;
693 warn "WARNING: error removing cvv: $error\n";
702 if ( $transaction->can('card_token') && $transaction->card_token ) {
704 $self->card_token($transaction->card_token);
706 if ( $options{'payinfo'} eq $self->payinfo ) {
707 $self->payinfo($transaction->card_token);
708 my $error = $self->replace;
710 warn "WARNING: error storing token: $error, but proceeding anyway\n";
720 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
732 if (ref($_[0]) eq 'HASH') {
735 my ( $method, $amount ) = ( shift, shift );
737 $options{method} = $method;
738 $options{amount} = $amount;
741 if ( $options{'fake_failure'} ) {
742 return "Error: No error; test failure requested with fake_failure";
746 #if ( $payment_gateway->gatewaynum ) { # agent override
747 # $paybatch = $payment_gateway->gatewaynum. '-';
750 #$paybatch .= "$processor:". $transaction->authorization;
752 #$paybatch .= ':'. $transaction->order_number
753 # if $transaction->can('order_number')
754 # && length($transaction->order_number);
756 my $paybatch = 'FakeProcessor:54:32';
758 my $cust_pay = new FS::cust_pay ( {
759 'custnum' => $self->custnum,
760 'invnum' => $options{'invnum'},
761 'paid' => $options{amount},
763 'payby' => $bop_method2payby{$options{method}},
764 #'payinfo' => $payinfo,
765 'payinfo' => '4111111111111111',
766 'paybatch' => $paybatch,
767 #'paydate' => $paydate,
768 'paydate' => '2012-05-01',
770 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
773 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
774 warn " $_ => $options{$_}\n" foreach keys %options;
777 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
780 $cust_pay->invnum(''); #try again with no specific invnum
781 my $error2 = $cust_pay->insert( $options{'manual'} ?
782 ( 'manual' => 1 ) : ()
785 # gah, even with transactions.
786 my $e = 'WARNING: Card/ACH debited but database not updated - '.
787 "error inserting (fake!) payment: $error2".
788 " (previously tried insert with invnum #$options{'invnum'}" .
795 if ( $options{'paynum_ref'} ) {
796 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
804 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
806 # Wraps up processing of a realtime credit card, ACH (electronic check) or
807 # phone bill transaction.
809 sub _realtime_bop_result {
810 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
812 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
815 warn "$me _realtime_bop_result: pending transaction ".
816 $cust_pay_pending->paypendingnum. "\n";
817 warn " $_ => $options{$_}\n" foreach keys %options;
820 my $payment_gateway = $options{payment_gateway}
821 or return "no payment gateway in arguments to _realtime_bop_result";
823 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
824 my $cpp_captured_err = $cust_pay_pending->replace;
825 return $cpp_captured_err if $cpp_captured_err;
827 if ( $transaction->is_success() ) {
830 if ( $payment_gateway->gatewaynum ) { # agent override
831 $paybatch = $payment_gateway->gatewaynum. '-';
834 $paybatch .= $payment_gateway->gateway_module. ":".
835 $transaction->authorization;
837 $paybatch .= ':'. $transaction->order_number
838 if $transaction->can('order_number')
839 && length($transaction->order_number);
841 my $cust_pay = new FS::cust_pay ( {
842 'custnum' => $self->custnum,
843 'invnum' => $options{'invnum'},
844 'paid' => $cust_pay_pending->paid,
846 'payby' => $cust_pay_pending->payby,
847 'payinfo' => $options{'payinfo'},
848 'paybatch' => $paybatch,
849 'paydate' => $cust_pay_pending->paydate,
850 'pkgnum' => $cust_pay_pending->pkgnum,
851 'discount_term' => $options{'discount_term'},
853 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
854 $cust_pay->payunique( $options{payunique} )
855 if defined($options{payunique}) && length($options{payunique});
857 my $oldAutoCommit = $FS::UID::AutoCommit;
858 local $FS::UID::AutoCommit = 0;
861 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
863 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
866 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
867 $cust_pay->invnum(''); #try again with no specific invnum
868 $cust_pay->paynum('');
869 my $error2 = $cust_pay->insert( $options{'manual'} ?
870 ( 'manual' => 1 ) : ()
873 # gah. but at least we have a record of the state we had to abort in
874 # from cust_pay_pending now.
875 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
876 my $e = "WARNING: $options{method} captured but payment not recorded -".
877 " error inserting payment (". $payment_gateway->gateway_module.
879 " (previously tried insert with invnum #$options{'invnum'}" .
880 ": $error ) - pending payment saved as paypendingnum ".
881 $cust_pay_pending->paypendingnum. "\n";
887 my $jobnum = $cust_pay_pending->jobnum;
889 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
891 unless ( $placeholder ) {
892 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
893 my $e = "WARNING: $options{method} captured but job $jobnum not ".
894 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
899 $error = $placeholder->delete;
902 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
903 my $e = "WARNING: $options{method} captured but could not delete ".
904 "job $jobnum for paypendingnum ".
905 $cust_pay_pending->paypendingnum. ": $error\n";
912 if ( $options{'paynum_ref'} ) {
913 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
916 $cust_pay_pending->status('done');
917 $cust_pay_pending->statustext('captured');
918 $cust_pay_pending->paynum($cust_pay->paynum);
919 my $cpp_done_err = $cust_pay_pending->replace;
921 if ( $cpp_done_err ) {
923 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
924 my $e = "WARNING: $options{method} captured but payment not recorded - ".
925 "error updating status for paypendingnum ".
926 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
932 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
934 if ( $options{'apply'} ) {
935 my $apply_error = $self->apply_payments_and_credits;
936 if ( $apply_error ) {
937 warn "WARNING: error applying payment: $apply_error\n";
938 #but we still should return no error cause the payment otherwise went
943 # have a CC surcharge portion --> one-time charge
944 if ( $options{'cc_surcharge'} > 0 ) {
945 # XXX: this whole block needs to be in a transaction?
948 $invnum = $options{'invnum'} if $options{'invnum'};
949 unless ( $invnum ) { # probably from a payment screen
950 # do we have any open invoices? pick earliest
951 # uses the fact that cust_main->cust_bill sorts by date ascending
952 my @open = $self->open_cust_bill;
953 $invnum = $open[0]->invnum if scalar(@open);
956 unless ( $invnum ) { # still nothing? pick last closed invoice
957 # again uses fact that cust_main->cust_bill sorts by date ascending
958 my @closed = $self->cust_bill;
959 $invnum = $closed[$#closed]->invnum if scalar(@closed);
963 # XXX: unlikely case - pre-paying before any invoices generated
964 # what it should do is create a new invoice and pick it
965 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
970 my $charge_error = $self->charge({
971 'amount' => $options{'cc_surcharge'},
972 'pkg' => 'Credit Card Surcharge',
974 'cust_pkg_ref' => \$cust_pkg,
977 warn 'Unable to add CC surcharge cust_pkg';
981 $cust_pkg->setup(time);
982 my $cp_error = $cust_pkg->replace;
984 warn 'Unable to set setup time on cust_pkg for cc surcharge';
988 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
989 unless ( $cust_bill ) {
990 warn "race condition + invoice deletion just happened";
995 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
997 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1001 return ''; #no error
1007 my $perror = $payment_gateway->gateway_module. " error: ".
1008 $transaction->error_message;
1010 my $jobnum = $cust_pay_pending->jobnum;
1012 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1014 if ( $placeholder ) {
1015 my $error = $placeholder->depended_delete;
1016 $error ||= $placeholder->delete;
1017 warn "error removing provisioning jobs after declined paypendingnum ".
1018 $cust_pay_pending->paypendingnum. ": $error\n";
1020 my $e = "error finding job $jobnum for declined paypendingnum ".
1021 $cust_pay_pending->paypendingnum. "\n";
1027 unless ( $transaction->error_message ) {
1030 if ( $transaction->can('response_page') ) {
1032 'page' => ( $transaction->can('response_page')
1033 ? $transaction->response_page
1036 'code' => ( $transaction->can('response_code')
1037 ? $transaction->response_code
1040 'headers' => ( $transaction->can('response_headers')
1041 ? $transaction->response_headers
1047 "No additional debugging information available for ".
1048 $payment_gateway->gateway_module;
1051 $perror .= "No error_message returned from ".
1052 $payment_gateway->gateway_module. " -- ".
1053 ( ref($t_response) ? Dumper($t_response) : $t_response );
1057 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1058 && $conf->exists('emaildecline', $self->agentnum)
1059 && grep { $_ ne 'POST' } $self->invoicing_list
1060 && ! grep { $transaction->error_message =~ /$_/ }
1061 $conf->config('emaildecline-exclude', $self->agentnum)
1064 # Send a decline alert to the customer.
1065 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1068 # include the raw error message in the transaction state
1069 $cust_pay_pending->setfield('error', $transaction->error_message);
1070 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1071 $error = $msg_template->send( 'cust_main' => $self,
1072 'object' => $cust_pay_pending );
1076 my @templ = $conf->config('declinetemplate');
1077 my $template = new Text::Template (
1079 SOURCE => [ map "$_\n", @templ ],
1080 ) or return "($perror) can't create template: $Text::Template::ERROR";
1081 $template->compile()
1082 or return "($perror) can't compile template: $Text::Template::ERROR";
1086 scalar( $conf->config('company_name', $self->agentnum ) ),
1087 'company_address' =>
1088 join("\n", $conf->config('company_address', $self->agentnum ) ),
1089 'error' => $transaction->error_message,
1092 my $error = send_email(
1093 'from' => $conf->config('invoice_from', $self->agentnum ),
1094 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1095 'subject' => 'Your payment could not be processed',
1096 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1100 $perror .= " (also received error sending decline notification: $error)"
1105 $cust_pay_pending->status('done');
1106 $cust_pay_pending->statustext("declined: $perror");
1107 my $cpp_done_err = $cust_pay_pending->replace;
1108 if ( $cpp_done_err ) {
1109 my $e = "WARNING: $options{method} declined but pending payment not ".
1110 "resolved - error updating status for paypendingnum ".
1111 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1113 $perror = "$e ($perror)";
1121 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1123 Verifies successful third party processing of a realtime credit card,
1124 ACH (electronic check) or phone bill transaction via a
1125 Business::OnlineThirdPartyPayment realtime gateway. See
1126 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1128 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1130 The additional options I<payname>, I<city>, I<state>,
1131 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1132 if set, will override the value from the customer record.
1134 I<description> is a free-text field passed to the gateway. It defaults to
1135 "Internet services".
1137 If an I<invnum> is specified, this payment (if successful) is applied to the
1138 specified invoice. If you don't specify an I<invnum> you might want to
1139 call the B<apply_payments> method.
1141 I<quiet> can be set true to surpress email decline notices.
1143 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1144 resulting paynum, if any.
1146 I<payunique> is a unique identifier for this payment.
1148 Returns a hashref containing elements bill_error (which will be undefined
1149 upon success) and session_id of any associated session.
1153 sub realtime_botpp_capture {
1154 my( $self, $cust_pay_pending, %options ) = @_;
1156 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1159 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1160 warn " $_ => $options{$_}\n" foreach keys %options;
1163 eval "use Business::OnlineThirdPartyPayment";
1167 # select the gateway
1170 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1172 my $payment_gateway;
1173 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1174 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1175 { gatewaynum => $gatewaynum }
1177 : $self->agent->payment_gateway( 'method' => $method,
1178 # 'invnum' => $cust_pay_pending->invnum,
1179 # 'payinfo' => $cust_pay_pending->payinfo,
1182 $options{payment_gateway} = $payment_gateway; # for the helper subs
1188 my @invoicing_list = $self->invoicing_list_emailonly;
1189 if ( $conf->exists('emailinvoiceautoalways')
1190 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1191 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1192 push @invoicing_list, $self->all_emails;
1195 my $email = ($conf->exists('business-onlinepayment-email-override'))
1196 ? $conf->config('business-onlinepayment-email-override')
1197 : $invoicing_list[0];
1201 $content{email_customer} =
1202 ( $conf->exists('business-onlinepayment-email_customer')
1203 || $conf->exists('business-onlinepayment-email-override') );
1206 # run transaction(s)
1210 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1211 $self->_bop_options(\%options),
1214 $transaction->reference({ %options });
1216 $transaction->content(
1218 $self->_bop_auth(\%options),
1219 'action' => 'Post Authorization',
1220 'description' => $options{'description'},
1221 'amount' => $cust_pay_pending->paid,
1222 #'invoice_number' => $options{'invnum'},
1223 'customer_id' => $self->custnum,
1224 'referer' => 'http://cleanwhisker.420.am/',
1225 'reference' => $cust_pay_pending->paypendingnum,
1227 'phone' => $self->daytime || $self->night,
1229 # plus whatever is required for bogus capture avoidance
1232 $transaction->submit();
1235 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1237 if ( $options{'apply'} ) {
1238 my $apply_error = $self->apply_payments_and_credits;
1239 if ( $apply_error ) {
1240 warn "WARNING: error applying payment: $apply_error\n";
1245 bill_error => $error,
1246 session_id => $cust_pay_pending->session_id,
1251 =item default_payment_gateway
1253 DEPRECATED -- use agent->payment_gateway
1257 sub default_payment_gateway {
1258 my( $self, $method ) = @_;
1260 die "Real-time processing not enabled\n"
1261 unless $conf->exists('business-onlinepayment');
1263 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1266 my $bop_config = 'business-onlinepayment';
1267 $bop_config .= '-ach'
1268 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1269 my ( $processor, $login, $password, $action, @bop_options ) =
1270 $conf->config($bop_config);
1271 $action ||= 'normal authorization';
1272 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1273 die "No real-time processor is enabled - ".
1274 "did you set the business-onlinepayment configuration value?\n"
1277 ( $processor, $login, $password, $action, @bop_options )
1280 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1282 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1283 via a Business::OnlinePayment realtime gateway. See
1284 L<http://420.am/business-onlinepayment> for supported gateways.
1286 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1288 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1290 Most gateways require a reference to an original payment transaction to refund,
1291 so you probably need to specify a I<paynum>.
1293 I<amount> defaults to the original amount of the payment if not specified.
1295 I<reason> specifies a reason for the refund.
1297 I<paydate> specifies the expiration date for a credit card overriding the
1298 value from the customer record or the payment record. Specified as yyyy-mm-dd
1300 Implementation note: If I<amount> is unspecified or equal to the amount of the
1301 orignal payment, first an attempt is made to "void" the transaction via
1302 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1303 the normal attempt is made to "refund" ("credit") the transaction via the
1304 gateway is attempted. No attempt to "void" the transaction is made if the
1305 gateway has introspection data and doesn't support void.
1307 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1308 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1309 #if set, will override the value from the customer record.
1311 #If an I<invnum> is specified, this payment (if successful) is applied to the
1312 #specified invoice. If you don't specify an I<invnum> you might want to
1313 #call the B<apply_payments> method.
1317 #some false laziness w/realtime_bop, not enough to make it worth merging
1318 #but some useful small subs should be pulled out
1319 sub realtime_refund_bop {
1322 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1325 if (ref($_[0]) eq 'HASH') {
1326 %options = %{$_[0]};
1330 $options{method} = $method;
1334 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1335 warn " $_ => $options{$_}\n" foreach keys %options;
1339 # look up the original payment and optionally a gateway for that payment
1343 my $amount = $options{'amount'};
1345 my( $processor, $login, $password, @bop_options, $namespace ) ;
1346 my( $auth, $order_number ) = ( '', '', '' );
1348 if ( $options{'paynum'} ) {
1350 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1351 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1352 or return "Unknown paynum $options{'paynum'}";
1353 $amount ||= $cust_pay->paid;
1355 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1356 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1357 $cust_pay->paybatch;
1358 my $gatewaynum = '';
1359 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1361 if ( $gatewaynum ) { #gateway for the payment to be refunded
1363 my $payment_gateway =
1364 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1365 die "payment gateway $gatewaynum not found"
1366 unless $payment_gateway;
1368 $processor = $payment_gateway->gateway_module;
1369 $login = $payment_gateway->gateway_username;
1370 $password = $payment_gateway->gateway_password;
1371 $namespace = $payment_gateway->gateway_namespace;
1372 @bop_options = $payment_gateway->options;
1374 } else { #try the default gateway
1377 my $payment_gateway =
1378 $self->agent->payment_gateway('method' => $options{method});
1380 ( $conf_processor, $login, $password, $namespace ) =
1381 map { my $method = "gateway_$_"; $payment_gateway->$method }
1382 qw( module username password namespace );
1384 @bop_options = $payment_gateway->gatewaynum
1385 ? $payment_gateway->options
1386 : @{ $payment_gateway->get('options') };
1388 return "processor of payment $options{'paynum'} $processor does not".
1389 " match default processor $conf_processor"
1390 unless $processor eq $conf_processor;
1395 } else { # didn't specify a paynum, so look for agent gateway overrides
1396 # like a normal transaction
1398 my $payment_gateway =
1399 $self->agent->payment_gateway( 'method' => $options{method},
1400 #'payinfo' => $payinfo,
1402 my( $processor, $login, $password, $namespace ) =
1403 map { my $method = "gateway_$_"; $payment_gateway->$method }
1404 qw( module username password namespace );
1406 my @bop_options = $payment_gateway->gatewaynum
1407 ? $payment_gateway->options
1408 : @{ $payment_gateway->get('options') };
1411 return "neither amount nor paynum specified" unless $amount;
1413 eval "use $namespace";
1417 'type' => $options{method},
1419 'password' => $password,
1420 'order_number' => $order_number,
1421 'amount' => $amount,
1422 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1424 $content{authorization} = $auth
1425 if length($auth); #echeck/ACH transactions have an order # but no auth
1426 #(at least with authorize.net)
1428 my $currency = $conf->exists('business-onlinepayment-currency')
1429 && $conf->config('business-onlinepayment-currency');
1430 $content{currency} = $currency if $currency;
1432 my $disable_void_after;
1433 if ($conf->exists('disable_void_after')
1434 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1435 $disable_void_after = $1;
1438 #first try void if applicable
1439 my $void = new Business::OnlinePayment( $processor, @bop_options );
1442 if ($void->can('info')) {
1444 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1445 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1446 my %supported_actions = $void->info('supported_actions');
1448 if ( %supported_actions && $paytype
1449 && defined($supported_actions{$paytype})
1450 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1453 if ( $cust_pay && $cust_pay->paid == $amount
1455 ( not defined($disable_void_after) )
1456 || ( time < ($cust_pay->_date + $disable_void_after ) )
1460 warn " attempting void\n" if $DEBUG > 1;
1461 if ( $void->can('info') ) {
1462 if ( $cust_pay->payby eq 'CARD'
1463 && $void->info('CC_void_requires_card') )
1465 $content{'card_number'} = $cust_pay->payinfo;
1466 } elsif ( $cust_pay->payby eq 'CHEK'
1467 && $void->info('ECHECK_void_requires_account') )
1469 ( $content{'account_number'}, $content{'routing_code'} ) =
1470 split('@', $cust_pay->payinfo);
1471 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1474 $void->content( 'action' => 'void', %content );
1475 $void->test_transaction(1)
1476 if $conf->exists('business-onlinepayment-test_transaction');
1478 if ( $void->is_success ) {
1479 my $error = $cust_pay->void($options{'reason'});
1481 # gah, even with transactions.
1482 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1483 "error voiding payment: $error";
1487 warn " void successful\n" if $DEBUG > 1;
1492 warn " void unsuccessful, trying refund\n"
1496 my $address = $self->address1;
1497 $address .= ", ". $self->address2 if $self->address2;
1499 my($payname, $payfirst, $paylast);
1500 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1501 $payname = $self->payname;
1502 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1503 or return "Illegal payname $payname";
1504 ($payfirst, $paylast) = ($1, $2);
1506 $payfirst = $self->getfield('first');
1507 $paylast = $self->getfield('last');
1508 $payname = "$payfirst $paylast";
1511 my @invoicing_list = $self->invoicing_list_emailonly;
1512 if ( $conf->exists('emailinvoiceautoalways')
1513 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1514 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1515 push @invoicing_list, $self->all_emails;
1518 my $email = ($conf->exists('business-onlinepayment-email-override'))
1519 ? $conf->config('business-onlinepayment-email-override')
1520 : $invoicing_list[0];
1522 my $payip = exists($options{'payip'})
1525 $content{customer_ip} = $payip
1529 if ( $options{method} eq 'CC' ) {
1532 $content{card_number} = $payinfo = $cust_pay->payinfo;
1533 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1534 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1535 ($content{expiration} = "$2/$1"); # where available
1537 $content{card_number} = $payinfo = $self->payinfo;
1538 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1539 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1540 $content{expiration} = "$2/$1";
1543 } elsif ( $options{method} eq 'ECHECK' ) {
1546 $payinfo = $cust_pay->payinfo;
1548 $payinfo = $self->payinfo;
1550 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1551 $content{bank_name} = $self->payname;
1552 $content{account_type} = 'CHECKING';
1553 $content{account_name} = $payname;
1554 $content{customer_org} = $self->company ? 'B' : 'I';
1555 $content{customer_ssn} = $self->ss;
1556 } elsif ( $options{method} eq 'LEC' ) {
1557 $content{phone} = $payinfo = $self->payinfo;
1561 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1562 my %sub_content = $refund->content(
1563 'action' => 'credit',
1564 'customer_id' => $self->custnum,
1565 'last_name' => $paylast,
1566 'first_name' => $payfirst,
1568 'address' => $address,
1569 'city' => $self->city,
1570 'state' => $self->state,
1571 'zip' => $self->zip,
1572 'country' => $self->country,
1574 'phone' => $self->daytime || $self->night,
1577 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1579 $refund->test_transaction(1)
1580 if $conf->exists('business-onlinepayment-test_transaction');
1583 return "$processor error: ". $refund->error_message
1584 unless $refund->is_success();
1586 my $paybatch = "$processor:". $refund->authorization;
1587 $paybatch .= ':'. $refund->order_number
1588 if $refund->can('order_number') && $refund->order_number;
1590 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1591 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1592 last unless @cust_bill_pay;
1593 my $cust_bill_pay = pop @cust_bill_pay;
1594 my $error = $cust_bill_pay->delete;
1598 my $cust_refund = new FS::cust_refund ( {
1599 'custnum' => $self->custnum,
1600 'paynum' => $options{'paynum'},
1601 'refund' => $amount,
1603 'payby' => $bop_method2payby{$options{method}},
1604 'payinfo' => $payinfo,
1605 'paybatch' => $paybatch,
1606 'reason' => $options{'reason'} || 'card or ACH refund',
1608 my $error = $cust_refund->insert;
1610 $cust_refund->paynum(''); #try again with no specific paynum
1611 my $error2 = $cust_refund->insert;
1613 # gah, even with transactions.
1614 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1615 "error inserting refund ($processor): $error2".
1616 " (previously tried insert with paynum #$options{'paynum'}" .
1635 L<FS::cust_main>, L<FS::cust_main::Billing>