1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.28;
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
17 $realtime_bop_decline_quiet = 0;
19 # 1 is mostly method/subroutine entry and options
20 # 2 traces progress of some operations
21 # 3 is even more information including possibly sensitive data
23 $me = '[FS::cust_main::Billing_Realtime]';
26 our $BOP_TESTING_SUCCESS = 1;
28 install_callback FS::UID sub {
30 #yes, need it for stuff below (prolly should be cached)
35 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
41 These methods are available on FS::cust_main objects.
47 =item realtime_collect [ OPTION => VALUE ... ]
49 Attempt to collect the customer's current balance with a realtime credit
50 card, electronic check, or phone bill transaction (see realtime_bop() below).
52 Returns the result of realtime_bop(): nothing, an error message, or a
53 hashref of state information for a third-party transaction.
55 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
57 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
58 then it is deduced from the customer record.
60 If no I<amount> is specified, then the customer balance is used.
62 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
63 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
64 if set, will override the value from the customer record.
66 I<description> is a free-text field passed to the gateway. It defaults to
67 the value defined by the business-onlinepayment-description configuration
68 option, or "Internet services" if that is unset.
70 If an I<invnum> is specified, this payment (if successful) is applied to the
73 I<apply> will automatically apply a resulting payment.
75 I<quiet> can be set true to suppress email decline notices.
77 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
78 resulting paynum, if any.
80 I<payunique> is a unique identifier for this payment.
82 I<session_id> is a session identifier associated with this payment.
84 I<depend_jobnum> allows payment capture to unlock export jobs
88 sub realtime_collect {
89 my( $self, %options ) = @_;
91 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
94 warn "$me realtime_collect:\n";
95 warn " $_ => $options{$_}\n" foreach keys %options;
98 $options{amount} = $self->balance unless exists( $options{amount} );
99 $options{method} = FS::payby->payby2bop($self->payby)
100 unless exists( $options{method} );
102 return $self->realtime_bop({%options});
106 =item realtime_bop { [ ARG => VALUE ... ] }
108 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
109 via a Business::OnlinePayment realtime gateway. See
110 L<http://420.am/business-onlinepayment> for supported gateways.
112 Required arguments in the hashref are I<method>, and I<amount>
114 Available methods are: I<CC>, I<ECHECK> and I<LEC>
116 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
118 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
119 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
120 if set, will override the value from the customer record.
122 I<description> is a free-text field passed to the gateway. It defaults to
123 the value defined by the business-onlinepayment-description configuration
124 option, or "Internet services" if that is unset.
126 If an I<invnum> is specified, this payment (if successful) is applied to the
127 specified invoice. If the customer has exactly one open invoice, that
128 invoice number will be assumed. If you don't specify an I<invnum> you might
129 want to call the B<apply_payments> method or set the I<apply> option.
131 I<apply> can be set to true to apply a resulting payment.
133 I<quiet> can be set true to surpress email decline notices.
135 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
136 resulting paynum, if any.
138 I<payunique> is a unique identifier for this payment.
140 I<session_id> is a session identifier associated with this payment.
142 I<depend_jobnum> allows payment capture to unlock export jobs
144 I<discount_term> attempts to take a discount by prepaying for discount_term.
145 The payment will fail if I<amount> is incorrect for this discount term.
147 A direct (Business::OnlinePayment) transaction will return nothing on success,
148 or an error message on failure.
150 A third-party transaction will return a hashref containing:
152 - popup_url: the URL to which a browser should be redirected to complete
154 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
155 - reference: a reference ID for the transaction, to show the customer.
157 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
161 # some helper routines
162 sub _bop_recurring_billing {
163 my( $self, %opt ) = @_;
165 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
167 if ( defined($method) && $method eq 'transaction_is_recur' ) {
169 return 1 if $opt{'trans_is_recur'};
173 # return 1 if the payinfo has been used for another payment
174 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
182 sub _payment_gateway {
183 my ($self, $options) = @_;
185 if ( $options->{'selfservice'} ) {
186 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
188 return $options->{payment_gateway} ||=
189 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
193 if ( $options->{'fake_gatewaynum'} ) {
194 $options->{payment_gateway} =
195 qsearchs('payment_gateway',
196 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
200 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
201 unless exists($options->{payment_gateway});
203 $options->{payment_gateway};
207 my ($self, $options) = @_;
210 'login' => $options->{payment_gateway}->gateway_username,
211 'password' => $options->{payment_gateway}->gateway_password,
216 my ($self, $options) = @_;
218 $options->{payment_gateway}->gatewaynum
219 ? $options->{payment_gateway}->options
220 : @{ $options->{payment_gateway}->get('options') };
225 my ($self, $options) = @_;
227 unless ( $options->{'description'} ) {
228 if ( $conf->exists('business-onlinepayment-description') ) {
229 my $dtempl = $conf->config('business-onlinepayment-description');
231 my $agent = $self->agent->agent;
233 $options->{'description'} = eval qq("$dtempl");
235 $options->{'description'} = 'Internet services';
239 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
241 # Default invoice number if the customer has exactly one open invoice.
242 if( ! $options->{'invnum'} ) {
243 $options->{'invnum'} = '';
244 my @open = $self->open_cust_bill;
245 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
248 $options->{payname} = $self->payname unless exists( $options->{payname} );
252 my ($self, $options) = @_;
255 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
256 $content{customer_ip} = $payip if length($payip);
258 $content{invoice_number} = $options->{'invnum'}
259 if exists($options->{'invnum'}) && length($options->{'invnum'});
261 $content{email_customer} =
262 ( $conf->exists('business-onlinepayment-email_customer')
263 || $conf->exists('business-onlinepayment-email-override') );
265 my ($payname, $payfirst, $paylast);
266 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
267 ($payname = $options->{payname}) =~
268 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
269 or return "Illegal payname $payname";
270 ($payfirst, $paylast) = ($1, $2);
272 $payfirst = $self->getfield('first');
273 $paylast = $self->getfield('last');
274 $payname = "$payfirst $paylast";
277 $content{last_name} = $paylast;
278 $content{first_name} = $payfirst;
280 $content{name} = $payname;
282 $content{address} = exists($options->{'address1'})
283 ? $options->{'address1'}
285 my $address2 = exists($options->{'address2'})
286 ? $options->{'address2'}
288 $content{address} .= ", ". $address2 if length($address2);
290 $content{city} = exists($options->{city})
293 $content{state} = exists($options->{state})
296 $content{zip} = exists($options->{zip})
299 $content{country} = exists($options->{country})
300 ? $options->{country}
303 #3.0 is a good a time as any to get rid of this... add a config to pass it
304 # if anyone still needs it
305 #$content{referer} = 'http://cleanwhisker.420.am/';
307 $content{phone} = $self->daytime || $self->night;
309 my $currency = $conf->exists('business-onlinepayment-currency')
310 && $conf->config('business-onlinepayment-currency');
311 $content{currency} = $currency if $currency;
316 my %bop_method2payby = (
325 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
328 if (ref($_[0]) eq 'HASH') {
331 my ( $method, $amount ) = ( shift, shift );
333 $options{method} = $method;
334 $options{amount} = $amount;
339 # optional credit card surcharge
342 my $cc_surcharge = 0;
343 my $cc_surcharge_pct = 0;
344 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
345 if $conf->config('credit-card-surcharge-percentage');
347 # always add cc surcharge if called from event
348 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
349 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
350 $options{'amount'} += $cc_surcharge;
351 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
353 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
354 # payment screen), so consider the given
355 # amount as post-surcharge
356 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
359 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
360 $options{'cc_surcharge'} = $cc_surcharge;
364 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
365 warn " cc_surcharge = $cc_surcharge\n";
366 warn " $_ => $options{$_}\n" foreach keys %options;
369 return $self->fake_bop(\%options) if $options{'fake'};
371 $self->_bop_defaults(\%options);
374 # set trans_is_recur based on invnum if there is one
377 my $trans_is_recur = 0;
378 if ( $options{'invnum'} ) {
380 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
381 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
387 $cust_bill->cust_bill_pkg;
390 if grep { $_->freq ne '0' } @part_pkg;
398 my $payment_gateway = $self->_payment_gateway( \%options );
399 my $namespace = $payment_gateway->gateway_namespace;
401 eval "use $namespace";
405 # check for banned credit card/ACH
408 my $ban = FS::banned_pay->ban_search(
409 'payby' => $bop_method2payby{$options{method}},
410 'payinfo' => $options{payinfo},
412 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
415 # check for term discount validity
418 my $discount_term = $options{discount_term};
419 if ( $discount_term ) {
420 my $bill = ($self->cust_bill)[-1]
421 or return "Can't apply a term discount to an unbilled customer";
422 my $plan = FS::discount_plan->new(
424 months => $discount_term
425 ) or return "No discount available for term '$discount_term'";
427 if ( $plan->discounted_total != $options{amount} ) {
428 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
436 my $bop_content = $self->_bop_content(\%options);
437 return $bop_content unless ref($bop_content);
439 my @invoicing_list = $self->invoicing_list_emailonly;
440 if ( $conf->exists('emailinvoiceautoalways')
441 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
442 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
443 push @invoicing_list, $self->all_emails;
446 my $email = ($conf->exists('business-onlinepayment-email-override'))
447 ? $conf->config('business-onlinepayment-email-override')
448 : $invoicing_list[0];
453 if ( $namespace eq 'Business::OnlinePayment' ) {
455 if ( $options{method} eq 'CC' ) {
457 $content{card_number} = $options{payinfo};
458 $paydate = exists($options{'paydate'})
459 ? $options{'paydate'}
461 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
462 $content{expiration} = "$2/$1";
464 my $paycvv = exists($options{'paycvv'})
467 $content{cvv2} = $paycvv
470 my $paystart_month = exists($options{'paystart_month'})
471 ? $options{'paystart_month'}
472 : $self->paystart_month;
474 my $paystart_year = exists($options{'paystart_year'})
475 ? $options{'paystart_year'}
476 : $self->paystart_year;
478 $content{card_start} = "$paystart_month/$paystart_year"
479 if $paystart_month && $paystart_year;
481 my $payissue = exists($options{'payissue'})
482 ? $options{'payissue'}
484 $content{issue_number} = $payissue if $payissue;
486 if ( $self->_bop_recurring_billing(
487 'payinfo' => $options{'payinfo'},
488 'trans_is_recur' => $trans_is_recur,
492 $content{recurring_billing} = 'YES';
493 $content{acct_code} = 'rebill'
494 if $conf->exists('credit_card-recurring_billing_acct_code');
497 } elsif ( $options{method} eq 'ECHECK' ){
499 ( $content{account_number}, $content{routing_code} ) =
500 split('@', $options{payinfo});
501 $content{bank_name} = $options{payname};
502 $content{bank_state} = exists($options{'paystate'})
503 ? $options{'paystate'}
504 : $self->getfield('paystate');
505 $content{account_type}=
506 (exists($options{'paytype'}) && $options{'paytype'})
507 ? uc($options{'paytype'})
508 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
509 $content{account_name} = $self->getfield('first'). ' '.
510 $self->getfield('last');
512 $content{customer_org} = $self->company ? 'B' : 'I';
513 $content{state_id} = exists($options{'stateid'})
514 ? $options{'stateid'}
515 : $self->getfield('stateid');
516 $content{state_id_state} = exists($options{'stateid_state'})
517 ? $options{'stateid_state'}
518 : $self->getfield('stateid_state');
519 $content{customer_ssn} = exists($options{'ss'})
523 } elsif ( $options{method} eq 'LEC' ) {
524 $content{phone} = $options{payinfo};
526 die "unknown method ". $options{method};
529 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
532 die "unknown namespace $namespace";
539 my $balance = exists( $options{'balance'} )
540 ? $options{'balance'}
543 $self->select_for_update; #mutex ... just until we get our pending record in
545 #the checks here are intended to catch concurrent payments
546 #double-form-submission prevention is taken care of in cust_pay_pending::check
549 return "The customer's balance has changed; $options{method} transaction aborted."
550 if $self->balance < $balance;
552 #also check and make sure there aren't *other* pending payments for this cust
554 my @pending = qsearch('cust_pay_pending', {
555 'custnum' => $self->custnum,
556 'status' => { op=>'!=', value=>'done' }
559 #for third-party payments only, remove pending payments if they're in the
560 #'thirdparty' (waiting for customer action) state.
561 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
562 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
563 my $error = $_->delete;
564 warn "error deleting unfinished third-party payment ".
565 $_->paypendingnum . ": $error\n"
568 @pending = grep { $_->status ne 'thirdparty' } @pending;
571 return "A payment is already being processed for this customer (".
572 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
573 "); $options{method} transaction aborted."
576 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
578 my $cust_pay_pending = new FS::cust_pay_pending {
579 'custnum' => $self->custnum,
580 'paid' => $options{amount},
582 'payby' => $bop_method2payby{$options{method}},
583 'payinfo' => $options{payinfo},
584 'paydate' => $paydate,
585 'recurring_billing' => $content{recurring_billing},
586 'pkgnum' => $options{'pkgnum'},
588 'gatewaynum' => $payment_gateway->gatewaynum || '',
589 'session_id' => $options{session_id} || '',
590 'jobnum' => $options{depend_jobnum} || '',
592 $cust_pay_pending->payunique( $options{payunique} )
593 if defined($options{payunique}) && length($options{payunique});
594 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
595 return $cpp_new_err if $cpp_new_err;
597 my( $action1, $action2 ) =
598 split( /\s*\,\s*/, $payment_gateway->gateway_action );
600 my $transaction = new $namespace( $payment_gateway->gateway_module,
601 $self->_bop_options(\%options),
604 $transaction->content(
605 'type' => $options{method},
606 $self->_bop_auth(\%options),
607 'action' => $action1,
608 'description' => $options{'description'},
609 'amount' => $options{amount},
610 #'invoice_number' => $options{'invnum'},
611 'customer_id' => $self->custnum,
613 'reference' => $cust_pay_pending->paypendingnum, #for now
614 'callback_url' => $payment_gateway->gateway_callback_url,
619 $cust_pay_pending->status('pending');
620 my $cpp_pending_err = $cust_pay_pending->replace;
621 return $cpp_pending_err if $cpp_pending_err;
623 warn Dumper($transaction) if $DEBUG > 2;
625 unless ( $BOP_TESTING ) {
626 $transaction->test_transaction(1)
627 if $conf->exists('business-onlinepayment-test_transaction');
628 $transaction->submit();
630 if ( $BOP_TESTING_SUCCESS ) {
631 $transaction->is_success(1);
632 $transaction->authorization('fake auth');
634 $transaction->is_success(0);
635 $transaction->error_message('fake failure');
639 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
641 $cust_pay_pending->status('thirdparty');
642 my $cpp_err = $cust_pay_pending->replace;
643 return { error => $cpp_err } if $cpp_err;
644 return { reference => $cust_pay_pending->paypendingnum,
645 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
647 } elsif ( $transaction->is_success() && $action2 ) {
649 $cust_pay_pending->status('authorized');
650 my $cpp_authorized_err = $cust_pay_pending->replace;
651 return $cpp_authorized_err if $cpp_authorized_err;
653 my $auth = $transaction->authorization;
654 my $ordernum = $transaction->can('order_number')
655 ? $transaction->order_number
659 new Business::OnlinePayment( $payment_gateway->gateway_module,
660 $self->_bop_options(\%options),
665 type => $options{method},
667 $self->_bop_auth(\%options),
668 order_number => $ordernum,
669 amount => $options{amount},
670 authorization => $auth,
671 description => $options{'description'},
674 foreach my $field (qw( authorization_source_code returned_ACI
675 transaction_identifier validation_code
676 transaction_sequence_num local_transaction_date
677 local_transaction_time AVS_result_code )) {
678 $capture{$field} = $transaction->$field() if $transaction->can($field);
681 $capture->content( %capture );
683 $capture->test_transaction(1)
684 if $conf->exists('business-onlinepayment-test_transaction');
687 unless ( $capture->is_success ) {
688 my $e = "Authorization successful but capture failed, custnum #".
689 $self->custnum. ': '. $capture->result_code.
690 ": ". $capture->error_message;
698 # remove paycvv after initial transaction
701 #false laziness w/misc/process/payment.cgi - check both to make sure working
703 if ( length($self->paycvv)
704 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
706 my $error = $self->remove_cvv;
708 warn "WARNING: error removing cvv: $error\n";
717 if ( $transaction->can('card_token') && $transaction->card_token ) {
719 $self->card_token($transaction->card_token);
721 if ( $options{'payinfo'} eq $self->payinfo ) {
722 $self->payinfo($transaction->card_token);
723 my $error = $self->replace;
725 warn "WARNING: error storing token: $error, but proceeding anyway\n";
735 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
747 if (ref($_[0]) eq 'HASH') {
750 my ( $method, $amount ) = ( shift, shift );
752 $options{method} = $method;
753 $options{amount} = $amount;
756 if ( $options{'fake_failure'} ) {
757 return "Error: No error; test failure requested with fake_failure";
761 #if ( $payment_gateway->gatewaynum ) { # agent override
762 # $paybatch = $payment_gateway->gatewaynum. '-';
765 #$paybatch .= "$processor:". $transaction->authorization;
767 #$paybatch .= ':'. $transaction->order_number
768 # if $transaction->can('order_number')
769 # && length($transaction->order_number);
771 my $paybatch = 'FakeProcessor:54:32';
773 my $cust_pay = new FS::cust_pay ( {
774 'custnum' => $self->custnum,
775 'invnum' => $options{'invnum'},
776 'paid' => $options{amount},
778 'payby' => $bop_method2payby{$options{method}},
779 #'payinfo' => $payinfo,
780 'payinfo' => '4111111111111111',
781 'paybatch' => $paybatch,
782 #'paydate' => $paydate,
783 'paydate' => '2012-05-01',
785 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
788 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
789 warn " $_ => $options{$_}\n" foreach keys %options;
792 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
795 $cust_pay->invnum(''); #try again with no specific invnum
796 my $error2 = $cust_pay->insert( $options{'manual'} ?
797 ( 'manual' => 1 ) : ()
800 # gah, even with transactions.
801 my $e = 'WARNING: Card/ACH debited but database not updated - '.
802 "error inserting (fake!) payment: $error2".
803 " (previously tried insert with invnum #$options{'invnum'}" .
810 if ( $options{'paynum_ref'} ) {
811 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
819 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
821 # Wraps up processing of a realtime credit card, ACH (electronic check) or
822 # phone bill transaction.
824 sub _realtime_bop_result {
825 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
827 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
830 warn "$me _realtime_bop_result: pending transaction ".
831 $cust_pay_pending->paypendingnum. "\n";
832 warn " $_ => $options{$_}\n" foreach keys %options;
835 my $payment_gateway = $options{payment_gateway}
836 or return "no payment gateway in arguments to _realtime_bop_result";
838 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
839 my $cpp_captured_err = $cust_pay_pending->replace;
840 return $cpp_captured_err if $cpp_captured_err;
842 if ( $transaction->is_success() ) {
845 if ( $payment_gateway->gatewaynum ) { # agent override
846 $paybatch = $payment_gateway->gatewaynum. '-';
849 $paybatch .= $payment_gateway->gateway_module. ":".
850 $transaction->authorization;
852 $paybatch .= ':'. $transaction->order_number
853 if $transaction->can('order_number')
854 && length($transaction->order_number);
856 my $cust_pay = new FS::cust_pay ( {
857 'custnum' => $self->custnum,
858 'invnum' => $options{'invnum'},
859 'paid' => $cust_pay_pending->paid,
861 'payby' => $cust_pay_pending->payby,
862 'payinfo' => $options{'payinfo'},
863 'paybatch' => $paybatch,
864 'paydate' => $cust_pay_pending->paydate,
865 'pkgnum' => $cust_pay_pending->pkgnum,
866 'discount_term' => $options{'discount_term'},
868 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
869 $cust_pay->payunique( $options{payunique} )
870 if defined($options{payunique}) && length($options{payunique});
872 my $oldAutoCommit = $FS::UID::AutoCommit;
873 local $FS::UID::AutoCommit = 0;
876 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
878 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
881 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
882 $cust_pay->invnum(''); #try again with no specific invnum
883 $cust_pay->paynum('');
884 my $error2 = $cust_pay->insert( $options{'manual'} ?
885 ( 'manual' => 1 ) : ()
888 # gah. but at least we have a record of the state we had to abort in
889 # from cust_pay_pending now.
890 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
891 my $e = "WARNING: $options{method} captured but payment not recorded -".
892 " error inserting payment (". $payment_gateway->gateway_module.
894 " (previously tried insert with invnum #$options{'invnum'}" .
895 ": $error ) - pending payment saved as paypendingnum ".
896 $cust_pay_pending->paypendingnum. "\n";
902 my $jobnum = $cust_pay_pending->jobnum;
904 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
906 unless ( $placeholder ) {
907 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
908 my $e = "WARNING: $options{method} captured but job $jobnum not ".
909 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
914 $error = $placeholder->delete;
917 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
918 my $e = "WARNING: $options{method} captured but could not delete ".
919 "job $jobnum for paypendingnum ".
920 $cust_pay_pending->paypendingnum. ": $error\n";
927 if ( $options{'paynum_ref'} ) {
928 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
931 $cust_pay_pending->status('done');
932 $cust_pay_pending->statustext('captured');
933 $cust_pay_pending->paynum($cust_pay->paynum);
934 my $cpp_done_err = $cust_pay_pending->replace;
936 if ( $cpp_done_err ) {
938 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
939 my $e = "WARNING: $options{method} captured but payment not recorded - ".
940 "error updating status for paypendingnum ".
941 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
947 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
949 if ( $options{'apply'} ) {
950 my $apply_error = $self->apply_payments_and_credits;
951 if ( $apply_error ) {
952 warn "WARNING: error applying payment: $apply_error\n";
953 #but we still should return no error cause the payment otherwise went
958 # have a CC surcharge portion --> one-time charge
959 if ( $options{'cc_surcharge'} > 0 ) {
960 # XXX: this whole block needs to be in a transaction?
963 $invnum = $options{'invnum'} if $options{'invnum'};
964 unless ( $invnum ) { # probably from a payment screen
965 # do we have any open invoices? pick earliest
966 # uses the fact that cust_main->cust_bill sorts by date ascending
967 my @open = $self->open_cust_bill;
968 $invnum = $open[0]->invnum if scalar(@open);
971 unless ( $invnum ) { # still nothing? pick last closed invoice
972 # again uses fact that cust_main->cust_bill sorts by date ascending
973 my @closed = $self->cust_bill;
974 $invnum = $closed[$#closed]->invnum if scalar(@closed);
978 # XXX: unlikely case - pre-paying before any invoices generated
979 # what it should do is create a new invoice and pick it
980 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
985 my $charge_error = $self->charge({
986 'amount' => $options{'cc_surcharge'},
987 'pkg' => 'Credit Card Surcharge',
989 'cust_pkg_ref' => \$cust_pkg,
992 warn 'Unable to add CC surcharge cust_pkg';
996 $cust_pkg->setup(time);
997 my $cp_error = $cust_pkg->replace;
999 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1003 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1004 unless ( $cust_bill ) {
1005 warn "race condition + invoice deletion just happened";
1010 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1012 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1016 return ''; #no error
1022 my $perror = $payment_gateway->gateway_module. " error: ".
1023 $transaction->error_message;
1025 my $jobnum = $cust_pay_pending->jobnum;
1027 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1029 if ( $placeholder ) {
1030 my $error = $placeholder->depended_delete;
1031 $error ||= $placeholder->delete;
1032 warn "error removing provisioning jobs after declined paypendingnum ".
1033 $cust_pay_pending->paypendingnum. ": $error\n";
1035 my $e = "error finding job $jobnum for declined paypendingnum ".
1036 $cust_pay_pending->paypendingnum. "\n";
1042 unless ( $transaction->error_message ) {
1045 if ( $transaction->can('response_page') ) {
1047 'page' => ( $transaction->can('response_page')
1048 ? $transaction->response_page
1051 'code' => ( $transaction->can('response_code')
1052 ? $transaction->response_code
1055 'headers' => ( $transaction->can('response_headers')
1056 ? $transaction->response_headers
1062 "No additional debugging information available for ".
1063 $payment_gateway->gateway_module;
1066 $perror .= "No error_message returned from ".
1067 $payment_gateway->gateway_module. " -- ".
1068 ( ref($t_response) ? Dumper($t_response) : $t_response );
1072 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1073 && $conf->exists('emaildecline', $self->agentnum)
1074 && grep { $_ ne 'POST' } $self->invoicing_list
1075 && ! grep { $transaction->error_message =~ /$_/ }
1076 $conf->config('emaildecline-exclude', $self->agentnum)
1079 # Send a decline alert to the customer.
1080 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1083 # include the raw error message in the transaction state
1084 $cust_pay_pending->setfield('error', $transaction->error_message);
1085 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1086 $error = $msg_template->send( 'cust_main' => $self,
1087 'object' => $cust_pay_pending );
1091 my @templ = $conf->config('declinetemplate');
1092 my $template = new Text::Template (
1094 SOURCE => [ map "$_\n", @templ ],
1095 ) or return "($perror) can't create template: $Text::Template::ERROR";
1096 $template->compile()
1097 or return "($perror) can't compile template: $Text::Template::ERROR";
1101 scalar( $conf->config('company_name', $self->agentnum ) ),
1102 'company_address' =>
1103 join("\n", $conf->config('company_address', $self->agentnum ) ),
1104 'error' => $transaction->error_message,
1107 my $error = send_email(
1108 'from' => $conf->config('invoice_from', $self->agentnum ),
1109 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1110 'subject' => 'Your payment could not be processed',
1111 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1115 $perror .= " (also received error sending decline notification: $error)"
1120 $cust_pay_pending->status('done');
1121 $cust_pay_pending->statustext("declined: $perror");
1122 my $cpp_done_err = $cust_pay_pending->replace;
1123 if ( $cpp_done_err ) {
1124 my $e = "WARNING: $options{method} declined but pending payment not ".
1125 "resolved - error updating status for paypendingnum ".
1126 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1128 $perror = "$e ($perror)";
1136 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1138 Verifies successful third party processing of a realtime credit card,
1139 ACH (electronic check) or phone bill transaction via a
1140 Business::OnlineThirdPartyPayment realtime gateway. See
1141 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1143 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1145 The additional options I<payname>, I<city>, I<state>,
1146 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1147 if set, will override the value from the customer record.
1149 I<description> is a free-text field passed to the gateway. It defaults to
1150 "Internet services".
1152 If an I<invnum> is specified, this payment (if successful) is applied to the
1153 specified invoice. If you don't specify an I<invnum> you might want to
1154 call the B<apply_payments> method.
1156 I<quiet> can be set true to surpress email decline notices.
1158 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1159 resulting paynum, if any.
1161 I<payunique> is a unique identifier for this payment.
1163 Returns a hashref containing elements bill_error (which will be undefined
1164 upon success) and session_id of any associated session.
1168 sub realtime_botpp_capture {
1169 my( $self, $cust_pay_pending, %options ) = @_;
1171 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1174 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1175 warn " $_ => $options{$_}\n" foreach keys %options;
1178 eval "use Business::OnlineThirdPartyPayment";
1182 # select the gateway
1185 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1187 my $payment_gateway;
1188 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1189 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1190 { gatewaynum => $gatewaynum }
1192 : $self->agent->payment_gateway( 'method' => $method,
1193 # 'invnum' => $cust_pay_pending->invnum,
1194 # 'payinfo' => $cust_pay_pending->payinfo,
1197 $options{payment_gateway} = $payment_gateway; # for the helper subs
1203 my @invoicing_list = $self->invoicing_list_emailonly;
1204 if ( $conf->exists('emailinvoiceautoalways')
1205 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1206 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1207 push @invoicing_list, $self->all_emails;
1210 my $email = ($conf->exists('business-onlinepayment-email-override'))
1211 ? $conf->config('business-onlinepayment-email-override')
1212 : $invoicing_list[0];
1216 $content{email_customer} =
1217 ( $conf->exists('business-onlinepayment-email_customer')
1218 || $conf->exists('business-onlinepayment-email-override') );
1221 # run transaction(s)
1225 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1226 $self->_bop_options(\%options),
1229 $transaction->reference({ %options });
1231 $transaction->content(
1233 $self->_bop_auth(\%options),
1234 'action' => 'Post Authorization',
1235 'description' => $options{'description'},
1236 'amount' => $cust_pay_pending->paid,
1237 #'invoice_number' => $options{'invnum'},
1238 'customer_id' => $self->custnum,
1240 #3.0 is a good a time as any to get rid of this... add a config to pass it
1241 # if anyone still needs it
1242 #'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,
1442 #3.0 is a good a time as any to get rid of this... add a config to pass it
1443 # if anyone still needs it
1444 #'referer' => 'http://cleanwhisker.420.am/',
1446 $content{authorization} = $auth
1447 if length($auth); #echeck/ACH transactions have an order # but no auth
1448 #(at least with authorize.net)
1450 my $currency = $conf->exists('business-onlinepayment-currency')
1451 && $conf->config('business-onlinepayment-currency');
1452 $content{currency} = $currency if $currency;
1454 my $disable_void_after;
1455 if ($conf->exists('disable_void_after')
1456 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1457 $disable_void_after = $1;
1460 #first try void if applicable
1461 my $void = new Business::OnlinePayment( $processor, @bop_options );
1464 if ($void->can('info')) {
1466 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1467 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1468 my %supported_actions = $void->info('supported_actions');
1470 if ( %supported_actions && $paytype
1471 && defined($supported_actions{$paytype})
1472 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1475 if ( $cust_pay && $cust_pay->paid == $amount
1477 ( not defined($disable_void_after) )
1478 || ( time < ($cust_pay->_date + $disable_void_after ) )
1482 warn " attempting void\n" if $DEBUG > 1;
1483 if ( $void->can('info') ) {
1484 if ( $cust_pay->payby eq 'CARD'
1485 && $void->info('CC_void_requires_card') )
1487 $content{'card_number'} = $cust_pay->payinfo;
1488 } elsif ( $cust_pay->payby eq 'CHEK'
1489 && $void->info('ECHECK_void_requires_account') )
1491 ( $content{'account_number'}, $content{'routing_code'} ) =
1492 split('@', $cust_pay->payinfo);
1493 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1496 $void->content( 'action' => 'void', %content );
1497 $void->test_transaction(1)
1498 if $conf->exists('business-onlinepayment-test_transaction');
1500 if ( $void->is_success ) {
1501 my $error = $cust_pay->void($options{'reason'});
1503 # gah, even with transactions.
1504 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1505 "error voiding payment: $error";
1509 warn " void successful\n" if $DEBUG > 1;
1514 warn " void unsuccessful, trying refund\n"
1518 my $address = $self->address1;
1519 $address .= ", ". $self->address2 if $self->address2;
1521 my($payname, $payfirst, $paylast);
1522 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1523 $payname = $self->payname;
1524 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1525 or return "Illegal payname $payname";
1526 ($payfirst, $paylast) = ($1, $2);
1528 $payfirst = $self->getfield('first');
1529 $paylast = $self->getfield('last');
1530 $payname = "$payfirst $paylast";
1533 my @invoicing_list = $self->invoicing_list_emailonly;
1534 if ( $conf->exists('emailinvoiceautoalways')
1535 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1536 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1537 push @invoicing_list, $self->all_emails;
1540 my $email = ($conf->exists('business-onlinepayment-email-override'))
1541 ? $conf->config('business-onlinepayment-email-override')
1542 : $invoicing_list[0];
1544 my $payip = exists($options{'payip'})
1547 $content{customer_ip} = $payip
1551 if ( $options{method} eq 'CC' ) {
1554 $content{card_number} = $payinfo = $cust_pay->payinfo;
1555 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1556 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1557 ($content{expiration} = "$2/$1"); # where available
1559 $content{card_number} = $payinfo = $self->payinfo;
1560 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1561 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1562 $content{expiration} = "$2/$1";
1565 } elsif ( $options{method} eq 'ECHECK' ) {
1568 $payinfo = $cust_pay->payinfo;
1570 $payinfo = $self->payinfo;
1572 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1573 $content{bank_name} = $self->payname;
1574 $content{account_type} = 'CHECKING';
1575 $content{account_name} = $payname;
1576 $content{customer_org} = $self->company ? 'B' : 'I';
1577 $content{customer_ssn} = $self->ss;
1578 } elsif ( $options{method} eq 'LEC' ) {
1579 $content{phone} = $payinfo = $self->payinfo;
1583 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1584 my %sub_content = $refund->content(
1585 'action' => 'credit',
1586 'customer_id' => $self->custnum,
1587 'last_name' => $paylast,
1588 'first_name' => $payfirst,
1590 'address' => $address,
1591 'city' => $self->city,
1592 'state' => $self->state,
1593 'zip' => $self->zip,
1594 'country' => $self->country,
1596 'phone' => $self->daytime || $self->night,
1599 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1601 $refund->test_transaction(1)
1602 if $conf->exists('business-onlinepayment-test_transaction');
1605 return "$processor error: ". $refund->error_message
1606 unless $refund->is_success();
1608 my $paybatch = "$processor:". $refund->authorization;
1609 $paybatch .= ':'. $refund->order_number
1610 if $refund->can('order_number') && $refund->order_number;
1612 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1613 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1614 last unless @cust_bill_pay;
1615 my $cust_bill_pay = pop @cust_bill_pay;
1616 my $error = $cust_bill_pay->delete;
1620 my $cust_refund = new FS::cust_refund ( {
1621 'custnum' => $self->custnum,
1622 'paynum' => $options{'paynum'},
1623 'refund' => $amount,
1625 'payby' => $bop_method2payby{$options{method}},
1626 'payinfo' => $payinfo,
1627 'paybatch' => $paybatch,
1628 'reason' => $options{'reason'} || 'card or ACH refund',
1630 my $error = $cust_refund->insert;
1632 $cust_refund->paynum(''); #try again with no specific paynum
1633 my $error2 = $cust_refund->insert;
1635 # gah, even with transactions.
1636 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1637 "error inserting refund ($processor): $error2".
1638 " (previously tried insert with paynum #$options{'paynum'}" .
1657 L<FS::cust_main>, L<FS::cust_main::Billing>