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 $content{phone} = $self->daytime || $self->night;
305 my $currency = $conf->exists('business-onlinepayment-currency')
306 && $conf->config('business-onlinepayment-currency');
307 $content{currency} = $currency if $currency;
312 my %bop_method2payby = (
322 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
325 if (ref($_[0]) eq 'HASH') {
328 my ( $method, $amount ) = ( shift, shift );
330 $options{method} = $method;
331 $options{amount} = $amount;
336 # optional credit card surcharge
339 my $cc_surcharge = 0;
340 my $cc_surcharge_pct = 0;
341 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
342 if $conf->config('credit-card-surcharge-percentage');
344 # always add cc surcharge if called from event
345 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
346 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
347 $options{'amount'} += $cc_surcharge;
348 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
350 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
351 # payment screen), so consider the given
352 # amount as post-surcharge
353 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
356 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
357 $options{'cc_surcharge'} = $cc_surcharge;
361 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
362 warn " cc_surcharge = $cc_surcharge\n";
365 warn " $_ => $options{$_}\n" foreach keys %options;
368 return $self->fake_bop(\%options) if $options{'fake'};
370 $self->_bop_defaults(\%options);
373 # set trans_is_recur based on invnum if there is one
376 my $trans_is_recur = 0;
377 if ( $options{'invnum'} ) {
379 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
380 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
386 $cust_bill->cust_bill_pkg;
389 if grep { $_->freq ne '0' } @part_pkg;
397 my $payment_gateway = $self->_payment_gateway( \%options );
398 my $namespace = $payment_gateway->gateway_namespace;
400 eval "use $namespace";
404 # check for banned credit card/ACH
407 my $ban = FS::banned_pay->ban_search(
408 'payby' => $bop_method2payby{$options{method}},
409 'payinfo' => $options{payinfo},
411 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
414 # check for term discount validity
417 my $discount_term = $options{discount_term};
418 if ( $discount_term ) {
419 my $bill = ($self->cust_bill)[-1]
420 or return "Can't apply a term discount to an unbilled customer";
421 my $plan = FS::discount_plan->new(
423 months => $discount_term
424 ) or return "No discount available for term '$discount_term'";
426 if ( $plan->discounted_total != $options{amount} ) {
427 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
435 my $bop_content = $self->_bop_content(\%options);
436 return $bop_content unless ref($bop_content);
438 my @invoicing_list = $self->invoicing_list_emailonly;
439 if ( $conf->exists('emailinvoiceautoalways')
440 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
441 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
442 push @invoicing_list, $self->all_emails;
445 my $email = ($conf->exists('business-onlinepayment-email-override'))
446 ? $conf->config('business-onlinepayment-email-override')
447 : $invoicing_list[0];
452 if ( $namespace eq 'Business::OnlinePayment' ) {
454 if ( $options{method} eq 'CC' ) {
456 $content{card_number} = $options{payinfo};
457 $paydate = exists($options{'paydate'})
458 ? $options{'paydate'}
460 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
461 $content{expiration} = "$2/$1";
463 my $paycvv = exists($options{'paycvv'})
466 $content{cvv2} = $paycvv
469 my $paystart_month = exists($options{'paystart_month'})
470 ? $options{'paystart_month'}
471 : $self->paystart_month;
473 my $paystart_year = exists($options{'paystart_year'})
474 ? $options{'paystart_year'}
475 : $self->paystart_year;
477 $content{card_start} = "$paystart_month/$paystart_year"
478 if $paystart_month && $paystart_year;
480 my $payissue = exists($options{'payissue'})
481 ? $options{'payissue'}
483 $content{issue_number} = $payissue if $payissue;
485 if ( $self->_bop_recurring_billing(
486 'payinfo' => $options{'payinfo'},
487 'trans_is_recur' => $trans_is_recur,
491 $content{recurring_billing} = 'YES';
492 $content{acct_code} = 'rebill'
493 if $conf->exists('credit_card-recurring_billing_acct_code');
496 } elsif ( $options{method} eq 'ECHECK' ){
498 ( $content{account_number}, $content{routing_code} ) =
499 split('@', $options{payinfo});
500 $content{bank_name} = $options{payname};
501 $content{bank_state} = exists($options{'paystate'})
502 ? $options{'paystate'}
503 : $self->getfield('paystate');
504 $content{account_type}=
505 (exists($options{'paytype'}) && $options{'paytype'})
506 ? uc($options{'paytype'})
507 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
509 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
510 $content{account_name} = $self->company;
512 $content{account_name} = $self->getfield('first'). ' '.
513 $self->getfield('last');
516 $content{customer_org} = $self->company ? 'B' : 'I';
517 $content{state_id} = exists($options{'stateid'})
518 ? $options{'stateid'}
519 : $self->getfield('stateid');
520 $content{state_id_state} = exists($options{'stateid_state'})
521 ? $options{'stateid_state'}
522 : $self->getfield('stateid_state');
523 $content{customer_ssn} = exists($options{'ss'})
527 } elsif ( $options{method} eq 'LEC' ) {
528 $content{phone} = $options{payinfo};
530 die "unknown method ". $options{method};
533 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
536 die "unknown namespace $namespace";
543 my $balance = exists( $options{'balance'} )
544 ? $options{'balance'}
547 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
548 $self->select_for_update; #mutex ... just until we get our pending record in
549 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
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});
601 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
603 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
604 return $cpp_new_err if $cpp_new_err;
606 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
608 warn Dumper($cust_pay_pending) if $DEBUG > 2;
610 my( $action1, $action2 ) =
611 split( /\s*\,\s*/, $payment_gateway->gateway_action );
613 my $transaction = new $namespace( $payment_gateway->gateway_module,
614 $self->_bop_options(\%options),
617 $transaction->content(
618 'type' => $options{method},
619 $self->_bop_auth(\%options),
620 'action' => $action1,
621 'description' => $options{'description'},
622 'amount' => $options{amount},
623 #'invoice_number' => $options{'invnum'},
624 'customer_id' => $self->custnum,
626 'reference' => $cust_pay_pending->paypendingnum, #for now
627 'callback_url' => $payment_gateway->gateway_callback_url,
628 'cancel_url' => $payment_gateway->gateway_cancel_url,
633 $cust_pay_pending->status('pending');
634 my $cpp_pending_err = $cust_pay_pending->replace;
635 return $cpp_pending_err if $cpp_pending_err;
637 warn Dumper($transaction) if $DEBUG > 2;
639 unless ( $BOP_TESTING ) {
640 $transaction->test_transaction(1)
641 if $conf->exists('business-onlinepayment-test_transaction');
642 $transaction->submit();
644 if ( $BOP_TESTING_SUCCESS ) {
645 $transaction->is_success(1);
646 $transaction->authorization('fake auth');
648 $transaction->is_success(0);
649 $transaction->error_message('fake failure');
653 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
655 $cust_pay_pending->status('thirdparty');
656 my $cpp_err = $cust_pay_pending->replace;
657 return { error => $cpp_err } if $cpp_err;
658 return { reference => $cust_pay_pending->paypendingnum,
659 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
661 } elsif ( $transaction->is_success() && $action2 ) {
663 $cust_pay_pending->status('authorized');
664 my $cpp_authorized_err = $cust_pay_pending->replace;
665 return $cpp_authorized_err if $cpp_authorized_err;
667 my $auth = $transaction->authorization;
668 my $ordernum = $transaction->can('order_number')
669 ? $transaction->order_number
673 new Business::OnlinePayment( $payment_gateway->gateway_module,
674 $self->_bop_options(\%options),
679 type => $options{method},
681 $self->_bop_auth(\%options),
682 order_number => $ordernum,
683 amount => $options{amount},
684 authorization => $auth,
685 description => $options{'description'},
688 foreach my $field (qw( authorization_source_code returned_ACI
689 transaction_identifier validation_code
690 transaction_sequence_num local_transaction_date
691 local_transaction_time AVS_result_code )) {
692 $capture{$field} = $transaction->$field() if $transaction->can($field);
695 $capture->content( %capture );
697 $capture->test_transaction(1)
698 if $conf->exists('business-onlinepayment-test_transaction');
701 unless ( $capture->is_success ) {
702 my $e = "Authorization successful but capture failed, custnum #".
703 $self->custnum. ': '. $capture->result_code.
704 ": ". $capture->error_message;
712 # remove paycvv after initial transaction
715 #false laziness w/misc/process/payment.cgi - check both to make sure working
717 if ( length($self->paycvv)
718 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
720 my $error = $self->remove_cvv;
722 warn "WARNING: error removing cvv: $error\n";
731 if ( $transaction->can('card_token') && $transaction->card_token ) {
733 $self->card_token($transaction->card_token);
735 if ( $options{'payinfo'} eq $self->payinfo ) {
736 $self->payinfo($transaction->card_token);
737 my $error = $self->replace;
739 warn "WARNING: error storing token: $error, but proceeding anyway\n";
749 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
761 if (ref($_[0]) eq 'HASH') {
764 my ( $method, $amount ) = ( shift, shift );
766 $options{method} = $method;
767 $options{amount} = $amount;
770 if ( $options{'fake_failure'} ) {
771 return "Error: No error; test failure requested with fake_failure";
774 my $cust_pay = new FS::cust_pay ( {
775 'custnum' => $self->custnum,
776 'invnum' => $options{'invnum'},
777 'paid' => $options{amount},
779 'payby' => $bop_method2payby{$options{method}},
780 #'payinfo' => $payinfo,
781 'payinfo' => '4111111111111111',
782 #'paydate' => $paydate,
783 'paydate' => '2012-05-01',
784 'processor' => 'FakeProcessor',
786 'order_number' => '32',
788 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
791 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
792 warn " $_ => $options{$_}\n" foreach keys %options;
795 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
798 $cust_pay->invnum(''); #try again with no specific invnum
799 my $error2 = $cust_pay->insert( $options{'manual'} ?
800 ( 'manual' => 1 ) : ()
803 # gah, even with transactions.
804 my $e = 'WARNING: Card/ACH debited but database not updated - '.
805 "error inserting (fake!) payment: $error2".
806 " (previously tried insert with invnum #$options{'invnum'}" .
813 if ( $options{'paynum_ref'} ) {
814 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
822 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
824 # Wraps up processing of a realtime credit card, ACH (electronic check) or
825 # phone bill transaction.
827 sub _realtime_bop_result {
828 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
830 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
833 warn "$me _realtime_bop_result: pending transaction ".
834 $cust_pay_pending->paypendingnum. "\n";
835 warn " $_ => $options{$_}\n" foreach keys %options;
838 my $payment_gateway = $options{payment_gateway}
839 or return "no payment gateway in arguments to _realtime_bop_result";
841 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
842 my $cpp_captured_err = $cust_pay_pending->replace;
843 return $cpp_captured_err if $cpp_captured_err;
845 if ( $transaction->is_success() ) {
847 my $order_number = $transaction->order_number
848 if $transaction->can('order_number');
850 my $cust_pay = new FS::cust_pay ( {
851 'custnum' => $self->custnum,
852 'invnum' => $options{'invnum'},
853 'paid' => $cust_pay_pending->paid,
855 'payby' => $cust_pay_pending->payby,
856 'payinfo' => $options{'payinfo'},
857 'paydate' => $cust_pay_pending->paydate,
858 'pkgnum' => $cust_pay_pending->pkgnum,
859 'discount_term' => $options{'discount_term'},
860 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
861 'processor' => $payment_gateway->gateway_module,
862 'auth' => $transaction->authorization,
863 'order_number' => $order_number || '',
866 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
867 $cust_pay->payunique( $options{payunique} )
868 if defined($options{payunique}) && length($options{payunique});
870 my $oldAutoCommit = $FS::UID::AutoCommit;
871 local $FS::UID::AutoCommit = 0;
874 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
876 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
879 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
880 $cust_pay->invnum(''); #try again with no specific invnum
881 $cust_pay->paynum('');
882 my $error2 = $cust_pay->insert( $options{'manual'} ?
883 ( 'manual' => 1 ) : ()
886 # gah. but at least we have a record of the state we had to abort in
887 # from cust_pay_pending now.
888 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
889 my $e = "WARNING: $options{method} captured but payment not recorded -".
890 " error inserting payment (". $payment_gateway->gateway_module.
892 " (previously tried insert with invnum #$options{'invnum'}" .
893 ": $error ) - pending payment saved as paypendingnum ".
894 $cust_pay_pending->paypendingnum. "\n";
900 my $jobnum = $cust_pay_pending->jobnum;
902 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
904 unless ( $placeholder ) {
905 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
906 my $e = "WARNING: $options{method} captured but job $jobnum not ".
907 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
912 $error = $placeholder->delete;
915 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
916 my $e = "WARNING: $options{method} captured but could not delete ".
917 "job $jobnum for paypendingnum ".
918 $cust_pay_pending->paypendingnum. ": $error\n";
925 if ( $options{'paynum_ref'} ) {
926 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
929 $cust_pay_pending->status('done');
930 $cust_pay_pending->statustext('captured');
931 $cust_pay_pending->paynum($cust_pay->paynum);
932 my $cpp_done_err = $cust_pay_pending->replace;
934 if ( $cpp_done_err ) {
936 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
937 my $e = "WARNING: $options{method} captured but payment not recorded - ".
938 "error updating status for paypendingnum ".
939 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
945 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
947 if ( $options{'apply'} ) {
948 my $apply_error = $self->apply_payments_and_credits;
949 if ( $apply_error ) {
950 warn "WARNING: error applying payment: $apply_error\n";
951 #but we still should return no error cause the payment otherwise went
956 # have a CC surcharge portion --> one-time charge
957 if ( $options{'cc_surcharge'} > 0 ) {
958 # XXX: this whole block needs to be in a transaction?
961 $invnum = $options{'invnum'} if $options{'invnum'};
962 unless ( $invnum ) { # probably from a payment screen
963 # do we have any open invoices? pick earliest
964 # uses the fact that cust_main->cust_bill sorts by date ascending
965 my @open = $self->open_cust_bill;
966 $invnum = $open[0]->invnum if scalar(@open);
969 unless ( $invnum ) { # still nothing? pick last closed invoice
970 # again uses fact that cust_main->cust_bill sorts by date ascending
971 my @closed = $self->cust_bill;
972 $invnum = $closed[$#closed]->invnum if scalar(@closed);
976 # XXX: unlikely case - pre-paying before any invoices generated
977 # what it should do is create a new invoice and pick it
978 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
983 my $charge_error = $self->charge({
984 'amount' => $options{'cc_surcharge'},
985 'pkg' => 'Credit Card Surcharge',
987 'cust_pkg_ref' => \$cust_pkg,
990 warn 'Unable to add CC surcharge cust_pkg';
994 $cust_pkg->setup(time);
995 my $cp_error = $cust_pkg->replace;
997 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1001 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1002 unless ( $cust_bill ) {
1003 warn "race condition + invoice deletion just happened";
1008 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1010 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1014 return ''; #no error
1020 my $perror = $transaction->error_message;
1021 #$payment_gateway->gateway_module. " error: ".
1022 # removed for conciseness
1024 my $jobnum = $cust_pay_pending->jobnum;
1026 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1028 if ( $placeholder ) {
1029 my $error = $placeholder->depended_delete;
1030 $error ||= $placeholder->delete;
1031 warn "error removing provisioning jobs after declined paypendingnum ".
1032 $cust_pay_pending->paypendingnum. ": $error\n";
1034 my $e = "error finding job $jobnum for declined paypendingnum ".
1035 $cust_pay_pending->paypendingnum. "\n";
1041 unless ( $transaction->error_message ) {
1044 if ( $transaction->can('response_page') ) {
1046 'page' => ( $transaction->can('response_page')
1047 ? $transaction->response_page
1050 'code' => ( $transaction->can('response_code')
1051 ? $transaction->response_code
1054 'headers' => ( $transaction->can('response_headers')
1055 ? $transaction->response_headers
1061 "No additional debugging information available for ".
1062 $payment_gateway->gateway_module;
1065 $perror .= "No error_message returned from ".
1066 $payment_gateway->gateway_module. " -- ".
1067 ( ref($t_response) ? Dumper($t_response) : $t_response );
1071 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1072 && $conf->exists('emaildecline', $self->agentnum)
1073 && grep { $_ ne 'POST' } $self->invoicing_list
1074 && ! grep { $transaction->error_message =~ /$_/ }
1075 $conf->config('emaildecline-exclude', $self->agentnum)
1078 # Send a decline alert to the customer.
1079 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1082 # include the raw error message in the transaction state
1083 $cust_pay_pending->setfield('error', $transaction->error_message);
1084 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1085 $error = $msg_template->send( 'cust_main' => $self,
1086 'object' => $cust_pay_pending );
1090 my @templ = $conf->config('declinetemplate');
1091 my $template = new Text::Template (
1093 SOURCE => [ map "$_\n", @templ ],
1094 ) or return "($perror) can't create template: $Text::Template::ERROR";
1095 $template->compile()
1096 or return "($perror) can't compile template: $Text::Template::ERROR";
1100 scalar( $conf->config('company_name', $self->agentnum ) ),
1101 'company_address' =>
1102 join("\n", $conf->config('company_address', $self->agentnum ) ),
1103 'error' => $transaction->error_message,
1106 my $error = send_email(
1107 'from' => $conf->config('invoice_from', $self->agentnum ),
1108 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1109 'subject' => 'Your payment could not be processed',
1110 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1114 $perror .= " (also received error sending decline notification: $error)"
1119 $cust_pay_pending->status('done');
1120 $cust_pay_pending->statustext($perror);
1121 #'declined:': no, that's failure_status
1122 if ( $transaction->can('failure_status') ) {
1123 $cust_pay_pending->failure_status( $transaction->failure_status );
1125 my $cpp_done_err = $cust_pay_pending->replace;
1126 if ( $cpp_done_err ) {
1127 my $e = "WARNING: $options{method} declined but pending payment not ".
1128 "resolved - error updating status for paypendingnum ".
1129 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1131 $perror = "$e ($perror)";
1139 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1141 Verifies successful third party processing of a realtime credit card,
1142 ACH (electronic check) or phone bill transaction via a
1143 Business::OnlineThirdPartyPayment realtime gateway. See
1144 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1146 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1148 The additional options I<payname>, I<city>, I<state>,
1149 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1150 if set, will override the value from the customer record.
1152 I<description> is a free-text field passed to the gateway. It defaults to
1153 "Internet services".
1155 If an I<invnum> is specified, this payment (if successful) is applied to the
1156 specified invoice. If you don't specify an I<invnum> you might want to
1157 call the B<apply_payments> method.
1159 I<quiet> can be set true to surpress email decline notices.
1161 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1162 resulting paynum, if any.
1164 I<payunique> is a unique identifier for this payment.
1166 Returns a hashref containing elements bill_error (which will be undefined
1167 upon success) and session_id of any associated session.
1171 sub realtime_botpp_capture {
1172 my( $self, $cust_pay_pending, %options ) = @_;
1174 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1177 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1178 warn " $_ => $options{$_}\n" foreach keys %options;
1181 eval "use Business::OnlineThirdPartyPayment";
1185 # select the gateway
1188 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1190 my $payment_gateway;
1191 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1192 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1193 { gatewaynum => $gatewaynum }
1195 : $self->agent->payment_gateway( 'method' => $method,
1196 # 'invnum' => $cust_pay_pending->invnum,
1197 # 'payinfo' => $cust_pay_pending->payinfo,
1200 $options{payment_gateway} = $payment_gateway; # for the helper subs
1206 my @invoicing_list = $self->invoicing_list_emailonly;
1207 if ( $conf->exists('emailinvoiceautoalways')
1208 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1209 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1210 push @invoicing_list, $self->all_emails;
1213 my $email = ($conf->exists('business-onlinepayment-email-override'))
1214 ? $conf->config('business-onlinepayment-email-override')
1215 : $invoicing_list[0];
1219 $content{email_customer} =
1220 ( $conf->exists('business-onlinepayment-email_customer')
1221 || $conf->exists('business-onlinepayment-email-override') );
1224 # run transaction(s)
1228 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1229 $self->_bop_options(\%options),
1232 $transaction->reference({ %options });
1234 $transaction->content(
1236 $self->_bop_auth(\%options),
1237 'action' => 'Post Authorization',
1238 'description' => $options{'description'},
1239 'amount' => $cust_pay_pending->paid,
1240 #'invoice_number' => $options{'invnum'},
1241 'customer_id' => $self->custnum,
1242 'reference' => $cust_pay_pending->paypendingnum,
1244 'phone' => $self->daytime || $self->night,
1246 # plus whatever is required for bogus capture avoidance
1249 $transaction->submit();
1252 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1254 if ( $options{'apply'} ) {
1255 my $apply_error = $self->apply_payments_and_credits;
1256 if ( $apply_error ) {
1257 warn "WARNING: error applying payment: $apply_error\n";
1262 bill_error => $error,
1263 session_id => $cust_pay_pending->session_id,
1268 =item default_payment_gateway
1270 DEPRECATED -- use agent->payment_gateway
1274 sub default_payment_gateway {
1275 my( $self, $method ) = @_;
1277 die "Real-time processing not enabled\n"
1278 unless $conf->exists('business-onlinepayment');
1280 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1283 my $bop_config = 'business-onlinepayment';
1284 $bop_config .= '-ach'
1285 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1286 my ( $processor, $login, $password, $action, @bop_options ) =
1287 $conf->config($bop_config);
1288 $action ||= 'normal authorization';
1289 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1290 die "No real-time processor is enabled - ".
1291 "did you set the business-onlinepayment configuration value?\n"
1294 ( $processor, $login, $password, $action, @bop_options )
1297 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1299 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1300 via a Business::OnlinePayment realtime gateway. See
1301 L<http://420.am/business-onlinepayment> for supported gateways.
1303 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1305 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1307 Most gateways require a reference to an original payment transaction to refund,
1308 so you probably need to specify a I<paynum>.
1310 I<amount> defaults to the original amount of the payment if not specified.
1312 I<reason> specifies a reason for the refund.
1314 I<paydate> specifies the expiration date for a credit card overriding the
1315 value from the customer record or the payment record. Specified as yyyy-mm-dd
1317 Implementation note: If I<amount> is unspecified or equal to the amount of the
1318 orignal payment, first an attempt is made to "void" the transaction via
1319 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1320 the normal attempt is made to "refund" ("credit") the transaction via the
1321 gateway is attempted. No attempt to "void" the transaction is made if the
1322 gateway has introspection data and doesn't support void.
1324 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1325 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1326 #if set, will override the value from the customer record.
1328 #If an I<invnum> is specified, this payment (if successful) is applied to the
1329 #specified invoice. If you don't specify an I<invnum> you might want to
1330 #call the B<apply_payments> method.
1334 #some false laziness w/realtime_bop, not enough to make it worth merging
1335 #but some useful small subs should be pulled out
1336 sub realtime_refund_bop {
1339 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1342 if (ref($_[0]) eq 'HASH') {
1343 %options = %{$_[0]};
1347 $options{method} = $method;
1351 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1352 warn " $_ => $options{$_}\n" foreach keys %options;
1356 # look up the original payment and optionally a gateway for that payment
1360 my $amount = $options{'amount'};
1362 my( $processor, $login, $password, @bop_options, $namespace ) ;
1363 my( $auth, $order_number ) = ( '', '', '' );
1364 my $gatewaynum = '';
1366 if ( $options{'paynum'} ) {
1368 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1369 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1370 or return "Unknown paynum $options{'paynum'}";
1371 $amount ||= $cust_pay->paid;
1373 if ( $cust_pay->get('processor') ) {
1374 ($gatewaynum, $processor, $auth, $order_number) =
1376 $cust_pay->gatewaynum,
1377 $cust_pay->processor,
1379 $cust_pay->order_number,
1382 # this payment wasn't upgraded, which probably means this won't work,
1384 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1385 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1386 $cust_pay->paybatch;
1387 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1390 if ( $gatewaynum ) { #gateway for the payment to be refunded
1392 my $payment_gateway =
1393 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1394 die "payment gateway $gatewaynum not found"
1395 unless $payment_gateway;
1397 $processor = $payment_gateway->gateway_module;
1398 $login = $payment_gateway->gateway_username;
1399 $password = $payment_gateway->gateway_password;
1400 $namespace = $payment_gateway->gateway_namespace;
1401 @bop_options = $payment_gateway->options;
1403 } else { #try the default gateway
1406 my $payment_gateway =
1407 $self->agent->payment_gateway('method' => $options{method});
1409 ( $conf_processor, $login, $password, $namespace ) =
1410 map { my $method = "gateway_$_"; $payment_gateway->$method }
1411 qw( module username password namespace );
1413 @bop_options = $payment_gateway->gatewaynum
1414 ? $payment_gateway->options
1415 : @{ $payment_gateway->get('options') };
1417 return "processor of payment $options{'paynum'} $processor does not".
1418 " match default processor $conf_processor"
1419 unless $processor eq $conf_processor;
1424 } else { # didn't specify a paynum, so look for agent gateway overrides
1425 # like a normal transaction
1427 my $payment_gateway =
1428 $self->agent->payment_gateway( 'method' => $options{method},
1429 #'payinfo' => $payinfo,
1431 my( $processor, $login, $password, $namespace ) =
1432 map { my $method = "gateway_$_"; $payment_gateway->$method }
1433 qw( module username password namespace );
1435 my @bop_options = $payment_gateway->gatewaynum
1436 ? $payment_gateway->options
1437 : @{ $payment_gateway->get('options') };
1440 return "neither amount nor paynum specified" unless $amount;
1442 eval "use $namespace";
1446 'type' => $options{method},
1448 'password' => $password,
1449 'order_number' => $order_number,
1450 'amount' => $amount,
1452 $content{authorization} = $auth
1453 if length($auth); #echeck/ACH transactions have an order # but no auth
1454 #(at least with authorize.net)
1456 my $currency = $conf->exists('business-onlinepayment-currency')
1457 && $conf->config('business-onlinepayment-currency');
1458 $content{currency} = $currency if $currency;
1460 my $disable_void_after;
1461 if ($conf->exists('disable_void_after')
1462 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1463 $disable_void_after = $1;
1466 #first try void if applicable
1467 my $void = new Business::OnlinePayment( $processor, @bop_options );
1470 if ($void->can('info')) {
1472 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1473 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1474 my %supported_actions = $void->info('supported_actions');
1476 if ( %supported_actions && $paytype
1477 && defined($supported_actions{$paytype})
1478 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1481 if ( $cust_pay && $cust_pay->paid == $amount
1483 ( not defined($disable_void_after) )
1484 || ( time < ($cust_pay->_date + $disable_void_after ) )
1488 warn " attempting void\n" if $DEBUG > 1;
1489 if ( $void->can('info') ) {
1490 if ( $cust_pay->payby eq 'CARD'
1491 && $void->info('CC_void_requires_card') )
1493 $content{'card_number'} = $cust_pay->payinfo;
1494 } elsif ( $cust_pay->payby eq 'CHEK'
1495 && $void->info('ECHECK_void_requires_account') )
1497 ( $content{'account_number'}, $content{'routing_code'} ) =
1498 split('@', $cust_pay->payinfo);
1499 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1502 $void->content( 'action' => 'void', %content );
1503 $void->test_transaction(1)
1504 if $conf->exists('business-onlinepayment-test_transaction');
1506 if ( $void->is_success ) {
1507 my $error = $cust_pay->void($options{'reason'});
1509 # gah, even with transactions.
1510 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1511 "error voiding payment: $error";
1515 warn " void successful\n" if $DEBUG > 1;
1520 warn " void unsuccessful, trying refund\n"
1524 my $address = $self->address1;
1525 $address .= ", ". $self->address2 if $self->address2;
1527 my($payname, $payfirst, $paylast);
1528 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1529 $payname = $self->payname;
1530 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1531 or return "Illegal payname $payname";
1532 ($payfirst, $paylast) = ($1, $2);
1534 $payfirst = $self->getfield('first');
1535 $paylast = $self->getfield('last');
1536 $payname = "$payfirst $paylast";
1539 my @invoicing_list = $self->invoicing_list_emailonly;
1540 if ( $conf->exists('emailinvoiceautoalways')
1541 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1542 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1543 push @invoicing_list, $self->all_emails;
1546 my $email = ($conf->exists('business-onlinepayment-email-override'))
1547 ? $conf->config('business-onlinepayment-email-override')
1548 : $invoicing_list[0];
1550 my $payip = exists($options{'payip'})
1553 $content{customer_ip} = $payip
1557 if ( $options{method} eq 'CC' ) {
1560 $content{card_number} = $payinfo = $cust_pay->payinfo;
1561 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1562 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1563 ($content{expiration} = "$2/$1"); # where available
1565 $content{card_number} = $payinfo = $self->payinfo;
1566 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1567 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1568 $content{expiration} = "$2/$1";
1571 } elsif ( $options{method} eq 'ECHECK' ) {
1574 $payinfo = $cust_pay->payinfo;
1576 $payinfo = $self->payinfo;
1578 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1579 $content{bank_name} = $self->payname;
1580 $content{account_type} = 'CHECKING';
1581 $content{account_name} = $payname;
1582 $content{customer_org} = $self->company ? 'B' : 'I';
1583 $content{customer_ssn} = $self->ss;
1584 } elsif ( $options{method} eq 'LEC' ) {
1585 $content{phone} = $payinfo = $self->payinfo;
1589 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1590 my %sub_content = $refund->content(
1591 'action' => 'credit',
1592 'customer_id' => $self->custnum,
1593 'last_name' => $paylast,
1594 'first_name' => $payfirst,
1596 'address' => $address,
1597 'city' => $self->city,
1598 'state' => $self->state,
1599 'zip' => $self->zip,
1600 'country' => $self->country,
1602 'phone' => $self->daytime || $self->night,
1605 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1607 $refund->test_transaction(1)
1608 if $conf->exists('business-onlinepayment-test_transaction');
1611 return "$processor error: ". $refund->error_message
1612 unless $refund->is_success();
1614 $order_number = $refund->order_number if $refund->can('order_number');
1616 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1617 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1618 last unless @cust_bill_pay;
1619 my $cust_bill_pay = pop @cust_bill_pay;
1620 my $error = $cust_bill_pay->delete;
1624 my $cust_refund = new FS::cust_refund ( {
1625 'custnum' => $self->custnum,
1626 'paynum' => $options{'paynum'},
1627 'refund' => $amount,
1629 'payby' => $bop_method2payby{$options{method}},
1630 'payinfo' => $payinfo,
1631 'reason' => $options{'reason'} || 'card or ACH refund',
1632 'gatewaynum' => $gatewaynum, # may be null
1633 'processor' => $processor,
1634 'auth' => $refund->authorization,
1635 'order_number' => $order_number,
1637 my $error = $cust_refund->insert;
1639 $cust_refund->paynum(''); #try again with no specific paynum
1640 my $error2 = $cust_refund->insert;
1642 # gah, even with transactions.
1643 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1644 "error inserting refund ($processor): $error2".
1645 " (previously tried insert with paynum #$options{'paynum'}" .
1664 L<FS::cust_main>, L<FS::cust_main::Billing>