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>, I<LEC>, and I<PAYPAL>
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 = (
326 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
329 if (ref($_[0]) eq 'HASH') {
332 my ( $method, $amount ) = ( shift, shift );
334 $options{method} = $method;
335 $options{amount} = $amount;
340 # optional credit card surcharge
343 my $cc_surcharge = 0;
344 my $cc_surcharge_pct = 0;
345 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
346 if $conf->config('credit-card-surcharge-percentage');
348 # always add cc surcharge if called from event
349 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
350 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
351 $options{'amount'} += $cc_surcharge;
352 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
354 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
355 # payment screen), so consider the given
356 # amount as post-surcharge
357 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
360 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
361 $options{'cc_surcharge'} = $cc_surcharge;
365 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
366 warn " cc_surcharge = $cc_surcharge\n";
367 warn " $_ => $options{$_}\n" foreach keys %options;
370 return $self->fake_bop(\%options) if $options{'fake'};
372 $self->_bop_defaults(\%options);
375 # set trans_is_recur based on invnum if there is one
378 my $trans_is_recur = 0;
379 if ( $options{'invnum'} ) {
381 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
382 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
388 $cust_bill->cust_bill_pkg;
391 if grep { $_->freq ne '0' } @part_pkg;
399 my $payment_gateway = $self->_payment_gateway( \%options );
400 my $namespace = $payment_gateway->gateway_namespace;
402 eval "use $namespace";
406 # check for banned credit card/ACH
409 my $ban = FS::banned_pay->ban_search(
410 'payby' => $bop_method2payby{$options{method}},
411 'payinfo' => $options{payinfo},
413 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
416 # check for term discount validity
419 my $discount_term = $options{discount_term};
420 if ( $discount_term ) {
421 my $bill = ($self->cust_bill)[-1]
422 or return "Can't apply a term discount to an unbilled customer";
423 my $plan = FS::discount_plan->new(
425 months => $discount_term
426 ) or return "No discount available for term '$discount_term'";
428 if ( $plan->discounted_total != $options{amount} ) {
429 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
437 my $bop_content = $self->_bop_content(\%options);
438 return $bop_content unless ref($bop_content);
440 my @invoicing_list = $self->invoicing_list_emailonly;
441 if ( $conf->exists('emailinvoiceautoalways')
442 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
443 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
444 push @invoicing_list, $self->all_emails;
447 my $email = ($conf->exists('business-onlinepayment-email-override'))
448 ? $conf->config('business-onlinepayment-email-override')
449 : $invoicing_list[0];
454 if ( $namespace eq 'Business::OnlinePayment' ) {
456 if ( $options{method} eq 'CC' ) {
458 $content{card_number} = $options{payinfo};
459 $paydate = exists($options{'paydate'})
460 ? $options{'paydate'}
462 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
463 $content{expiration} = "$2/$1";
465 my $paycvv = exists($options{'paycvv'})
468 $content{cvv2} = $paycvv
471 my $paystart_month = exists($options{'paystart_month'})
472 ? $options{'paystart_month'}
473 : $self->paystart_month;
475 my $paystart_year = exists($options{'paystart_year'})
476 ? $options{'paystart_year'}
477 : $self->paystart_year;
479 $content{card_start} = "$paystart_month/$paystart_year"
480 if $paystart_month && $paystart_year;
482 my $payissue = exists($options{'payissue'})
483 ? $options{'payissue'}
485 $content{issue_number} = $payissue if $payissue;
487 if ( $self->_bop_recurring_billing(
488 'payinfo' => $options{'payinfo'},
489 'trans_is_recur' => $trans_is_recur,
493 $content{recurring_billing} = 'YES';
494 $content{acct_code} = 'rebill'
495 if $conf->exists('credit_card-recurring_billing_acct_code');
498 } elsif ( $options{method} eq 'ECHECK' ){
500 ( $content{account_number}, $content{routing_code} ) =
501 split('@', $options{payinfo});
502 $content{bank_name} = $options{payname};
503 $content{bank_state} = exists($options{'paystate'})
504 ? $options{'paystate'}
505 : $self->getfield('paystate');
506 $content{account_type}=
507 (exists($options{'paytype'}) && $options{'paytype'})
508 ? uc($options{'paytype'})
509 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
511 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
512 $content{account_name} = $self->company;
514 $content{account_name} = $self->getfield('first'). ' '.
515 $self->getfield('last');
518 $content{customer_org} = $self->company ? 'B' : 'I';
519 $content{state_id} = exists($options{'stateid'})
520 ? $options{'stateid'}
521 : $self->getfield('stateid');
522 $content{state_id_state} = exists($options{'stateid_state'})
523 ? $options{'stateid_state'}
524 : $self->getfield('stateid_state');
525 $content{customer_ssn} = exists($options{'ss'})
529 } elsif ( $options{method} eq 'LEC' ) {
530 $content{phone} = $options{payinfo};
532 die "unknown method ". $options{method};
535 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
538 die "unknown namespace $namespace";
545 my $balance = exists( $options{'balance'} )
546 ? $options{'balance'}
549 $self->select_for_update; #mutex ... just until we get our pending record in
551 #the checks here are intended to catch concurrent payments
552 #double-form-submission prevention is taken care of in cust_pay_pending::check
555 return "The customer's balance has changed; $options{method} transaction aborted."
556 if $self->balance < $balance;
558 #also check and make sure there aren't *other* pending payments for this cust
560 my @pending = qsearch('cust_pay_pending', {
561 'custnum' => $self->custnum,
562 'status' => { op=>'!=', value=>'done' }
565 #for third-party payments only, remove pending payments if they're in the
566 #'thirdparty' (waiting for customer action) state.
567 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
568 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
569 my $error = $_->delete;
570 warn "error deleting unfinished third-party payment ".
571 $_->paypendingnum . ": $error\n"
574 @pending = grep { $_->status ne 'thirdparty' } @pending;
577 return "A payment is already being processed for this customer (".
578 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
579 "); $options{method} transaction aborted."
582 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
584 my $cust_pay_pending = new FS::cust_pay_pending {
585 'custnum' => $self->custnum,
586 'paid' => $options{amount},
588 'payby' => $bop_method2payby{$options{method}},
589 'payinfo' => $options{payinfo},
590 'paydate' => $paydate,
591 'recurring_billing' => $content{recurring_billing},
592 'pkgnum' => $options{'pkgnum'},
594 'gatewaynum' => $payment_gateway->gatewaynum || '',
595 'session_id' => $options{session_id} || '',
596 'jobnum' => $options{depend_jobnum} || '',
598 $cust_pay_pending->payunique( $options{payunique} )
599 if defined($options{payunique}) && length($options{payunique});
600 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
601 return $cpp_new_err if $cpp_new_err;
603 my( $action1, $action2 ) =
604 split( /\s*\,\s*/, $payment_gateway->gateway_action );
606 my $transaction = new $namespace( $payment_gateway->gateway_module,
607 $self->_bop_options(\%options),
610 $transaction->content(
611 'type' => $options{method},
612 $self->_bop_auth(\%options),
613 'action' => $action1,
614 'description' => $options{'description'},
615 'amount' => $options{amount},
616 #'invoice_number' => $options{'invnum'},
617 'customer_id' => $self->custnum,
619 'reference' => $cust_pay_pending->paypendingnum, #for now
620 'callback_url' => $payment_gateway->gateway_callback_url,
621 'cancel_url' => $payment_gateway->gateway_cancel_url,
626 $cust_pay_pending->status('pending');
627 my $cpp_pending_err = $cust_pay_pending->replace;
628 return $cpp_pending_err if $cpp_pending_err;
630 warn Dumper($transaction) if $DEBUG > 2;
632 unless ( $BOP_TESTING ) {
633 $transaction->test_transaction(1)
634 if $conf->exists('business-onlinepayment-test_transaction');
635 $transaction->submit();
637 if ( $BOP_TESTING_SUCCESS ) {
638 $transaction->is_success(1);
639 $transaction->authorization('fake auth');
641 $transaction->is_success(0);
642 $transaction->error_message('fake failure');
646 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
648 $cust_pay_pending->status('thirdparty');
649 my $cpp_err = $cust_pay_pending->replace;
650 return { error => $cpp_err } if $cpp_err;
651 return { reference => $cust_pay_pending->paypendingnum,
652 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
654 } elsif ( $transaction->is_success() && $action2 ) {
656 $cust_pay_pending->status('authorized');
657 my $cpp_authorized_err = $cust_pay_pending->replace;
658 return $cpp_authorized_err if $cpp_authorized_err;
660 my $auth = $transaction->authorization;
661 my $ordernum = $transaction->can('order_number')
662 ? $transaction->order_number
666 new Business::OnlinePayment( $payment_gateway->gateway_module,
667 $self->_bop_options(\%options),
672 type => $options{method},
674 $self->_bop_auth(\%options),
675 order_number => $ordernum,
676 amount => $options{amount},
677 authorization => $auth,
678 description => $options{'description'},
681 foreach my $field (qw( authorization_source_code returned_ACI
682 transaction_identifier validation_code
683 transaction_sequence_num local_transaction_date
684 local_transaction_time AVS_result_code )) {
685 $capture{$field} = $transaction->$field() if $transaction->can($field);
688 $capture->content( %capture );
690 $capture->test_transaction(1)
691 if $conf->exists('business-onlinepayment-test_transaction');
694 unless ( $capture->is_success ) {
695 my $e = "Authorization successful but capture failed, custnum #".
696 $self->custnum. ': '. $capture->result_code.
697 ": ". $capture->error_message;
705 # remove paycvv after initial transaction
708 #false laziness w/misc/process/payment.cgi - check both to make sure working
710 if ( length($self->paycvv)
711 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
713 my $error = $self->remove_cvv;
715 warn "WARNING: error removing cvv: $error\n";
724 if ( $transaction->can('card_token') && $transaction->card_token ) {
726 $self->card_token($transaction->card_token);
728 if ( $options{'payinfo'} eq $self->payinfo ) {
729 $self->payinfo($transaction->card_token);
730 my $error = $self->replace;
732 warn "WARNING: error storing token: $error, but proceeding anyway\n";
742 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
754 if (ref($_[0]) eq 'HASH') {
757 my ( $method, $amount ) = ( shift, shift );
759 $options{method} = $method;
760 $options{amount} = $amount;
763 if ( $options{'fake_failure'} ) {
764 return "Error: No error; test failure requested with fake_failure";
767 my $cust_pay = new FS::cust_pay ( {
768 'custnum' => $self->custnum,
769 'invnum' => $options{'invnum'},
770 'paid' => $options{amount},
772 'payby' => $bop_method2payby{$options{method}},
773 #'payinfo' => $payinfo,
774 'payinfo' => '4111111111111111',
775 #'paydate' => $paydate,
776 'paydate' => '2012-05-01',
777 'processor' => 'FakeProcessor',
779 'order_number' => '32',
781 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
784 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
785 warn " $_ => $options{$_}\n" foreach keys %options;
788 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
791 $cust_pay->invnum(''); #try again with no specific invnum
792 my $error2 = $cust_pay->insert( $options{'manual'} ?
793 ( 'manual' => 1 ) : ()
796 # gah, even with transactions.
797 my $e = 'WARNING: Card/ACH debited but database not updated - '.
798 "error inserting (fake!) payment: $error2".
799 " (previously tried insert with invnum #$options{'invnum'}" .
806 if ( $options{'paynum_ref'} ) {
807 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
815 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
817 # Wraps up processing of a realtime credit card, ACH (electronic check) or
818 # phone bill transaction.
820 sub _realtime_bop_result {
821 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
823 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
826 warn "$me _realtime_bop_result: pending transaction ".
827 $cust_pay_pending->paypendingnum. "\n";
828 warn " $_ => $options{$_}\n" foreach keys %options;
831 my $payment_gateway = $options{payment_gateway}
832 or return "no payment gateway in arguments to _realtime_bop_result";
834 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
835 my $cpp_captured_err = $cust_pay_pending->replace;
836 return $cpp_captured_err if $cpp_captured_err;
838 if ( $transaction->is_success() ) {
840 my $order_number = $transaction->order_number
841 if $transaction->can('order_number');
843 my $cust_pay = new FS::cust_pay ( {
844 'custnum' => $self->custnum,
845 'invnum' => $options{'invnum'},
846 'paid' => $cust_pay_pending->paid,
848 'payby' => $cust_pay_pending->payby,
849 'payinfo' => $options{'payinfo'},
850 'paydate' => $cust_pay_pending->paydate,
851 'pkgnum' => $cust_pay_pending->pkgnum,
852 'discount_term' => $options{'discount_term'},
853 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
854 'processor' => $payment_gateway->gateway_module,
855 'auth' => $transaction->authorization,
856 'order_number' => $order_number || '',
859 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
860 $cust_pay->payunique( $options{payunique} )
861 if defined($options{payunique}) && length($options{payunique});
863 my $oldAutoCommit = $FS::UID::AutoCommit;
864 local $FS::UID::AutoCommit = 0;
867 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
869 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
872 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
873 $cust_pay->invnum(''); #try again with no specific invnum
874 $cust_pay->paynum('');
875 my $error2 = $cust_pay->insert( $options{'manual'} ?
876 ( 'manual' => 1 ) : ()
879 # gah. but at least we have a record of the state we had to abort in
880 # from cust_pay_pending now.
881 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
882 my $e = "WARNING: $options{method} captured but payment not recorded -".
883 " error inserting payment (". $payment_gateway->gateway_module.
885 " (previously tried insert with invnum #$options{'invnum'}" .
886 ": $error ) - pending payment saved as paypendingnum ".
887 $cust_pay_pending->paypendingnum. "\n";
893 my $jobnum = $cust_pay_pending->jobnum;
895 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
897 unless ( $placeholder ) {
898 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
899 my $e = "WARNING: $options{method} captured but job $jobnum not ".
900 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
905 $error = $placeholder->delete;
908 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
909 my $e = "WARNING: $options{method} captured but could not delete ".
910 "job $jobnum for paypendingnum ".
911 $cust_pay_pending->paypendingnum. ": $error\n";
918 if ( $options{'paynum_ref'} ) {
919 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
922 $cust_pay_pending->status('done');
923 $cust_pay_pending->statustext('captured');
924 $cust_pay_pending->paynum($cust_pay->paynum);
925 my $cpp_done_err = $cust_pay_pending->replace;
927 if ( $cpp_done_err ) {
929 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
930 my $e = "WARNING: $options{method} captured but payment not recorded - ".
931 "error updating status for paypendingnum ".
932 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
938 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
940 if ( $options{'apply'} ) {
941 my $apply_error = $self->apply_payments_and_credits;
942 if ( $apply_error ) {
943 warn "WARNING: error applying payment: $apply_error\n";
944 #but we still should return no error cause the payment otherwise went
949 # have a CC surcharge portion --> one-time charge
950 if ( $options{'cc_surcharge'} > 0 ) {
951 # XXX: this whole block needs to be in a transaction?
954 $invnum = $options{'invnum'} if $options{'invnum'};
955 unless ( $invnum ) { # probably from a payment screen
956 # do we have any open invoices? pick earliest
957 # uses the fact that cust_main->cust_bill sorts by date ascending
958 my @open = $self->open_cust_bill;
959 $invnum = $open[0]->invnum if scalar(@open);
962 unless ( $invnum ) { # still nothing? pick last closed invoice
963 # again uses fact that cust_main->cust_bill sorts by date ascending
964 my @closed = $self->cust_bill;
965 $invnum = $closed[$#closed]->invnum if scalar(@closed);
969 # XXX: unlikely case - pre-paying before any invoices generated
970 # what it should do is create a new invoice and pick it
971 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
976 my $charge_error = $self->charge({
977 'amount' => $options{'cc_surcharge'},
978 'pkg' => 'Credit Card Surcharge',
980 'cust_pkg_ref' => \$cust_pkg,
983 warn 'Unable to add CC surcharge cust_pkg';
987 $cust_pkg->setup(time);
988 my $cp_error = $cust_pkg->replace;
990 warn 'Unable to set setup time on cust_pkg for cc surcharge';
994 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
995 unless ( $cust_bill ) {
996 warn "race condition + invoice deletion just happened";
1001 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1003 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1007 return ''; #no error
1013 my $perror = $transaction->error_message;
1014 #$payment_gateway->gateway_module. " error: ".
1015 # removed for conciseness
1017 my $jobnum = $cust_pay_pending->jobnum;
1019 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1021 if ( $placeholder ) {
1022 my $error = $placeholder->depended_delete;
1023 $error ||= $placeholder->delete;
1024 warn "error removing provisioning jobs after declined paypendingnum ".
1025 $cust_pay_pending->paypendingnum. ": $error\n";
1027 my $e = "error finding job $jobnum for declined paypendingnum ".
1028 $cust_pay_pending->paypendingnum. "\n";
1034 unless ( $transaction->error_message ) {
1037 if ( $transaction->can('response_page') ) {
1039 'page' => ( $transaction->can('response_page')
1040 ? $transaction->response_page
1043 'code' => ( $transaction->can('response_code')
1044 ? $transaction->response_code
1047 'headers' => ( $transaction->can('response_headers')
1048 ? $transaction->response_headers
1054 "No additional debugging information available for ".
1055 $payment_gateway->gateway_module;
1058 $perror .= "No error_message returned from ".
1059 $payment_gateway->gateway_module. " -- ".
1060 ( ref($t_response) ? Dumper($t_response) : $t_response );
1064 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1065 && $conf->exists('emaildecline', $self->agentnum)
1066 && grep { $_ ne 'POST' } $self->invoicing_list
1067 && ! grep { $transaction->error_message =~ /$_/ }
1068 $conf->config('emaildecline-exclude', $self->agentnum)
1071 # Send a decline alert to the customer.
1072 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1075 # include the raw error message in the transaction state
1076 $cust_pay_pending->setfield('error', $transaction->error_message);
1077 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1078 $error = $msg_template->send( 'cust_main' => $self,
1079 'object' => $cust_pay_pending );
1083 my @templ = $conf->config('declinetemplate');
1084 my $template = new Text::Template (
1086 SOURCE => [ map "$_\n", @templ ],
1087 ) or return "($perror) can't create template: $Text::Template::ERROR";
1088 $template->compile()
1089 or return "($perror) can't compile template: $Text::Template::ERROR";
1093 scalar( $conf->config('company_name', $self->agentnum ) ),
1094 'company_address' =>
1095 join("\n", $conf->config('company_address', $self->agentnum ) ),
1096 'error' => $transaction->error_message,
1099 my $error = send_email(
1100 'from' => $conf->config('invoice_from', $self->agentnum ),
1101 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1102 'subject' => 'Your payment could not be processed',
1103 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1107 $perror .= " (also received error sending decline notification: $error)"
1112 $cust_pay_pending->status('done');
1113 $cust_pay_pending->statustext($perror);
1114 #'declined:': no, that's failure_status
1115 if ( $transaction->can('failure_status') ) {
1116 $cust_pay_pending->failure_status( $transaction->failure_status );
1118 my $cpp_done_err = $cust_pay_pending->replace;
1119 if ( $cpp_done_err ) {
1120 my $e = "WARNING: $options{method} declined but pending payment not ".
1121 "resolved - error updating status for paypendingnum ".
1122 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1124 $perror = "$e ($perror)";
1132 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1134 Verifies successful third party processing of a realtime credit card,
1135 ACH (electronic check) or phone bill transaction via a
1136 Business::OnlineThirdPartyPayment realtime gateway. See
1137 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1139 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1141 The additional options I<payname>, I<city>, I<state>,
1142 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1143 if set, will override the value from the customer record.
1145 I<description> is a free-text field passed to the gateway. It defaults to
1146 "Internet services".
1148 If an I<invnum> is specified, this payment (if successful) is applied to the
1149 specified invoice. If you don't specify an I<invnum> you might want to
1150 call the B<apply_payments> method.
1152 I<quiet> can be set true to surpress email decline notices.
1154 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1155 resulting paynum, if any.
1157 I<payunique> is a unique identifier for this payment.
1159 Returns a hashref containing elements bill_error (which will be undefined
1160 upon success) and session_id of any associated session.
1164 sub realtime_botpp_capture {
1165 my( $self, $cust_pay_pending, %options ) = @_;
1167 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1170 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1171 warn " $_ => $options{$_}\n" foreach keys %options;
1174 eval "use Business::OnlineThirdPartyPayment";
1178 # select the gateway
1181 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1183 my $payment_gateway;
1184 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1185 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1186 { gatewaynum => $gatewaynum }
1188 : $self->agent->payment_gateway( 'method' => $method,
1189 # 'invnum' => $cust_pay_pending->invnum,
1190 # 'payinfo' => $cust_pay_pending->payinfo,
1193 $options{payment_gateway} = $payment_gateway; # for the helper subs
1199 my @invoicing_list = $self->invoicing_list_emailonly;
1200 if ( $conf->exists('emailinvoiceautoalways')
1201 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1202 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1203 push @invoicing_list, $self->all_emails;
1206 my $email = ($conf->exists('business-onlinepayment-email-override'))
1207 ? $conf->config('business-onlinepayment-email-override')
1208 : $invoicing_list[0];
1212 $content{email_customer} =
1213 ( $conf->exists('business-onlinepayment-email_customer')
1214 || $conf->exists('business-onlinepayment-email-override') );
1217 # run transaction(s)
1221 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1222 $self->_bop_options(\%options),
1225 $transaction->reference({ %options });
1227 $transaction->content(
1229 $self->_bop_auth(\%options),
1230 'action' => 'Post Authorization',
1231 'description' => $options{'description'},
1232 'amount' => $cust_pay_pending->paid,
1233 #'invoice_number' => $options{'invnum'},
1234 'customer_id' => $self->custnum,
1236 #3.0 is a good a time as any to get rid of this... add a config to pass it
1237 # if anyone still needs it
1238 #'referer' => 'http://cleanwhisker.420.am/',
1240 'reference' => $cust_pay_pending->paypendingnum,
1242 'phone' => $self->daytime || $self->night,
1244 # plus whatever is required for bogus capture avoidance
1247 $transaction->submit();
1250 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1252 if ( $options{'apply'} ) {
1253 my $apply_error = $self->apply_payments_and_credits;
1254 if ( $apply_error ) {
1255 warn "WARNING: error applying payment: $apply_error\n";
1260 bill_error => $error,
1261 session_id => $cust_pay_pending->session_id,
1266 =item default_payment_gateway
1268 DEPRECATED -- use agent->payment_gateway
1272 sub default_payment_gateway {
1273 my( $self, $method ) = @_;
1275 die "Real-time processing not enabled\n"
1276 unless $conf->exists('business-onlinepayment');
1278 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1281 my $bop_config = 'business-onlinepayment';
1282 $bop_config .= '-ach'
1283 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1284 my ( $processor, $login, $password, $action, @bop_options ) =
1285 $conf->config($bop_config);
1286 $action ||= 'normal authorization';
1287 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1288 die "No real-time processor is enabled - ".
1289 "did you set the business-onlinepayment configuration value?\n"
1292 ( $processor, $login, $password, $action, @bop_options )
1295 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1297 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1298 via a Business::OnlinePayment realtime gateway. See
1299 L<http://420.am/business-onlinepayment> for supported gateways.
1301 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1303 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1305 Most gateways require a reference to an original payment transaction to refund,
1306 so you probably need to specify a I<paynum>.
1308 I<amount> defaults to the original amount of the payment if not specified.
1310 I<reason> specifies a reason for the refund.
1312 I<paydate> specifies the expiration date for a credit card overriding the
1313 value from the customer record or the payment record. Specified as yyyy-mm-dd
1315 Implementation note: If I<amount> is unspecified or equal to the amount of the
1316 orignal payment, first an attempt is made to "void" the transaction via
1317 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1318 the normal attempt is made to "refund" ("credit") the transaction via the
1319 gateway is attempted. No attempt to "void" the transaction is made if the
1320 gateway has introspection data and doesn't support void.
1322 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1323 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1324 #if set, will override the value from the customer record.
1326 #If an I<invnum> is specified, this payment (if successful) is applied to the
1327 #specified invoice. If you don't specify an I<invnum> you might want to
1328 #call the B<apply_payments> method.
1332 #some false laziness w/realtime_bop, not enough to make it worth merging
1333 #but some useful small subs should be pulled out
1334 sub realtime_refund_bop {
1337 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1340 if (ref($_[0]) eq 'HASH') {
1341 %options = %{$_[0]};
1345 $options{method} = $method;
1349 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1350 warn " $_ => $options{$_}\n" foreach keys %options;
1354 # look up the original payment and optionally a gateway for that payment
1358 my $amount = $options{'amount'};
1360 my( $processor, $login, $password, @bop_options, $namespace ) ;
1361 my( $auth, $order_number ) = ( '', '', '' );
1362 my $gatewaynum = '';
1364 if ( $options{'paynum'} ) {
1366 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1367 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1368 or return "Unknown paynum $options{'paynum'}";
1369 $amount ||= $cust_pay->paid;
1371 if ( $cust_pay->get('processor') ) {
1372 ($gatewaynum, $processor, $auth, $order_number) =
1374 $cust_pay->gatewaynum,
1375 $cust_pay->processor,
1377 $cust_pay->order_number,
1380 # this payment wasn't upgraded, which probably means this won't work,
1382 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1383 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1384 $cust_pay->paybatch;
1385 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1388 if ( $gatewaynum ) { #gateway for the payment to be refunded
1390 my $payment_gateway =
1391 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1392 die "payment gateway $gatewaynum not found"
1393 unless $payment_gateway;
1395 $processor = $payment_gateway->gateway_module;
1396 $login = $payment_gateway->gateway_username;
1397 $password = $payment_gateway->gateway_password;
1398 $namespace = $payment_gateway->gateway_namespace;
1399 @bop_options = $payment_gateway->options;
1401 } else { #try the default gateway
1404 my $payment_gateway =
1405 $self->agent->payment_gateway('method' => $options{method});
1407 ( $conf_processor, $login, $password, $namespace ) =
1408 map { my $method = "gateway_$_"; $payment_gateway->$method }
1409 qw( module username password namespace );
1411 @bop_options = $payment_gateway->gatewaynum
1412 ? $payment_gateway->options
1413 : @{ $payment_gateway->get('options') };
1415 return "processor of payment $options{'paynum'} $processor does not".
1416 " match default processor $conf_processor"
1417 unless $processor eq $conf_processor;
1422 } else { # didn't specify a paynum, so look for agent gateway overrides
1423 # like a normal transaction
1425 my $payment_gateway =
1426 $self->agent->payment_gateway( 'method' => $options{method},
1427 #'payinfo' => $payinfo,
1429 my( $processor, $login, $password, $namespace ) =
1430 map { my $method = "gateway_$_"; $payment_gateway->$method }
1431 qw( module username password namespace );
1433 my @bop_options = $payment_gateway->gatewaynum
1434 ? $payment_gateway->options
1435 : @{ $payment_gateway->get('options') };
1438 return "neither amount nor paynum specified" unless $amount;
1440 eval "use $namespace";
1444 'type' => $options{method},
1446 'password' => $password,
1447 'order_number' => $order_number,
1448 'amount' => $amount,
1450 #3.0 is a good a time as any to get rid of this... add a config to pass it
1451 # if anyone still needs it
1452 #'referer' => 'http://cleanwhisker.420.am/',
1454 $content{authorization} = $auth
1455 if length($auth); #echeck/ACH transactions have an order # but no auth
1456 #(at least with authorize.net)
1458 my $currency = $conf->exists('business-onlinepayment-currency')
1459 && $conf->config('business-onlinepayment-currency');
1460 $content{currency} = $currency if $currency;
1462 my $disable_void_after;
1463 if ($conf->exists('disable_void_after')
1464 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1465 $disable_void_after = $1;
1468 #first try void if applicable
1469 my $void = new Business::OnlinePayment( $processor, @bop_options );
1472 if ($void->can('info')) {
1474 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1475 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1476 my %supported_actions = $void->info('supported_actions');
1478 if ( %supported_actions && $paytype
1479 && defined($supported_actions{$paytype})
1480 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1483 if ( $cust_pay && $cust_pay->paid == $amount
1485 ( not defined($disable_void_after) )
1486 || ( time < ($cust_pay->_date + $disable_void_after ) )
1490 warn " attempting void\n" if $DEBUG > 1;
1491 if ( $void->can('info') ) {
1492 if ( $cust_pay->payby eq 'CARD'
1493 && $void->info('CC_void_requires_card') )
1495 $content{'card_number'} = $cust_pay->payinfo;
1496 } elsif ( $cust_pay->payby eq 'CHEK'
1497 && $void->info('ECHECK_void_requires_account') )
1499 ( $content{'account_number'}, $content{'routing_code'} ) =
1500 split('@', $cust_pay->payinfo);
1501 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1504 $void->content( 'action' => 'void', %content );
1505 $void->test_transaction(1)
1506 if $conf->exists('business-onlinepayment-test_transaction');
1508 if ( $void->is_success ) {
1509 my $error = $cust_pay->void($options{'reason'});
1511 # gah, even with transactions.
1512 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1513 "error voiding payment: $error";
1517 warn " void successful\n" if $DEBUG > 1;
1522 warn " void unsuccessful, trying refund\n"
1526 my $address = $self->address1;
1527 $address .= ", ". $self->address2 if $self->address2;
1529 my($payname, $payfirst, $paylast);
1530 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1531 $payname = $self->payname;
1532 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1533 or return "Illegal payname $payname";
1534 ($payfirst, $paylast) = ($1, $2);
1536 $payfirst = $self->getfield('first');
1537 $paylast = $self->getfield('last');
1538 $payname = "$payfirst $paylast";
1541 my @invoicing_list = $self->invoicing_list_emailonly;
1542 if ( $conf->exists('emailinvoiceautoalways')
1543 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1544 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1545 push @invoicing_list, $self->all_emails;
1548 my $email = ($conf->exists('business-onlinepayment-email-override'))
1549 ? $conf->config('business-onlinepayment-email-override')
1550 : $invoicing_list[0];
1552 my $payip = exists($options{'payip'})
1555 $content{customer_ip} = $payip
1559 if ( $options{method} eq 'CC' ) {
1562 $content{card_number} = $payinfo = $cust_pay->payinfo;
1563 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1564 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1565 ($content{expiration} = "$2/$1"); # where available
1567 $content{card_number} = $payinfo = $self->payinfo;
1568 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1569 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1570 $content{expiration} = "$2/$1";
1573 } elsif ( $options{method} eq 'ECHECK' ) {
1576 $payinfo = $cust_pay->payinfo;
1578 $payinfo = $self->payinfo;
1580 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1581 $content{bank_name} = $self->payname;
1582 $content{account_type} = 'CHECKING';
1583 $content{account_name} = $payname;
1584 $content{customer_org} = $self->company ? 'B' : 'I';
1585 $content{customer_ssn} = $self->ss;
1586 } elsif ( $options{method} eq 'LEC' ) {
1587 $content{phone} = $payinfo = $self->payinfo;
1591 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1592 my %sub_content = $refund->content(
1593 'action' => 'credit',
1594 'customer_id' => $self->custnum,
1595 'last_name' => $paylast,
1596 'first_name' => $payfirst,
1598 'address' => $address,
1599 'city' => $self->city,
1600 'state' => $self->state,
1601 'zip' => $self->zip,
1602 'country' => $self->country,
1604 'phone' => $self->daytime || $self->night,
1607 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1609 $refund->test_transaction(1)
1610 if $conf->exists('business-onlinepayment-test_transaction');
1613 return "$processor error: ". $refund->error_message
1614 unless $refund->is_success();
1616 $order_number = $refund->order_number if $refund->can('order_number');
1618 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1619 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1620 last unless @cust_bill_pay;
1621 my $cust_bill_pay = pop @cust_bill_pay;
1622 my $error = $cust_bill_pay->delete;
1626 my $cust_refund = new FS::cust_refund ( {
1627 'custnum' => $self->custnum,
1628 'paynum' => $options{'paynum'},
1629 'refund' => $amount,
1631 'payby' => $bop_method2payby{$options{method}},
1632 'payinfo' => $payinfo,
1633 'reason' => $options{'reason'} || 'card or ACH refund',
1634 'gatewaynum' => $gatewaynum, # may be null
1635 'processor' => $processor,
1636 'auth' => $refund->authorization,
1637 'order_number' => $order_number,
1639 my $error = $cust_refund->insert;
1641 $cust_refund->paynum(''); #try again with no specific paynum
1642 my $error2 = $cust_refund->insert;
1644 # gah, even with transactions.
1645 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1646 "error inserting refund ($processor): $error2".
1647 " (previously tried insert with paynum #$options{'paynum'}" .
1666 L<FS::cust_main>, L<FS::cust_main::Billing>