1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
8 use Business::CreditCard 0.28;
10 use FS::Record qw( qsearch qsearchs );
13 use FS::cust_pay_pending;
14 use FS::cust_bill_pay;
18 $realtime_bop_decline_quiet = 0;
20 # 1 is mostly method/subroutine entry and options
21 # 2 traces progress of some operations
22 # 3 is even more information including possibly sensitive data
24 $me = '[FS::cust_main::Billing_Realtime]';
27 our $BOP_TESTING_SUCCESS = 1;
29 install_callback FS::UID sub {
31 #yes, need it for stuff below (prolly should be cached)
36 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
42 These methods are available on FS::cust_main objects.
48 =item realtime_cust_payby
52 sub realtime_cust_payby {
53 my( $self, %options ) = @_;
55 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
57 $options{amount} = $self->balance unless exists( $options{amount} );
59 my @cust_payby = $self->cust_payby('CARD','CHEK');
62 foreach my $cust_payby (@cust_payby) {
63 $error = $cust_payby->realtime_bop( %options, );
67 #XXX what about the earlier errors?
73 =item realtime_collect [ OPTION => VALUE ... ]
75 Attempt to collect the customer's current balance with a realtime credit
76 card, electronic check, or phone bill transaction (see realtime_bop() below).
78 Returns the result of realtime_bop(): nothing, an error message, or a
79 hashref of state information for a third-party transaction.
81 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
83 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
84 then it is deduced from the customer record.
86 If no I<amount> is specified, then the customer balance is used.
88 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
89 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
90 if set, will override the value from the customer record.
92 I<description> is a free-text field passed to the gateway. It defaults to
93 the value defined by the business-onlinepayment-description configuration
94 option, or "Internet services" if that is unset.
96 If an I<invnum> is specified, this payment (if successful) is applied to the
99 I<apply> will automatically apply a resulting payment.
101 I<quiet> can be set true to suppress email decline notices.
103 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
104 resulting paynum, if any.
106 I<payunique> is a unique identifier for this payment.
108 I<session_id> is a session identifier associated with this payment.
110 I<depend_jobnum> allows payment capture to unlock export jobs
114 sub realtime_collect {
115 my( $self, %options ) = @_;
117 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
120 warn "$me realtime_collect:\n";
121 warn " $_ => $options{$_}\n" foreach keys %options;
124 $options{amount} = $self->balance unless exists( $options{amount} );
125 $options{method} = FS::payby->payby2bop($self->payby)
126 unless exists( $options{method} );
128 return $self->realtime_bop({%options});
132 =item realtime_bop { [ ARG => VALUE ... ] }
134 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
135 via a Business::OnlinePayment realtime gateway. See
136 L<http://420.am/business-onlinepayment> for supported gateways.
138 Required arguments in the hashref are I<method>, and I<amount>
140 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
142 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
144 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
145 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
146 if set, will override the value from the customer record.
148 I<description> is a free-text field passed to the gateway. It defaults to
149 the value defined by the business-onlinepayment-description configuration
150 option, or "Internet services" if that is unset.
152 If an I<invnum> is specified, this payment (if successful) is applied to the
153 specified invoice. If the customer has exactly one open invoice, that
154 invoice number will be assumed. If you don't specify an I<invnum> you might
155 want to call the B<apply_payments> method or set the I<apply> option.
157 I<no_invnum> can be set to true to prevent that default invnum from being set.
159 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
161 I<no_auto_apply> can be set to true to set that flag on the resulting payment
162 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
163 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
165 I<quiet> can be set true to surpress email decline notices.
167 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
168 resulting paynum, if any.
170 I<payunique> is a unique identifier for this payment.
172 I<session_id> is a session identifier associated with this payment.
174 I<depend_jobnum> allows payment capture to unlock export jobs
176 I<discount_term> attempts to take a discount by prepaying for discount_term.
177 The payment will fail if I<amount> is incorrect for this discount term.
179 A direct (Business::OnlinePayment) transaction will return nothing on success,
180 or an error message on failure.
182 A third-party transaction will return a hashref containing:
184 - popup_url: the URL to which a browser should be redirected to complete
186 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
187 - reference: a reference ID for the transaction, to show the customer.
189 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
193 # some helper routines
195 # _bop_recurring_billing: Checks whether this payment should have the
196 # recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
197 # vSecure, etc.). This works in two different modes:
198 # - actual_oncard (default): treat the payment as recurring if the customer
199 # has made a payment using this card before.
200 # - transaction_is_recur: treat the payment as recurring if the invoice
201 # being paid has any recurring package charges.
203 sub _bop_recurring_billing {
204 my( $self, %opt ) = @_;
206 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
208 if ( defined($method) && $method eq 'transaction_is_recur' ) {
210 return 1 if $opt{'trans_is_recur'};
214 # return 1 if the payinfo has been used for another payment
215 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
223 sub _payment_gateway {
224 my ($self, $options) = @_;
226 if ( $options->{'selfservice'} ) {
227 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
229 return $options->{payment_gateway} ||=
230 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
234 if ( $options->{'fake_gatewaynum'} ) {
235 $options->{payment_gateway} =
236 qsearchs('payment_gateway',
237 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
241 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
242 unless exists($options->{payment_gateway});
244 $options->{payment_gateway};
248 my ($self, $options) = @_;
251 'login' => $options->{payment_gateway}->gateway_username,
252 'password' => $options->{payment_gateway}->gateway_password,
257 my ($self, $options) = @_;
259 $options->{payment_gateway}->gatewaynum
260 ? $options->{payment_gateway}->options
261 : @{ $options->{payment_gateway}->get('options') };
266 my ($self, $options) = @_;
268 unless ( $options->{'description'} ) {
269 if ( $conf->exists('business-onlinepayment-description') ) {
270 my $dtempl = $conf->config('business-onlinepayment-description');
272 my $agent = $self->agent->agent;
274 $options->{'description'} = eval qq("$dtempl");
276 $options->{'description'} = 'Internet services';
280 unless ( exists( $options->{'payinfo'} ) ) {
281 $options->{'payinfo'} = $self->payinfo;
282 $options->{'paymask'} = $self->paymask;
285 # Default invoice number if the customer has exactly one open invoice.
286 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
287 $options->{'invnum'} = '';
288 my @open = $self->open_cust_bill;
289 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
292 $options->{payname} = $self->payname unless exists( $options->{payname} );
296 my ($self, $options) = @_;
299 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
300 $content{customer_ip} = $payip if length($payip);
302 $content{invoice_number} = $options->{'invnum'}
303 if exists($options->{'invnum'}) && length($options->{'invnum'});
305 $content{email_customer} =
306 ( $conf->exists('business-onlinepayment-email_customer')
307 || $conf->exists('business-onlinepayment-email-override') );
309 my ($payname, $payfirst, $paylast);
310 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
311 ($payname = $options->{payname}) =~
312 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
313 or return "Illegal payname $payname";
314 ($payfirst, $paylast) = ($1, $2);
316 $payfirst = $self->getfield('first');
317 $paylast = $self->getfield('last');
318 $payname = "$payfirst $paylast";
321 $content{last_name} = $paylast;
322 $content{first_name} = $payfirst;
324 $content{name} = $payname;
326 $content{address} = exists($options->{'address1'})
327 ? $options->{'address1'}
329 my $address2 = exists($options->{'address2'})
330 ? $options->{'address2'}
332 $content{address} .= ", ". $address2 if length($address2);
334 $content{city} = exists($options->{city})
337 $content{state} = exists($options->{state})
340 $content{zip} = exists($options->{zip})
343 $content{country} = exists($options->{country})
344 ? $options->{country}
347 $content{phone} = $self->daytime || $self->night;
349 my $currency = $conf->exists('business-onlinepayment-currency')
350 && $conf->config('business-onlinepayment-currency');
351 $content{currency} = $currency if $currency;
356 my %bop_method2payby = (
366 confess "Can't call realtime_bop within another transaction ".
367 '($FS::UID::AutoCommit is false)'
368 unless $FS::UID::AutoCommit;
370 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
373 if (ref($_[0]) eq 'HASH') {
376 my ( $method, $amount ) = ( shift, shift );
378 $options{method} = $method;
379 $options{amount} = $amount;
384 # optional credit card surcharge
387 my $cc_surcharge = 0;
388 my $cc_surcharge_pct = 0;
389 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
390 if $conf->config('credit-card-surcharge-percentage')
391 && $options{method} eq 'CC';
393 # always add cc surcharge if called from event
394 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
395 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
396 $options{'amount'} += $cc_surcharge;
397 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
399 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
400 # payment screen), so consider the given
401 # amount as post-surcharge
402 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
405 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
406 $options{'cc_surcharge'} = $cc_surcharge;
410 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
411 warn " cc_surcharge = $cc_surcharge\n";
414 warn " $_ => $options{$_}\n" foreach keys %options;
417 return $self->fake_bop(\%options) if $options{'fake'};
419 $self->_bop_defaults(\%options);
422 # set trans_is_recur based on invnum if there is one
425 my $trans_is_recur = 0;
426 if ( $options{'invnum'} ) {
428 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
429 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
435 $cust_bill->cust_bill_pkg;
438 if grep { $_->freq ne '0' } @part_pkg;
446 my $payment_gateway = $self->_payment_gateway( \%options );
447 my $namespace = $payment_gateway->gateway_namespace;
449 eval "use $namespace";
453 # check for banned credit card/ACH
456 my $ban = FS::banned_pay->ban_search(
457 'payby' => $bop_method2payby{$options{method}},
458 'payinfo' => $options{payinfo},
460 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
463 # check for term discount validity
466 my $discount_term = $options{discount_term};
467 if ( $discount_term ) {
468 my $bill = ($self->cust_bill)[-1]
469 or return "Can't apply a term discount to an unbilled customer";
470 my $plan = FS::discount_plan->new(
472 months => $discount_term
473 ) or return "No discount available for term '$discount_term'";
475 if ( $plan->discounted_total != $options{amount} ) {
476 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
484 my $bop_content = $self->_bop_content(\%options);
485 return $bop_content unless ref($bop_content);
487 my @invoicing_list = $self->invoicing_list_emailonly;
488 if ( $conf->exists('emailinvoiceautoalways')
489 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
490 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
491 push @invoicing_list, $self->all_emails;
494 my $email = ($conf->exists('business-onlinepayment-email-override'))
495 ? $conf->config('business-onlinepayment-email-override')
496 : $invoicing_list[0];
501 if ( $namespace eq 'Business::OnlinePayment' ) {
503 if ( $options{method} eq 'CC' ) {
505 $content{card_number} = $options{payinfo};
506 $paydate = exists($options{'paydate'})
507 ? $options{'paydate'}
509 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
510 $content{expiration} = "$2/$1";
512 my $paycvv = exists($options{'paycvv'})
515 $content{cvv2} = $paycvv
518 my $paystart_month = exists($options{'paystart_month'})
519 ? $options{'paystart_month'}
520 : $self->paystart_month;
522 my $paystart_year = exists($options{'paystart_year'})
523 ? $options{'paystart_year'}
524 : $self->paystart_year;
526 $content{card_start} = "$paystart_month/$paystart_year"
527 if $paystart_month && $paystart_year;
529 my $payissue = exists($options{'payissue'})
530 ? $options{'payissue'}
532 $content{issue_number} = $payissue if $payissue;
534 if ( $self->_bop_recurring_billing(
535 'payinfo' => $options{'payinfo'},
536 'trans_is_recur' => $trans_is_recur,
540 $content{recurring_billing} = 'YES';
541 $content{acct_code} = 'rebill'
542 if $conf->exists('credit_card-recurring_billing_acct_code');
545 } elsif ( $options{method} eq 'ECHECK' ){
547 ( $content{account_number}, $content{routing_code} ) =
548 split('@', $options{payinfo});
549 $content{bank_name} = $options{payname};
550 $content{bank_state} = exists($options{'paystate'})
551 ? $options{'paystate'}
552 : $self->getfield('paystate');
553 $content{account_type}=
554 (exists($options{'paytype'}) && $options{'paytype'})
555 ? uc($options{'paytype'})
556 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
558 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
559 $content{account_name} = $self->company;
561 $content{account_name} = $self->getfield('first'). ' '.
562 $self->getfield('last');
565 $content{customer_org} = $self->company ? 'B' : 'I';
566 $content{state_id} = exists($options{'stateid'})
567 ? $options{'stateid'}
568 : $self->getfield('stateid');
569 $content{state_id_state} = exists($options{'stateid_state'})
570 ? $options{'stateid_state'}
571 : $self->getfield('stateid_state');
572 $content{customer_ssn} = exists($options{'ss'})
576 } elsif ( $options{method} eq 'LEC' ) {
577 $content{phone} = $options{payinfo};
579 die "unknown method ". $options{method};
582 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
585 die "unknown namespace $namespace";
592 my $balance = exists( $options{'balance'} )
593 ? $options{'balance'}
596 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
597 $self->select_for_update; #mutex ... just until we get our pending record in
598 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
600 #the checks here are intended to catch concurrent payments
601 #double-form-submission prevention is taken care of in cust_pay_pending::check
604 return "The customer's balance has changed; $options{method} transaction aborted."
605 if $self->balance < $balance;
607 #also check and make sure there aren't *other* pending payments for this cust
609 my @pending = qsearch('cust_pay_pending', {
610 'custnum' => $self->custnum,
611 'status' => { op=>'!=', value=>'done' }
614 #for third-party payments only, remove pending payments if they're in the
615 #'thirdparty' (waiting for customer action) state.
616 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
617 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
618 my $error = $_->delete;
619 warn "error deleting unfinished third-party payment ".
620 $_->paypendingnum . ": $error\n"
623 @pending = grep { $_->status ne 'thirdparty' } @pending;
626 return "A payment is already being processed for this customer (".
627 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
628 "); $options{method} transaction aborted."
631 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
633 my $cust_pay_pending = new FS::cust_pay_pending {
634 'custnum' => $self->custnum,
635 'paid' => $options{amount},
637 'payby' => $bop_method2payby{$options{method}},
638 'payinfo' => $options{payinfo},
639 'paymask' => $options{paymask},
640 'paydate' => $paydate,
641 'recurring_billing' => $content{recurring_billing},
642 'pkgnum' => $options{'pkgnum'},
644 'gatewaynum' => $payment_gateway->gatewaynum || '',
645 'session_id' => $options{session_id} || '',
646 'jobnum' => $options{depend_jobnum} || '',
648 $cust_pay_pending->payunique( $options{payunique} )
649 if defined($options{payunique}) && length($options{payunique});
651 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
653 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
654 return $cpp_new_err if $cpp_new_err;
656 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
658 warn Dumper($cust_pay_pending) if $DEBUG > 2;
660 my( $action1, $action2 ) =
661 split( /\s*\,\s*/, $payment_gateway->gateway_action );
663 my $transaction = new $namespace( $payment_gateway->gateway_module,
664 $self->_bop_options(\%options),
667 $transaction->content(
668 'type' => $options{method},
669 $self->_bop_auth(\%options),
670 'action' => $action1,
671 'description' => $options{'description'},
672 'amount' => $options{amount},
673 #'invoice_number' => $options{'invnum'},
674 'customer_id' => $self->custnum,
676 'reference' => $cust_pay_pending->paypendingnum, #for now
677 'callback_url' => $payment_gateway->gateway_callback_url,
678 'cancel_url' => $payment_gateway->gateway_cancel_url,
683 $cust_pay_pending->status('pending');
684 my $cpp_pending_err = $cust_pay_pending->replace;
685 return $cpp_pending_err if $cpp_pending_err;
687 warn Dumper($transaction) if $DEBUG > 2;
689 unless ( $BOP_TESTING ) {
690 $transaction->test_transaction(1)
691 if $conf->exists('business-onlinepayment-test_transaction');
692 $transaction->submit();
694 if ( $BOP_TESTING_SUCCESS ) {
695 $transaction->is_success(1);
696 $transaction->authorization('fake auth');
698 $transaction->is_success(0);
699 $transaction->error_message('fake failure');
703 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
705 $cust_pay_pending->status('thirdparty');
706 my $cpp_err = $cust_pay_pending->replace;
707 return { error => $cpp_err } if $cpp_err;
708 return { reference => $cust_pay_pending->paypendingnum,
709 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
711 } elsif ( $transaction->is_success() && $action2 ) {
713 $cust_pay_pending->status('authorized');
714 my $cpp_authorized_err = $cust_pay_pending->replace;
715 return $cpp_authorized_err if $cpp_authorized_err;
717 my $auth = $transaction->authorization;
718 my $ordernum = $transaction->can('order_number')
719 ? $transaction->order_number
723 new Business::OnlinePayment( $payment_gateway->gateway_module,
724 $self->_bop_options(\%options),
729 type => $options{method},
731 $self->_bop_auth(\%options),
732 order_number => $ordernum,
733 amount => $options{amount},
734 authorization => $auth,
735 description => $options{'description'},
738 foreach my $field (qw( authorization_source_code returned_ACI
739 transaction_identifier validation_code
740 transaction_sequence_num local_transaction_date
741 local_transaction_time AVS_result_code )) {
742 $capture{$field} = $transaction->$field() if $transaction->can($field);
745 $capture->content( %capture );
747 $capture->test_transaction(1)
748 if $conf->exists('business-onlinepayment-test_transaction');
751 unless ( $capture->is_success ) {
752 my $e = "Authorization successful but capture failed, custnum #".
753 $self->custnum. ': '. $capture->result_code.
754 ": ". $capture->error_message;
762 # remove paycvv after initial transaction
765 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
766 if ( length($self->paycvv)
767 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
769 my $error = $self->remove_cvv;
771 warn "WARNING: error removing cvv: $error\n";
780 if ( $transaction->can('card_token') && $transaction->card_token ) {
782 if ( $options{'payinfo'} eq $self->payinfo ) {
783 $self->payinfo($transaction->card_token);
784 my $error = $self->replace;
786 warn "WARNING: error storing token: $error, but proceeding anyway\n";
796 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
808 if (ref($_[0]) eq 'HASH') {
811 my ( $method, $amount ) = ( shift, shift );
813 $options{method} = $method;
814 $options{amount} = $amount;
817 if ( $options{'fake_failure'} ) {
818 return "Error: No error; test failure requested with fake_failure";
821 my $cust_pay = new FS::cust_pay ( {
822 'custnum' => $self->custnum,
823 'invnum' => $options{'invnum'},
824 'paid' => $options{amount},
826 'payby' => $bop_method2payby{$options{method}},
827 #'payinfo' => $payinfo,
828 'payinfo' => '4111111111111111',
829 #'paydate' => $paydate,
830 'paydate' => '2012-05-01',
831 'processor' => 'FakeProcessor',
833 'order_number' => '32',
835 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
838 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
839 warn " $_ => $options{$_}\n" foreach keys %options;
842 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
845 $cust_pay->invnum(''); #try again with no specific invnum
846 my $error2 = $cust_pay->insert( $options{'manual'} ?
847 ( 'manual' => 1 ) : ()
850 # gah, even with transactions.
851 my $e = 'WARNING: Card/ACH debited but database not updated - '.
852 "error inserting (fake!) payment: $error2".
853 " (previously tried insert with invnum #$options{'invnum'}" .
860 if ( $options{'paynum_ref'} ) {
861 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
869 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
871 # Wraps up processing of a realtime credit card, ACH (electronic check) or
872 # phone bill transaction.
874 sub _realtime_bop_result {
875 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
877 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
880 warn "$me _realtime_bop_result: pending transaction ".
881 $cust_pay_pending->paypendingnum. "\n";
882 warn " $_ => $options{$_}\n" foreach keys %options;
885 my $payment_gateway = $options{payment_gateway}
886 or return "no payment gateway in arguments to _realtime_bop_result";
888 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
889 my $cpp_captured_err = $cust_pay_pending->replace;
890 return $cpp_captured_err if $cpp_captured_err;
892 if ( $transaction->is_success() ) {
894 my $order_number = $transaction->order_number
895 if $transaction->can('order_number');
897 my $cust_pay = new FS::cust_pay ( {
898 'custnum' => $self->custnum,
899 'invnum' => $options{'invnum'},
900 'paid' => $cust_pay_pending->paid,
902 'payby' => $cust_pay_pending->payby,
903 'payinfo' => $options{'payinfo'},
904 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
905 'paydate' => $cust_pay_pending->paydate,
906 'pkgnum' => $cust_pay_pending->pkgnum,
907 'discount_term' => $options{'discount_term'},
908 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
909 'processor' => $payment_gateway->gateway_module,
910 'auth' => $transaction->authorization,
911 'order_number' => $order_number || '',
912 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
914 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
915 $cust_pay->payunique( $options{payunique} )
916 if defined($options{payunique}) && length($options{payunique});
918 my $oldAutoCommit = $FS::UID::AutoCommit;
919 local $FS::UID::AutoCommit = 0;
922 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
924 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
927 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
928 $cust_pay->invnum(''); #try again with no specific invnum
929 $cust_pay->paynum('');
930 my $error2 = $cust_pay->insert( $options{'manual'} ?
931 ( 'manual' => 1 ) : ()
934 # gah. but at least we have a record of the state we had to abort in
935 # from cust_pay_pending now.
936 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
937 my $e = "WARNING: $options{method} captured but payment not recorded -".
938 " error inserting payment (". $payment_gateway->gateway_module.
940 " (previously tried insert with invnum #$options{'invnum'}" .
941 ": $error ) - pending payment saved as paypendingnum ".
942 $cust_pay_pending->paypendingnum. "\n";
948 my $jobnum = $cust_pay_pending->jobnum;
950 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
952 unless ( $placeholder ) {
953 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
954 my $e = "WARNING: $options{method} captured but job $jobnum not ".
955 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
960 $error = $placeholder->delete;
963 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
964 my $e = "WARNING: $options{method} captured but could not delete ".
965 "job $jobnum for paypendingnum ".
966 $cust_pay_pending->paypendingnum. ": $error\n";
971 $cust_pay_pending->set('jobnum','');
975 if ( $options{'paynum_ref'} ) {
976 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
979 $cust_pay_pending->status('done');
980 $cust_pay_pending->statustext('captured');
981 $cust_pay_pending->paynum($cust_pay->paynum);
982 my $cpp_done_err = $cust_pay_pending->replace;
984 if ( $cpp_done_err ) {
986 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
987 my $e = "WARNING: $options{method} captured but payment not recorded - ".
988 "error updating status for paypendingnum ".
989 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
995 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
997 if ( $options{'apply'} ) {
998 my $apply_error = $self->apply_payments_and_credits;
999 if ( $apply_error ) {
1000 warn "WARNING: error applying payment: $apply_error\n";
1001 #but we still should return no error cause the payment otherwise went
1006 # have a CC surcharge portion --> one-time charge
1007 if ( $options{'cc_surcharge'} > 0 ) {
1008 # XXX: this whole block needs to be in a transaction?
1011 $invnum = $options{'invnum'} if $options{'invnum'};
1012 unless ( $invnum ) { # probably from a payment screen
1013 # do we have any open invoices? pick earliest
1014 # uses the fact that cust_main->cust_bill sorts by date ascending
1015 my @open = $self->open_cust_bill;
1016 $invnum = $open[0]->invnum if scalar(@open);
1019 unless ( $invnum ) { # still nothing? pick last closed invoice
1020 # again uses fact that cust_main->cust_bill sorts by date ascending
1021 my @closed = $self->cust_bill;
1022 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1025 unless ( $invnum ) {
1026 # XXX: unlikely case - pre-paying before any invoices generated
1027 # what it should do is create a new invoice and pick it
1028 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1033 my $charge_error = $self->charge({
1034 'amount' => $options{'cc_surcharge'},
1035 'pkg' => 'Credit Card Surcharge',
1037 'cust_pkg_ref' => \$cust_pkg,
1040 warn 'Unable to add CC surcharge cust_pkg';
1044 $cust_pkg->setup(time);
1045 my $cp_error = $cust_pkg->replace;
1047 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1051 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1052 unless ( $cust_bill ) {
1053 warn "race condition + invoice deletion just happened";
1058 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1060 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1064 return ''; #no error
1070 my $perror = $transaction->error_message;
1071 #$payment_gateway->gateway_module. " error: ".
1072 # removed for conciseness
1074 my $jobnum = $cust_pay_pending->jobnum;
1076 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1078 if ( $placeholder ) {
1079 my $error = $placeholder->depended_delete;
1080 $error ||= $placeholder->delete;
1081 $cust_pay_pending->set('jobnum','');
1082 warn "error removing provisioning jobs after declined paypendingnum ".
1083 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1085 my $e = "error finding job $jobnum for declined paypendingnum ".
1086 $cust_pay_pending->paypendingnum. "\n";
1092 unless ( $transaction->error_message ) {
1095 if ( $transaction->can('response_page') ) {
1097 'page' => ( $transaction->can('response_page')
1098 ? $transaction->response_page
1101 'code' => ( $transaction->can('response_code')
1102 ? $transaction->response_code
1105 'headers' => ( $transaction->can('response_headers')
1106 ? $transaction->response_headers
1112 "No additional debugging information available for ".
1113 $payment_gateway->gateway_module;
1116 $perror .= "No error_message returned from ".
1117 $payment_gateway->gateway_module. " -- ".
1118 ( ref($t_response) ? Dumper($t_response) : $t_response );
1122 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1123 && $conf->exists('emaildecline', $self->agentnum)
1124 && grep { $_ ne 'POST' } $self->invoicing_list
1125 && ! grep { $transaction->error_message =~ /$_/ }
1126 $conf->config('emaildecline-exclude', $self->agentnum)
1129 # Send a decline alert to the customer.
1130 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1133 # include the raw error message in the transaction state
1134 $cust_pay_pending->setfield('error', $transaction->error_message);
1135 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1136 $error = $msg_template->send( 'cust_main' => $self,
1137 'object' => $cust_pay_pending );
1141 $perror .= " (also received error sending decline notification: $error)"
1146 $cust_pay_pending->status('done');
1147 $cust_pay_pending->statustext($perror);
1148 #'declined:': no, that's failure_status
1149 if ( $transaction->can('failure_status') ) {
1150 $cust_pay_pending->failure_status( $transaction->failure_status );
1152 my $cpp_done_err = $cust_pay_pending->replace;
1153 if ( $cpp_done_err ) {
1154 my $e = "WARNING: $options{method} declined but pending payment not ".
1155 "resolved - error updating status for paypendingnum ".
1156 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1158 $perror = "$e ($perror)";
1166 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1168 Verifies successful third party processing of a realtime credit card,
1169 ACH (electronic check) or phone bill transaction via a
1170 Business::OnlineThirdPartyPayment realtime gateway. See
1171 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1173 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1175 The additional options I<payname>, I<city>, I<state>,
1176 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1177 if set, will override the value from the customer record.
1179 I<description> is a free-text field passed to the gateway. It defaults to
1180 "Internet services".
1182 If an I<invnum> is specified, this payment (if successful) is applied to the
1183 specified invoice. If you don't specify an I<invnum> you might want to
1184 call the B<apply_payments> method.
1186 I<quiet> can be set true to surpress email decline notices.
1188 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1189 resulting paynum, if any.
1191 I<payunique> is a unique identifier for this payment.
1193 Returns a hashref containing elements bill_error (which will be undefined
1194 upon success) and session_id of any associated session.
1198 sub realtime_botpp_capture {
1199 my( $self, $cust_pay_pending, %options ) = @_;
1201 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1204 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1205 warn " $_ => $options{$_}\n" foreach keys %options;
1208 eval "use Business::OnlineThirdPartyPayment";
1212 # select the gateway
1215 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1217 my $payment_gateway;
1218 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1219 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1220 { gatewaynum => $gatewaynum }
1222 : $self->agent->payment_gateway( 'method' => $method,
1223 # 'invnum' => $cust_pay_pending->invnum,
1224 # 'payinfo' => $cust_pay_pending->payinfo,
1227 $options{payment_gateway} = $payment_gateway; # for the helper subs
1233 my @invoicing_list = $self->invoicing_list_emailonly;
1234 if ( $conf->exists('emailinvoiceautoalways')
1235 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1236 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1237 push @invoicing_list, $self->all_emails;
1240 my $email = ($conf->exists('business-onlinepayment-email-override'))
1241 ? $conf->config('business-onlinepayment-email-override')
1242 : $invoicing_list[0];
1246 $content{email_customer} =
1247 ( $conf->exists('business-onlinepayment-email_customer')
1248 || $conf->exists('business-onlinepayment-email-override') );
1251 # run transaction(s)
1255 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1256 $self->_bop_options(\%options),
1259 $transaction->reference({ %options });
1261 $transaction->content(
1263 $self->_bop_auth(\%options),
1264 'action' => 'Post Authorization',
1265 'description' => $options{'description'},
1266 'amount' => $cust_pay_pending->paid,
1267 #'invoice_number' => $options{'invnum'},
1268 'customer_id' => $self->custnum,
1269 'reference' => $cust_pay_pending->paypendingnum,
1271 'phone' => $self->daytime || $self->night,
1273 # plus whatever is required for bogus capture avoidance
1276 $transaction->submit();
1279 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1281 if ( $options{'apply'} ) {
1282 my $apply_error = $self->apply_payments_and_credits;
1283 if ( $apply_error ) {
1284 warn "WARNING: error applying payment: $apply_error\n";
1289 bill_error => $error,
1290 session_id => $cust_pay_pending->session_id,
1295 =item default_payment_gateway
1297 DEPRECATED -- use agent->payment_gateway
1301 sub default_payment_gateway {
1302 my( $self, $method ) = @_;
1304 die "Real-time processing not enabled\n"
1305 unless $conf->exists('business-onlinepayment');
1307 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1310 my $bop_config = 'business-onlinepayment';
1311 $bop_config .= '-ach'
1312 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1313 my ( $processor, $login, $password, $action, @bop_options ) =
1314 $conf->config($bop_config);
1315 $action ||= 'normal authorization';
1316 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1317 die "No real-time processor is enabled - ".
1318 "did you set the business-onlinepayment configuration value?\n"
1321 ( $processor, $login, $password, $action, @bop_options )
1324 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1326 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1327 via a Business::OnlinePayment realtime gateway. See
1328 L<http://420.am/business-onlinepayment> for supported gateways.
1330 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1332 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1334 Most gateways require a reference to an original payment transaction to refund,
1335 so you probably need to specify a I<paynum>.
1337 I<amount> defaults to the original amount of the payment if not specified.
1339 I<reasonnum> specified an existing refund reason for the refund
1341 I<paydate> specifies the expiration date for a credit card overriding the
1342 value from the customer record or the payment record. Specified as yyyy-mm-dd
1344 Implementation note: If I<amount> is unspecified or equal to the amount of the
1345 orignal payment, first an attempt is made to "void" the transaction via
1346 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1347 the normal attempt is made to "refund" ("credit") the transaction via the
1348 gateway is attempted. No attempt to "void" the transaction is made if the
1349 gateway has introspection data and doesn't support void.
1351 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1352 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1353 #if set, will override the value from the customer record.
1355 #If an I<invnum> is specified, this payment (if successful) is applied to the
1356 #specified invoice. If you don't specify an I<invnum> you might want to
1357 #call the B<apply_payments> method.
1361 #some false laziness w/realtime_bop, not enough to make it worth merging
1362 #but some useful small subs should be pulled out
1363 sub realtime_refund_bop {
1366 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1369 if (ref($_[0]) eq 'HASH') {
1370 %options = %{$_[0]};
1374 $options{method} = $method;
1378 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1379 warn " $_ => $options{$_}\n" foreach keys %options;
1382 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1387 # look up the original payment and optionally a gateway for that payment
1391 my $amount = $options{'amount'};
1393 my( $processor, $login, $password, @bop_options, $namespace ) ;
1394 my( $auth, $order_number ) = ( '', '', '' );
1395 my $gatewaynum = '';
1397 if ( $options{'paynum'} ) {
1399 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1400 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1401 or return "Unknown paynum $options{'paynum'}";
1402 $amount ||= $cust_pay->paid;
1404 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1405 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1407 if ( $cust_pay->get('processor') ) {
1408 ($gatewaynum, $processor, $auth, $order_number) =
1410 $cust_pay->gatewaynum,
1411 $cust_pay->processor,
1413 $cust_pay->order_number,
1416 # this payment wasn't upgraded, which probably means this won't work,
1418 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1419 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1420 $cust_pay->paybatch;
1421 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1424 if ( $gatewaynum ) { #gateway for the payment to be refunded
1426 my $payment_gateway =
1427 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1428 die "payment gateway $gatewaynum not found"
1429 unless $payment_gateway;
1431 $processor = $payment_gateway->gateway_module;
1432 $login = $payment_gateway->gateway_username;
1433 $password = $payment_gateway->gateway_password;
1434 $namespace = $payment_gateway->gateway_namespace;
1435 @bop_options = $payment_gateway->options;
1437 } else { #try the default gateway
1440 my $payment_gateway =
1441 $self->agent->payment_gateway('method' => $options{method});
1443 ( $conf_processor, $login, $password, $namespace ) =
1444 map { my $method = "gateway_$_"; $payment_gateway->$method }
1445 qw( module username password namespace );
1447 @bop_options = $payment_gateway->gatewaynum
1448 ? $payment_gateway->options
1449 : @{ $payment_gateway->get('options') };
1451 return "processor of payment $options{'paynum'} $processor does not".
1452 " match default processor $conf_processor"
1453 unless $processor eq $conf_processor;
1458 } else { # didn't specify a paynum, so look for agent gateway overrides
1459 # like a normal transaction
1461 my $payment_gateway =
1462 $self->agent->payment_gateway( 'method' => $options{method},
1463 #'payinfo' => $payinfo,
1465 my( $processor, $login, $password, $namespace ) =
1466 map { my $method = "gateway_$_"; $payment_gateway->$method }
1467 qw( module username password namespace );
1469 my @bop_options = $payment_gateway->gatewaynum
1470 ? $payment_gateway->options
1471 : @{ $payment_gateway->get('options') };
1474 return "neither amount nor paynum specified" unless $amount;
1476 eval "use $namespace";
1481 'type' => $options{method},
1483 'password' => $password,
1484 'order_number' => $order_number,
1485 'amount' => $amount,
1487 $content{authorization} = $auth
1488 if length($auth); #echeck/ACH transactions have an order # but no auth
1489 #(at least with authorize.net)
1491 my $currency = $conf->exists('business-onlinepayment-currency')
1492 && $conf->config('business-onlinepayment-currency');
1493 $content{currency} = $currency if $currency;
1495 my $disable_void_after;
1496 if ($conf->exists('disable_void_after')
1497 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1498 $disable_void_after = $1;
1501 #first try void if applicable
1502 my $void = new Business::OnlinePayment( $processor, @bop_options );
1505 if ($void->can('info')) {
1507 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1508 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1509 my %supported_actions = $void->info('supported_actions');
1511 if ( %supported_actions && $paytype
1512 && defined($supported_actions{$paytype})
1513 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1516 if ( $cust_pay && $cust_pay->paid == $amount
1518 ( not defined($disable_void_after) )
1519 || ( time < ($cust_pay->_date + $disable_void_after ) )
1523 warn " attempting void\n" if $DEBUG > 1;
1524 if ( $void->can('info') ) {
1525 if ( $cust_pay->payby eq 'CARD'
1526 && $void->info('CC_void_requires_card') )
1528 $content{'card_number'} = $cust_pay->payinfo;
1529 } elsif ( $cust_pay->payby eq 'CHEK'
1530 && $void->info('ECHECK_void_requires_account') )
1532 ( $content{'account_number'}, $content{'routing_code'} ) =
1533 split('@', $cust_pay->payinfo);
1534 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1537 $void->content( 'action' => 'void', %content );
1538 $void->test_transaction(1)
1539 if $conf->exists('business-onlinepayment-test_transaction');
1541 if ( $void->is_success ) {
1542 # specified as a refund reason, but now we want a payment void reason
1543 # extract just the reason text, let cust_pay::void handle new_or_existing
1544 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1546 $error = 'Reason could not be loaded' unless $reason;
1547 $error = $cust_pay->void($reason->reason) unless $error;
1549 # gah, even with transactions.
1550 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1551 "error voiding payment: $error";
1555 warn " void successful\n" if $DEBUG > 1;
1560 warn " void unsuccessful, trying refund\n"
1564 my $address = $self->address1;
1565 $address .= ", ". $self->address2 if $self->address2;
1567 my($payname, $payfirst, $paylast);
1568 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1569 $payname = $self->payname;
1570 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1571 or return "Illegal payname $payname";
1572 ($payfirst, $paylast) = ($1, $2);
1574 $payfirst = $self->getfield('first');
1575 $paylast = $self->getfield('last');
1576 $payname = "$payfirst $paylast";
1579 my @invoicing_list = $self->invoicing_list_emailonly;
1580 if ( $conf->exists('emailinvoiceautoalways')
1581 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1582 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1583 push @invoicing_list, $self->all_emails;
1586 my $email = ($conf->exists('business-onlinepayment-email-override'))
1587 ? $conf->config('business-onlinepayment-email-override')
1588 : $invoicing_list[0];
1590 my $payip = exists($options{'payip'})
1593 $content{customer_ip} = $payip
1597 if ( $options{method} eq 'CC' ) {
1600 $content{card_number} = $payinfo = $cust_pay->payinfo;
1601 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1602 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1603 ($content{expiration} = "$2/$1"); # where available
1605 $content{card_number} = $payinfo = $self->payinfo;
1606 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1607 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1608 $content{expiration} = "$2/$1";
1611 } elsif ( $options{method} eq 'ECHECK' ) {
1614 $payinfo = $cust_pay->payinfo;
1616 $payinfo = $self->payinfo;
1618 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1619 $content{bank_name} = $self->payname;
1620 $content{account_type} = 'CHECKING';
1621 $content{account_name} = $payname;
1622 $content{customer_org} = $self->company ? 'B' : 'I';
1623 $content{customer_ssn} = $self->ss;
1624 } elsif ( $options{method} eq 'LEC' ) {
1625 $content{phone} = $payinfo = $self->payinfo;
1629 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1630 my %sub_content = $refund->content(
1631 'action' => 'credit',
1632 'customer_id' => $self->custnum,
1633 'last_name' => $paylast,
1634 'first_name' => $payfirst,
1636 'address' => $address,
1637 'city' => $self->city,
1638 'state' => $self->state,
1639 'zip' => $self->zip,
1640 'country' => $self->country,
1642 'phone' => $self->daytime || $self->night,
1645 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1647 $refund->test_transaction(1)
1648 if $conf->exists('business-onlinepayment-test_transaction');
1651 return "$processor error: ". $refund->error_message
1652 unless $refund->is_success();
1654 $order_number = $refund->order_number if $refund->can('order_number');
1656 # change this to just use $cust_pay->delete_cust_bill_pay?
1657 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1658 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1659 last unless @cust_bill_pay;
1660 my $cust_bill_pay = pop @cust_bill_pay;
1661 my $error = $cust_bill_pay->delete;
1665 my $cust_refund = new FS::cust_refund ( {
1666 'custnum' => $self->custnum,
1667 'paynum' => $options{'paynum'},
1668 'source_paynum' => $options{'paynum'},
1669 'refund' => $amount,
1671 'payby' => $bop_method2payby{$options{method}},
1672 'payinfo' => $payinfo,
1673 'reasonnum' => $options{'reasonnum'},
1674 'gatewaynum' => $gatewaynum, # may be null
1675 'processor' => $processor,
1676 'auth' => $refund->authorization,
1677 'order_number' => $order_number,
1679 my $error = $cust_refund->insert;
1681 $cust_refund->paynum(''); #try again with no specific paynum
1682 $cust_refund->source_paynum('');
1683 my $error2 = $cust_refund->insert;
1685 # gah, even with transactions.
1686 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1687 "error inserting refund ($processor): $error2".
1688 " (previously tried insert with paynum #$options{'paynum'}" .
1707 L<FS::cust_main>, L<FS::cust_main::Billing>