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 return '' unless $options{amount} > 0;
127 $options{method} = FS::payby->payby2bop($self->payby)
128 unless exists( $options{method} );
130 return $self->realtime_bop({%options});
134 =item realtime_bop { [ ARG => VALUE ... ] }
136 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
137 via a Business::OnlinePayment realtime gateway. See
138 L<http://420.am/business-onlinepayment> for supported gateways.
140 Required arguments in the hashref are I<method>, and I<amount>
142 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
144 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
146 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
147 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
148 if set, will override the value from the customer record.
150 I<description> is a free-text field passed to the gateway. It defaults to
151 the value defined by the business-onlinepayment-description configuration
152 option, or "Internet services" if that is unset.
154 If an I<invnum> is specified, this payment (if successful) is applied to the
155 specified invoice. If the customer has exactly one open invoice, that
156 invoice number will be assumed. If you don't specify an I<invnum> you might
157 want to call the B<apply_payments> method or set the I<apply> option.
159 I<no_invnum> can be set to true to prevent that default invnum from being set.
161 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
163 I<no_auto_apply> can be set to true to set that flag on the resulting payment
164 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
165 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
167 I<quiet> can be set true to surpress email decline notices.
169 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
170 resulting paynum, if any.
172 I<payunique> is a unique identifier for this payment.
174 I<session_id> is a session identifier associated with this payment.
176 I<depend_jobnum> allows payment capture to unlock export jobs
178 I<discount_term> attempts to take a discount by prepaying for discount_term.
179 The payment will fail if I<amount> is incorrect for this discount term.
181 A direct (Business::OnlinePayment) transaction will return nothing on success,
182 or an error message on failure.
184 A third-party transaction will return a hashref containing:
186 - popup_url: the URL to which a browser should be redirected to complete
188 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
189 - reference: a reference ID for the transaction, to show the customer.
191 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
195 # some helper routines
197 # _bop_recurring_billing: Checks whether this payment should have the
198 # recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
199 # vSecure, etc.). This works in two different modes:
200 # - actual_oncard (default): treat the payment as recurring if the customer
201 # has made a payment using this card before.
202 # - transaction_is_recur: treat the payment as recurring if the invoice
203 # being paid has any recurring package charges.
205 sub _bop_recurring_billing {
206 my( $self, %opt ) = @_;
208 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
210 if ( defined($method) && $method eq 'transaction_is_recur' ) {
212 return 1 if $opt{'trans_is_recur'};
216 # return 1 if the payinfo has been used for another payment
217 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
225 sub _payment_gateway {
226 my ($self, $options) = @_;
228 if ( $options->{'selfservice'} ) {
229 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
231 return $options->{payment_gateway} ||=
232 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
236 if ( $options->{'fake_gatewaynum'} ) {
237 $options->{payment_gateway} =
238 qsearchs('payment_gateway',
239 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
243 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
244 unless exists($options->{payment_gateway});
246 $options->{payment_gateway};
250 my ($self, $options) = @_;
253 'login' => $options->{payment_gateway}->gateway_username,
254 'password' => $options->{payment_gateway}->gateway_password,
259 my ($self, $options) = @_;
261 $options->{payment_gateway}->gatewaynum
262 ? $options->{payment_gateway}->options
263 : @{ $options->{payment_gateway}->get('options') };
268 my ($self, $options) = @_;
270 unless ( $options->{'description'} ) {
271 if ( $conf->exists('business-onlinepayment-description') ) {
272 my $dtempl = $conf->config('business-onlinepayment-description');
274 my $agent = $self->agent->agent;
276 $options->{'description'} = eval qq("$dtempl");
278 $options->{'description'} = 'Internet services';
282 unless ( exists( $options->{'payinfo'} ) ) {
283 $options->{'payinfo'} = $self->payinfo;
284 $options->{'paymask'} = $self->paymask;
287 # Default invoice number if the customer has exactly one open invoice.
288 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
289 $options->{'invnum'} = '';
290 my @open = $self->open_cust_bill;
291 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
294 $options->{payname} = $self->payname unless exists( $options->{payname} );
298 my ($self, $options) = @_;
301 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
302 $content{customer_ip} = $payip if length($payip);
304 $content{invoice_number} = $options->{'invnum'}
305 if exists($options->{'invnum'}) && length($options->{'invnum'});
307 $content{email_customer} =
308 ( $conf->exists('business-onlinepayment-email_customer')
309 || $conf->exists('business-onlinepayment-email-override') );
311 my ($payname, $payfirst, $paylast);
312 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
313 ($payname = $options->{payname}) =~
314 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
315 or return "Illegal payname $payname";
316 ($payfirst, $paylast) = ($1, $2);
318 $payfirst = $self->getfield('first');
319 $paylast = $self->getfield('last');
320 $payname = "$payfirst $paylast";
323 $content{last_name} = $paylast;
324 $content{first_name} = $payfirst;
326 $content{name} = $payname;
328 $content{address} = exists($options->{'address1'})
329 ? $options->{'address1'}
331 my $address2 = exists($options->{'address2'})
332 ? $options->{'address2'}
334 $content{address} .= ", ". $address2 if length($address2);
336 $content{city} = exists($options->{city})
339 $content{state} = exists($options->{state})
342 $content{zip} = exists($options->{zip})
345 $content{country} = exists($options->{country})
346 ? $options->{country}
349 $content{phone} = $self->daytime || $self->night;
351 my $currency = $conf->exists('business-onlinepayment-currency')
352 && $conf->config('business-onlinepayment-currency');
353 $content{currency} = $currency if $currency;
358 my %bop_method2payby = (
368 confess "Can't call realtime_bop within another transaction ".
369 '($FS::UID::AutoCommit is false)'
370 unless $FS::UID::AutoCommit;
372 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
375 if (ref($_[0]) eq 'HASH') {
378 my ( $method, $amount ) = ( shift, shift );
380 $options{method} = $method;
381 $options{amount} = $amount;
386 # optional credit card surcharge
389 my $cc_surcharge = 0;
390 my $cc_surcharge_pct = 0;
391 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
392 if $conf->config('credit-card-surcharge-percentage')
393 && $options{method} eq 'CC';
395 # always add cc surcharge if called from event
396 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
397 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
398 $options{'amount'} += $cc_surcharge;
399 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
401 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
402 # payment screen), so consider the given
403 # amount as post-surcharge
404 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
407 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
408 $options{'cc_surcharge'} = $cc_surcharge;
412 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
413 warn " cc_surcharge = $cc_surcharge\n";
416 warn " $_ => $options{$_}\n" foreach keys %options;
419 return $self->fake_bop(\%options) if $options{'fake'};
421 $self->_bop_defaults(\%options);
424 # set trans_is_recur based on invnum if there is one
427 my $trans_is_recur = 0;
428 if ( $options{'invnum'} ) {
430 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
431 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
437 $cust_bill->cust_bill_pkg;
440 if grep { $_->freq ne '0' } @part_pkg;
448 my $payment_gateway = $self->_payment_gateway( \%options );
449 my $namespace = $payment_gateway->gateway_namespace;
451 eval "use $namespace";
455 # check for banned credit card/ACH
458 my $ban = FS::banned_pay->ban_search(
459 'payby' => $bop_method2payby{$options{method}},
460 'payinfo' => $options{payinfo},
462 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
465 # check for term discount validity
468 my $discount_term = $options{discount_term};
469 if ( $discount_term ) {
470 my $bill = ($self->cust_bill)[-1]
471 or return "Can't apply a term discount to an unbilled customer";
472 my $plan = FS::discount_plan->new(
474 months => $discount_term
475 ) or return "No discount available for term '$discount_term'";
477 if ( $plan->discounted_total != $options{amount} ) {
478 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
486 my $bop_content = $self->_bop_content(\%options);
487 return $bop_content unless ref($bop_content);
489 my @invoicing_list = $self->invoicing_list_emailonly;
490 if ( $conf->exists('emailinvoiceautoalways')
491 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
492 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
493 push @invoicing_list, $self->all_emails;
496 my $email = ($conf->exists('business-onlinepayment-email-override'))
497 ? $conf->config('business-onlinepayment-email-override')
498 : $invoicing_list[0];
503 if ( $namespace eq 'Business::OnlinePayment' ) {
505 if ( $options{method} eq 'CC' ) {
507 $content{card_number} = $options{payinfo};
508 $paydate = exists($options{'paydate'})
509 ? $options{'paydate'}
511 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
512 $content{expiration} = "$2/$1";
514 my $paycvv = exists($options{'paycvv'})
517 $content{cvv2} = $paycvv
520 my $paystart_month = exists($options{'paystart_month'})
521 ? $options{'paystart_month'}
522 : $self->paystart_month;
524 my $paystart_year = exists($options{'paystart_year'})
525 ? $options{'paystart_year'}
526 : $self->paystart_year;
528 $content{card_start} = "$paystart_month/$paystart_year"
529 if $paystart_month && $paystart_year;
531 my $payissue = exists($options{'payissue'})
532 ? $options{'payissue'}
534 $content{issue_number} = $payissue if $payissue;
536 if ( $self->_bop_recurring_billing(
537 'payinfo' => $options{'payinfo'},
538 'trans_is_recur' => $trans_is_recur,
542 $content{recurring_billing} = 'YES';
543 $content{acct_code} = 'rebill'
544 if $conf->exists('credit_card-recurring_billing_acct_code');
547 } elsif ( $options{method} eq 'ECHECK' ){
549 ( $content{account_number}, $content{routing_code} ) =
550 split('@', $options{payinfo});
551 $content{bank_name} = $options{payname};
552 $content{bank_state} = exists($options{'paystate'})
553 ? $options{'paystate'}
554 : $self->getfield('paystate');
555 $content{account_type}=
556 (exists($options{'paytype'}) && $options{'paytype'})
557 ? uc($options{'paytype'})
558 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
560 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
561 $content{account_name} = $self->company;
563 $content{account_name} = $self->getfield('first'). ' '.
564 $self->getfield('last');
567 $content{customer_org} = $self->company ? 'B' : 'I';
568 $content{state_id} = exists($options{'stateid'})
569 ? $options{'stateid'}
570 : $self->getfield('stateid');
571 $content{state_id_state} = exists($options{'stateid_state'})
572 ? $options{'stateid_state'}
573 : $self->getfield('stateid_state');
574 $content{customer_ssn} = exists($options{'ss'})
578 } elsif ( $options{method} eq 'LEC' ) {
579 $content{phone} = $options{payinfo};
581 die "unknown method ". $options{method};
584 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
587 die "unknown namespace $namespace";
594 my $balance = exists( $options{'balance'} )
595 ? $options{'balance'}
598 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
599 $self->select_for_update; #mutex ... just until we get our pending record in
600 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
602 #the checks here are intended to catch concurrent payments
603 #double-form-submission prevention is taken care of in cust_pay_pending::check
606 return "The customer's balance has changed; $options{method} transaction aborted."
607 if $self->balance < $balance;
609 #also check and make sure there aren't *other* pending payments for this cust
611 my @pending = qsearch('cust_pay_pending', {
612 'custnum' => $self->custnum,
613 'status' => { op=>'!=', value=>'done' }
616 #for third-party payments only, remove pending payments if they're in the
617 #'thirdparty' (waiting for customer action) state.
618 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
619 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
620 my $error = $_->delete;
621 warn "error deleting unfinished third-party payment ".
622 $_->paypendingnum . ": $error\n"
625 @pending = grep { $_->status ne 'thirdparty' } @pending;
628 return "A payment is already being processed for this customer (".
629 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
630 "); $options{method} transaction aborted."
633 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
635 my $cust_pay_pending = new FS::cust_pay_pending {
636 'custnum' => $self->custnum,
637 'paid' => $options{amount},
639 'payby' => $bop_method2payby{$options{method}},
640 'payinfo' => $options{payinfo},
641 'paymask' => $options{paymask},
642 'paydate' => $paydate,
643 'recurring_billing' => $content{recurring_billing},
644 'pkgnum' => $options{'pkgnum'},
646 'gatewaynum' => $payment_gateway->gatewaynum || '',
647 'session_id' => $options{session_id} || '',
648 'jobnum' => $options{depend_jobnum} || '',
650 $cust_pay_pending->payunique( $options{payunique} )
651 if defined($options{payunique}) && length($options{payunique});
653 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
655 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
656 return $cpp_new_err if $cpp_new_err;
658 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
660 warn Dumper($cust_pay_pending) if $DEBUG > 2;
662 my( $action1, $action2 ) =
663 split( /\s*\,\s*/, $payment_gateway->gateway_action );
665 my $transaction = new $namespace( $payment_gateway->gateway_module,
666 $self->_bop_options(\%options),
669 $transaction->content(
670 'type' => $options{method},
671 $self->_bop_auth(\%options),
672 'action' => $action1,
673 'description' => $options{'description'},
674 'amount' => $options{amount},
675 #'invoice_number' => $options{'invnum'},
676 'customer_id' => $self->custnum,
678 'reference' => $cust_pay_pending->paypendingnum, #for now
679 'callback_url' => $payment_gateway->gateway_callback_url,
680 'cancel_url' => $payment_gateway->gateway_cancel_url,
685 $cust_pay_pending->status('pending');
686 my $cpp_pending_err = $cust_pay_pending->replace;
687 return $cpp_pending_err if $cpp_pending_err;
689 warn Dumper($transaction) if $DEBUG > 2;
691 unless ( $BOP_TESTING ) {
692 $transaction->test_transaction(1)
693 if $conf->exists('business-onlinepayment-test_transaction');
694 $transaction->submit();
696 if ( $BOP_TESTING_SUCCESS ) {
697 $transaction->is_success(1);
698 $transaction->authorization('fake auth');
700 $transaction->is_success(0);
701 $transaction->error_message('fake failure');
705 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
707 $cust_pay_pending->status('thirdparty');
708 my $cpp_err = $cust_pay_pending->replace;
709 return { error => $cpp_err } if $cpp_err;
710 return { reference => $cust_pay_pending->paypendingnum,
711 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
713 } elsif ( $transaction->is_success() && $action2 ) {
715 $cust_pay_pending->status('authorized');
716 my $cpp_authorized_err = $cust_pay_pending->replace;
717 return $cpp_authorized_err if $cpp_authorized_err;
719 my $auth = $transaction->authorization;
720 my $ordernum = $transaction->can('order_number')
721 ? $transaction->order_number
725 new Business::OnlinePayment( $payment_gateway->gateway_module,
726 $self->_bop_options(\%options),
731 type => $options{method},
733 $self->_bop_auth(\%options),
734 order_number => $ordernum,
735 amount => $options{amount},
736 authorization => $auth,
737 description => $options{'description'},
740 foreach my $field (qw( authorization_source_code returned_ACI
741 transaction_identifier validation_code
742 transaction_sequence_num local_transaction_date
743 local_transaction_time AVS_result_code )) {
744 $capture{$field} = $transaction->$field() if $transaction->can($field);
747 $capture->content( %capture );
749 $capture->test_transaction(1)
750 if $conf->exists('business-onlinepayment-test_transaction');
753 unless ( $capture->is_success ) {
754 my $e = "Authorization successful but capture failed, custnum #".
755 $self->custnum. ': '. $capture->result_code.
756 ": ". $capture->error_message;
764 # remove paycvv after initial transaction
767 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
768 if ( length($self->paycvv)
769 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
771 my $error = $self->remove_cvv;
773 warn "WARNING: error removing cvv: $error\n";
782 if ( $transaction->can('card_token') && $transaction->card_token ) {
784 if ( $options{'payinfo'} eq $self->payinfo ) {
785 $self->payinfo($transaction->card_token);
786 my $error = $self->replace;
788 warn "WARNING: error storing token: $error, but proceeding anyway\n";
798 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
810 if (ref($_[0]) eq 'HASH') {
813 my ( $method, $amount ) = ( shift, shift );
815 $options{method} = $method;
816 $options{amount} = $amount;
819 if ( $options{'fake_failure'} ) {
820 return "Error: No error; test failure requested with fake_failure";
823 my $cust_pay = new FS::cust_pay ( {
824 'custnum' => $self->custnum,
825 'invnum' => $options{'invnum'},
826 'paid' => $options{amount},
828 'payby' => $bop_method2payby{$options{method}},
829 #'payinfo' => $payinfo,
830 'payinfo' => '4111111111111111',
831 #'paydate' => $paydate,
832 'paydate' => '2012-05-01',
833 'processor' => 'FakeProcessor',
835 'order_number' => '32',
837 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
840 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
841 warn " $_ => $options{$_}\n" foreach keys %options;
844 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
847 $cust_pay->invnum(''); #try again with no specific invnum
848 my $error2 = $cust_pay->insert( $options{'manual'} ?
849 ( 'manual' => 1 ) : ()
852 # gah, even with transactions.
853 my $e = 'WARNING: Card/ACH debited but database not updated - '.
854 "error inserting (fake!) payment: $error2".
855 " (previously tried insert with invnum #$options{'invnum'}" .
862 if ( $options{'paynum_ref'} ) {
863 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
871 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
873 # Wraps up processing of a realtime credit card, ACH (electronic check) or
874 # phone bill transaction.
876 sub _realtime_bop_result {
877 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
879 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
882 warn "$me _realtime_bop_result: pending transaction ".
883 $cust_pay_pending->paypendingnum. "\n";
884 warn " $_ => $options{$_}\n" foreach keys %options;
887 my $payment_gateway = $options{payment_gateway}
888 or return "no payment gateway in arguments to _realtime_bop_result";
890 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
891 my $cpp_captured_err = $cust_pay_pending->replace;
892 return $cpp_captured_err if $cpp_captured_err;
894 if ( $transaction->is_success() ) {
896 my $order_number = $transaction->order_number
897 if $transaction->can('order_number');
899 my $cust_pay = new FS::cust_pay ( {
900 'custnum' => $self->custnum,
901 'invnum' => $options{'invnum'},
902 'paid' => $cust_pay_pending->paid,
904 'payby' => $cust_pay_pending->payby,
905 'payinfo' => $options{'payinfo'},
906 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
907 'paydate' => $cust_pay_pending->paydate,
908 'pkgnum' => $cust_pay_pending->pkgnum,
909 'discount_term' => $options{'discount_term'},
910 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
911 'processor' => $payment_gateway->gateway_module,
912 'auth' => $transaction->authorization,
913 'order_number' => $order_number || '',
914 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
916 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
917 $cust_pay->payunique( $options{payunique} )
918 if defined($options{payunique}) && length($options{payunique});
920 my $oldAutoCommit = $FS::UID::AutoCommit;
921 local $FS::UID::AutoCommit = 0;
924 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
926 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
929 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
930 $cust_pay->invnum(''); #try again with no specific invnum
931 $cust_pay->paynum('');
932 my $error2 = $cust_pay->insert( $options{'manual'} ?
933 ( 'manual' => 1 ) : ()
936 # gah. but at least we have a record of the state we had to abort in
937 # from cust_pay_pending now.
938 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
939 my $e = "WARNING: $options{method} captured but payment not recorded -".
940 " error inserting payment (". $payment_gateway->gateway_module.
942 " (previously tried insert with invnum #$options{'invnum'}" .
943 ": $error ) - pending payment saved as paypendingnum ".
944 $cust_pay_pending->paypendingnum. "\n";
950 my $jobnum = $cust_pay_pending->jobnum;
952 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
954 unless ( $placeholder ) {
955 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
956 my $e = "WARNING: $options{method} captured but job $jobnum not ".
957 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
962 $error = $placeholder->delete;
965 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
966 my $e = "WARNING: $options{method} captured but could not delete ".
967 "job $jobnum for paypendingnum ".
968 $cust_pay_pending->paypendingnum. ": $error\n";
973 $cust_pay_pending->set('jobnum','');
977 if ( $options{'paynum_ref'} ) {
978 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
981 $cust_pay_pending->status('done');
982 $cust_pay_pending->statustext('captured');
983 $cust_pay_pending->paynum($cust_pay->paynum);
984 my $cpp_done_err = $cust_pay_pending->replace;
986 if ( $cpp_done_err ) {
988 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
989 my $e = "WARNING: $options{method} captured but payment not recorded - ".
990 "error updating status for paypendingnum ".
991 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
997 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
999 if ( $options{'apply'} ) {
1000 my $apply_error = $self->apply_payments_and_credits;
1001 if ( $apply_error ) {
1002 warn "WARNING: error applying payment: $apply_error\n";
1003 #but we still should return no error cause the payment otherwise went
1008 # have a CC surcharge portion --> one-time charge
1009 if ( $options{'cc_surcharge'} > 0 ) {
1010 # XXX: this whole block needs to be in a transaction?
1013 $invnum = $options{'invnum'} if $options{'invnum'};
1014 unless ( $invnum ) { # probably from a payment screen
1015 # do we have any open invoices? pick earliest
1016 # uses the fact that cust_main->cust_bill sorts by date ascending
1017 my @open = $self->open_cust_bill;
1018 $invnum = $open[0]->invnum if scalar(@open);
1021 unless ( $invnum ) { # still nothing? pick last closed invoice
1022 # again uses fact that cust_main->cust_bill sorts by date ascending
1023 my @closed = $self->cust_bill;
1024 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1027 unless ( $invnum ) {
1028 # XXX: unlikely case - pre-paying before any invoices generated
1029 # what it should do is create a new invoice and pick it
1030 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1035 my $charge_error = $self->charge({
1036 'amount' => $options{'cc_surcharge'},
1037 'pkg' => 'Credit Card Surcharge',
1039 'cust_pkg_ref' => \$cust_pkg,
1042 warn 'Unable to add CC surcharge cust_pkg';
1046 $cust_pkg->setup(time);
1047 my $cp_error = $cust_pkg->replace;
1049 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1053 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1054 unless ( $cust_bill ) {
1055 warn "race condition + invoice deletion just happened";
1060 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1062 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1066 return ''; #no error
1072 my $perror = $transaction->error_message;
1073 #$payment_gateway->gateway_module. " error: ".
1074 # removed for conciseness
1076 my $jobnum = $cust_pay_pending->jobnum;
1078 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1080 if ( $placeholder ) {
1081 my $error = $placeholder->depended_delete;
1082 $error ||= $placeholder->delete;
1083 $cust_pay_pending->set('jobnum','');
1084 warn "error removing provisioning jobs after declined paypendingnum ".
1085 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1087 my $e = "error finding job $jobnum for declined paypendingnum ".
1088 $cust_pay_pending->paypendingnum. "\n";
1094 unless ( $transaction->error_message ) {
1097 if ( $transaction->can('response_page') ) {
1099 'page' => ( $transaction->can('response_page')
1100 ? $transaction->response_page
1103 'code' => ( $transaction->can('response_code')
1104 ? $transaction->response_code
1107 'headers' => ( $transaction->can('response_headers')
1108 ? $transaction->response_headers
1114 "No additional debugging information available for ".
1115 $payment_gateway->gateway_module;
1118 $perror .= "No error_message returned from ".
1119 $payment_gateway->gateway_module. " -- ".
1120 ( ref($t_response) ? Dumper($t_response) : $t_response );
1124 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1125 && $conf->exists('emaildecline', $self->agentnum)
1126 && grep { $_ ne 'POST' } $self->invoicing_list
1127 && ! grep { $transaction->error_message =~ /$_/ }
1128 $conf->config('emaildecline-exclude', $self->agentnum)
1131 # Send a decline alert to the customer.
1132 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1135 # include the raw error message in the transaction state
1136 $cust_pay_pending->setfield('error', $transaction->error_message);
1137 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1138 $error = $msg_template->send( 'cust_main' => $self,
1139 'object' => $cust_pay_pending );
1143 $perror .= " (also received error sending decline notification: $error)"
1148 $cust_pay_pending->status('done');
1149 $cust_pay_pending->statustext($perror);
1150 #'declined:': no, that's failure_status
1151 if ( $transaction->can('failure_status') ) {
1152 $cust_pay_pending->failure_status( $transaction->failure_status );
1154 my $cpp_done_err = $cust_pay_pending->replace;
1155 if ( $cpp_done_err ) {
1156 my $e = "WARNING: $options{method} declined but pending payment not ".
1157 "resolved - error updating status for paypendingnum ".
1158 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1160 $perror = "$e ($perror)";
1168 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1170 Verifies successful third party processing of a realtime credit card,
1171 ACH (electronic check) or phone bill transaction via a
1172 Business::OnlineThirdPartyPayment realtime gateway. See
1173 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1175 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1177 The additional options I<payname>, I<city>, I<state>,
1178 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1179 if set, will override the value from the customer record.
1181 I<description> is a free-text field passed to the gateway. It defaults to
1182 "Internet services".
1184 If an I<invnum> is specified, this payment (if successful) is applied to the
1185 specified invoice. If you don't specify an I<invnum> you might want to
1186 call the B<apply_payments> method.
1188 I<quiet> can be set true to surpress email decline notices.
1190 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1191 resulting paynum, if any.
1193 I<payunique> is a unique identifier for this payment.
1195 Returns a hashref containing elements bill_error (which will be undefined
1196 upon success) and session_id of any associated session.
1200 sub realtime_botpp_capture {
1201 my( $self, $cust_pay_pending, %options ) = @_;
1203 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1206 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1207 warn " $_ => $options{$_}\n" foreach keys %options;
1210 eval "use Business::OnlineThirdPartyPayment";
1214 # select the gateway
1217 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1219 my $payment_gateway;
1220 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1221 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1222 { gatewaynum => $gatewaynum }
1224 : $self->agent->payment_gateway( 'method' => $method,
1225 # 'invnum' => $cust_pay_pending->invnum,
1226 # 'payinfo' => $cust_pay_pending->payinfo,
1229 $options{payment_gateway} = $payment_gateway; # for the helper subs
1235 my @invoicing_list = $self->invoicing_list_emailonly;
1236 if ( $conf->exists('emailinvoiceautoalways')
1237 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1238 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1239 push @invoicing_list, $self->all_emails;
1242 my $email = ($conf->exists('business-onlinepayment-email-override'))
1243 ? $conf->config('business-onlinepayment-email-override')
1244 : $invoicing_list[0];
1248 $content{email_customer} =
1249 ( $conf->exists('business-onlinepayment-email_customer')
1250 || $conf->exists('business-onlinepayment-email-override') );
1253 # run transaction(s)
1257 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1258 $self->_bop_options(\%options),
1261 $transaction->reference({ %options });
1263 $transaction->content(
1265 $self->_bop_auth(\%options),
1266 'action' => 'Post Authorization',
1267 'description' => $options{'description'},
1268 'amount' => $cust_pay_pending->paid,
1269 #'invoice_number' => $options{'invnum'},
1270 'customer_id' => $self->custnum,
1271 'reference' => $cust_pay_pending->paypendingnum,
1273 'phone' => $self->daytime || $self->night,
1275 # plus whatever is required for bogus capture avoidance
1278 $transaction->submit();
1281 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1283 if ( $options{'apply'} ) {
1284 my $apply_error = $self->apply_payments_and_credits;
1285 if ( $apply_error ) {
1286 warn "WARNING: error applying payment: $apply_error\n";
1291 bill_error => $error,
1292 session_id => $cust_pay_pending->session_id,
1297 =item default_payment_gateway
1299 DEPRECATED -- use agent->payment_gateway
1303 sub default_payment_gateway {
1304 my( $self, $method ) = @_;
1306 die "Real-time processing not enabled\n"
1307 unless $conf->exists('business-onlinepayment');
1309 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1312 my $bop_config = 'business-onlinepayment';
1313 $bop_config .= '-ach'
1314 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1315 my ( $processor, $login, $password, $action, @bop_options ) =
1316 $conf->config($bop_config);
1317 $action ||= 'normal authorization';
1318 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1319 die "No real-time processor is enabled - ".
1320 "did you set the business-onlinepayment configuration value?\n"
1323 ( $processor, $login, $password, $action, @bop_options )
1326 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1328 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1329 via a Business::OnlinePayment realtime gateway. See
1330 L<http://420.am/business-onlinepayment> for supported gateways.
1332 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1334 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1336 Most gateways require a reference to an original payment transaction to refund,
1337 so you probably need to specify a I<paynum>.
1339 I<amount> defaults to the original amount of the payment if not specified.
1341 I<reasonnum> specified an existing refund reason for the refund
1343 I<paydate> specifies the expiration date for a credit card overriding the
1344 value from the customer record or the payment record. Specified as yyyy-mm-dd
1346 Implementation note: If I<amount> is unspecified or equal to the amount of the
1347 orignal payment, first an attempt is made to "void" the transaction via
1348 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1349 the normal attempt is made to "refund" ("credit") the transaction via the
1350 gateway is attempted. No attempt to "void" the transaction is made if the
1351 gateway has introspection data and doesn't support void.
1353 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1354 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1355 #if set, will override the value from the customer record.
1357 #If an I<invnum> is specified, this payment (if successful) is applied to the
1358 #specified invoice. If you don't specify an I<invnum> you might want to
1359 #call the B<apply_payments> method.
1363 #some false laziness w/realtime_bop, not enough to make it worth merging
1364 #but some useful small subs should be pulled out
1365 sub realtime_refund_bop {
1368 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1371 if (ref($_[0]) eq 'HASH') {
1372 %options = %{$_[0]};
1376 $options{method} = $method;
1380 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1381 warn " $_ => $options{$_}\n" foreach keys %options;
1384 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1389 # look up the original payment and optionally a gateway for that payment
1393 my $amount = $options{'amount'};
1395 my( $processor, $login, $password, @bop_options, $namespace ) ;
1396 my( $auth, $order_number ) = ( '', '', '' );
1397 my $gatewaynum = '';
1399 if ( $options{'paynum'} ) {
1401 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1402 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1403 or return "Unknown paynum $options{'paynum'}";
1404 $amount ||= $cust_pay->paid;
1406 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1407 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1409 if ( $cust_pay->get('processor') ) {
1410 ($gatewaynum, $processor, $auth, $order_number) =
1412 $cust_pay->gatewaynum,
1413 $cust_pay->processor,
1415 $cust_pay->order_number,
1418 # this payment wasn't upgraded, which probably means this won't work,
1420 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1421 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1422 $cust_pay->paybatch;
1423 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1426 if ( $gatewaynum ) { #gateway for the payment to be refunded
1428 my $payment_gateway =
1429 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1430 die "payment gateway $gatewaynum not found"
1431 unless $payment_gateway;
1433 $processor = $payment_gateway->gateway_module;
1434 $login = $payment_gateway->gateway_username;
1435 $password = $payment_gateway->gateway_password;
1436 $namespace = $payment_gateway->gateway_namespace;
1437 @bop_options = $payment_gateway->options;
1439 } else { #try the default gateway
1442 my $payment_gateway =
1443 $self->agent->payment_gateway('method' => $options{method});
1445 ( $conf_processor, $login, $password, $namespace ) =
1446 map { my $method = "gateway_$_"; $payment_gateway->$method }
1447 qw( module username password namespace );
1449 @bop_options = $payment_gateway->gatewaynum
1450 ? $payment_gateway->options
1451 : @{ $payment_gateway->get('options') };
1453 return "processor of payment $options{'paynum'} $processor does not".
1454 " match default processor $conf_processor"
1455 unless $processor eq $conf_processor;
1460 } else { # didn't specify a paynum, so look for agent gateway overrides
1461 # like a normal transaction
1463 my $payment_gateway =
1464 $self->agent->payment_gateway( 'method' => $options{method},
1465 #'payinfo' => $payinfo,
1467 my( $processor, $login, $password, $namespace ) =
1468 map { my $method = "gateway_$_"; $payment_gateway->$method }
1469 qw( module username password namespace );
1471 my @bop_options = $payment_gateway->gatewaynum
1472 ? $payment_gateway->options
1473 : @{ $payment_gateway->get('options') };
1476 return "neither amount nor paynum specified" unless $amount;
1478 eval "use $namespace";
1483 'type' => $options{method},
1485 'password' => $password,
1486 'order_number' => $order_number,
1487 'amount' => $amount,
1489 $content{authorization} = $auth
1490 if length($auth); #echeck/ACH transactions have an order # but no auth
1491 #(at least with authorize.net)
1493 my $currency = $conf->exists('business-onlinepayment-currency')
1494 && $conf->config('business-onlinepayment-currency');
1495 $content{currency} = $currency if $currency;
1497 my $disable_void_after;
1498 if ($conf->exists('disable_void_after')
1499 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1500 $disable_void_after = $1;
1503 #first try void if applicable
1504 my $void = new Business::OnlinePayment( $processor, @bop_options );
1507 if ($void->can('info')) {
1509 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1510 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1511 my %supported_actions = $void->info('supported_actions');
1513 if ( %supported_actions && $paytype
1514 && defined($supported_actions{$paytype})
1515 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1518 if ( $cust_pay && $cust_pay->paid == $amount
1520 ( not defined($disable_void_after) )
1521 || ( time < ($cust_pay->_date + $disable_void_after ) )
1525 warn " attempting void\n" if $DEBUG > 1;
1526 if ( $void->can('info') ) {
1527 if ( $cust_pay->payby eq 'CARD'
1528 && $void->info('CC_void_requires_card') )
1530 $content{'card_number'} = $cust_pay->payinfo;
1531 } elsif ( $cust_pay->payby eq 'CHEK'
1532 && $void->info('ECHECK_void_requires_account') )
1534 ( $content{'account_number'}, $content{'routing_code'} ) =
1535 split('@', $cust_pay->payinfo);
1536 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1539 $void->content( 'action' => 'void', %content );
1540 $void->test_transaction(1)
1541 if $conf->exists('business-onlinepayment-test_transaction');
1543 if ( $void->is_success ) {
1544 # specified as a refund reason, but now we want a payment void reason
1545 # extract just the reason text, let cust_pay::void handle new_or_existing
1546 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1548 $error = 'Reason could not be loaded' unless $reason;
1549 $error = $cust_pay->void($reason->reason) unless $error;
1551 # gah, even with transactions.
1552 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1553 "error voiding payment: $error";
1557 warn " void successful\n" if $DEBUG > 1;
1562 warn " void unsuccessful, trying refund\n"
1566 my $address = $self->address1;
1567 $address .= ", ". $self->address2 if $self->address2;
1569 my($payname, $payfirst, $paylast);
1570 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1571 $payname = $self->payname;
1572 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1573 or return "Illegal payname $payname";
1574 ($payfirst, $paylast) = ($1, $2);
1576 $payfirst = $self->getfield('first');
1577 $paylast = $self->getfield('last');
1578 $payname = "$payfirst $paylast";
1581 my @invoicing_list = $self->invoicing_list_emailonly;
1582 if ( $conf->exists('emailinvoiceautoalways')
1583 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1584 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1585 push @invoicing_list, $self->all_emails;
1588 my $email = ($conf->exists('business-onlinepayment-email-override'))
1589 ? $conf->config('business-onlinepayment-email-override')
1590 : $invoicing_list[0];
1592 my $payip = exists($options{'payip'})
1595 $content{customer_ip} = $payip
1599 if ( $options{method} eq 'CC' ) {
1602 $content{card_number} = $payinfo = $cust_pay->payinfo;
1603 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1604 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1605 ($content{expiration} = "$2/$1"); # where available
1607 $content{card_number} = $payinfo = $self->payinfo;
1608 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1609 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1610 $content{expiration} = "$2/$1";
1613 } elsif ( $options{method} eq 'ECHECK' ) {
1616 $payinfo = $cust_pay->payinfo;
1618 $payinfo = $self->payinfo;
1620 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1621 $content{bank_name} = $self->payname;
1622 $content{account_type} = 'CHECKING';
1623 $content{account_name} = $payname;
1624 $content{customer_org} = $self->company ? 'B' : 'I';
1625 $content{customer_ssn} = $self->ss;
1626 } elsif ( $options{method} eq 'LEC' ) {
1627 $content{phone} = $payinfo = $self->payinfo;
1631 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1632 my %sub_content = $refund->content(
1633 'action' => 'credit',
1634 'customer_id' => $self->custnum,
1635 'last_name' => $paylast,
1636 'first_name' => $payfirst,
1638 'address' => $address,
1639 'city' => $self->city,
1640 'state' => $self->state,
1641 'zip' => $self->zip,
1642 'country' => $self->country,
1644 'phone' => $self->daytime || $self->night,
1647 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1649 $refund->test_transaction(1)
1650 if $conf->exists('business-onlinepayment-test_transaction');
1653 return "$processor error: ". $refund->error_message
1654 unless $refund->is_success();
1656 $order_number = $refund->order_number if $refund->can('order_number');
1658 # change this to just use $cust_pay->delete_cust_bill_pay?
1659 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1660 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1661 last unless @cust_bill_pay;
1662 my $cust_bill_pay = pop @cust_bill_pay;
1663 my $error = $cust_bill_pay->delete;
1667 my $cust_refund = new FS::cust_refund ( {
1668 'custnum' => $self->custnum,
1669 'paynum' => $options{'paynum'},
1670 'source_paynum' => $options{'paynum'},
1671 'refund' => $amount,
1673 'payby' => $bop_method2payby{$options{method}},
1674 'payinfo' => $payinfo,
1675 'reasonnum' => $options{'reasonnum'},
1676 'gatewaynum' => $gatewaynum, # may be null
1677 'processor' => $processor,
1678 'auth' => $refund->authorization,
1679 'order_number' => $order_number,
1681 my $error = $cust_refund->insert;
1683 $cust_refund->paynum(''); #try again with no specific paynum
1684 $cust_refund->source_paynum('');
1685 my $error2 = $cust_refund->insert;
1687 # gah, even with transactions.
1688 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1689 "error inserting refund ($processor): $error2".
1690 " (previously tried insert with paynum #$options{'paynum'}" .
1709 L<FS::cust_main>, L<FS::cust_main::Billing>