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<apply> can be set to true to run B<apply_payments_and_credits> on success.
159 I<no_auto_apply> can be set to true to prevent resulting payment from being automatically applied.
161 I<quiet> can be set true to surpress email decline notices.
163 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
164 resulting paynum, if any.
166 I<payunique> is a unique identifier for this payment.
168 I<session_id> is a session identifier associated with this payment.
170 I<depend_jobnum> allows payment capture to unlock export jobs
172 I<discount_term> attempts to take a discount by prepaying for discount_term.
173 The payment will fail if I<amount> is incorrect for this discount term.
175 A direct (Business::OnlinePayment) transaction will return nothing on success,
176 or an error message on failure.
178 A third-party transaction will return a hashref containing:
180 - popup_url: the URL to which a browser should be redirected to complete
182 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
183 - reference: a reference ID for the transaction, to show the customer.
185 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
189 # some helper routines
191 # _bop_recurring_billing: Checks whether this payment should have the
192 # recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
193 # vSecure, etc.). This works in two different modes:
194 # - actual_oncard (default): treat the payment as recurring if the customer
195 # has made a payment using this card before.
196 # - transaction_is_recur: treat the payment as recurring if the invoice
197 # being paid has any recurring package charges.
199 sub _bop_recurring_billing {
200 my( $self, %opt ) = @_;
202 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
204 if ( defined($method) && $method eq 'transaction_is_recur' ) {
206 return 1 if $opt{'trans_is_recur'};
210 # return 1 if the payinfo has been used for another payment
211 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
219 sub _payment_gateway {
220 my ($self, $options) = @_;
222 if ( $options->{'selfservice'} ) {
223 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
225 return $options->{payment_gateway} ||=
226 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
230 if ( $options->{'fake_gatewaynum'} ) {
231 $options->{payment_gateway} =
232 qsearchs('payment_gateway',
233 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
237 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
238 unless exists($options->{payment_gateway});
240 $options->{payment_gateway};
244 my ($self, $options) = @_;
247 'login' => $options->{payment_gateway}->gateway_username,
248 'password' => $options->{payment_gateway}->gateway_password,
253 my ($self, $options) = @_;
255 $options->{payment_gateway}->gatewaynum
256 ? $options->{payment_gateway}->options
257 : @{ $options->{payment_gateway}->get('options') };
262 my ($self, $options) = @_;
264 unless ( $options->{'description'} ) {
265 if ( $conf->exists('business-onlinepayment-description') ) {
266 my $dtempl = $conf->config('business-onlinepayment-description');
268 my $agent = $self->agent->agent;
270 $options->{'description'} = eval qq("$dtempl");
272 $options->{'description'} = 'Internet services';
276 unless ( exists( $options->{'payinfo'} ) ) {
277 $options->{'payinfo'} = $self->payinfo;
278 $options->{'paymask'} = $self->paymask;
281 # Default invoice number if the customer has exactly one open invoice.
282 if( ! $options->{'invnum'} ) {
283 $options->{'invnum'} = '';
284 my @open = $self->open_cust_bill;
285 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
288 $options->{payname} = $self->payname unless exists( $options->{payname} );
292 my ($self, $options) = @_;
295 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
296 $content{customer_ip} = $payip if length($payip);
298 $content{invoice_number} = $options->{'invnum'}
299 if exists($options->{'invnum'}) && length($options->{'invnum'});
301 $content{email_customer} =
302 ( $conf->exists('business-onlinepayment-email_customer')
303 || $conf->exists('business-onlinepayment-email-override') );
305 my ($payname, $payfirst, $paylast);
306 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
307 ($payname = $options->{payname}) =~
308 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
309 or return "Illegal payname $payname";
310 ($payfirst, $paylast) = ($1, $2);
312 $payfirst = $self->getfield('first');
313 $paylast = $self->getfield('last');
314 $payname = "$payfirst $paylast";
317 $content{last_name} = $paylast;
318 $content{first_name} = $payfirst;
320 $content{name} = $payname;
322 $content{address} = exists($options->{'address1'})
323 ? $options->{'address1'}
325 my $address2 = exists($options->{'address2'})
326 ? $options->{'address2'}
328 $content{address} .= ", ". $address2 if length($address2);
330 $content{city} = exists($options->{city})
333 $content{state} = exists($options->{state})
336 $content{zip} = exists($options->{zip})
339 $content{country} = exists($options->{country})
340 ? $options->{country}
343 $content{phone} = $self->daytime || $self->night;
345 my $currency = $conf->exists('business-onlinepayment-currency')
346 && $conf->config('business-onlinepayment-currency');
347 $content{currency} = $currency if $currency;
352 my %bop_method2payby = (
362 confess "Can't call realtime_bop within another transaction ".
363 '($FS::UID::AutoCommit is false)'
364 unless $FS::UID::AutoCommit;
366 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
369 if (ref($_[0]) eq 'HASH') {
372 my ( $method, $amount ) = ( shift, shift );
374 $options{method} = $method;
375 $options{amount} = $amount;
380 # optional credit card surcharge
383 my $cc_surcharge = 0;
384 my $cc_surcharge_pct = 0;
385 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
386 if $conf->config('credit-card-surcharge-percentage')
387 && $options{method} eq 'CC';
389 # always add cc surcharge if called from event
390 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
391 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
392 $options{'amount'} += $cc_surcharge;
393 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
395 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
396 # payment screen), so consider the given
397 # amount as post-surcharge
398 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
401 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
402 $options{'cc_surcharge'} = $cc_surcharge;
406 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
407 warn " cc_surcharge = $cc_surcharge\n";
410 warn " $_ => $options{$_}\n" foreach keys %options;
413 return $self->fake_bop(\%options) if $options{'fake'};
415 $self->_bop_defaults(\%options);
418 # set trans_is_recur based on invnum if there is one
421 my $trans_is_recur = 0;
422 if ( $options{'invnum'} ) {
424 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
425 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
431 $cust_bill->cust_bill_pkg;
434 if grep { $_->freq ne '0' } @part_pkg;
442 my $payment_gateway = $self->_payment_gateway( \%options );
443 my $namespace = $payment_gateway->gateway_namespace;
445 eval "use $namespace";
449 # check for banned credit card/ACH
452 my $ban = FS::banned_pay->ban_search(
453 'payby' => $bop_method2payby{$options{method}},
454 'payinfo' => $options{payinfo},
456 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
459 # check for term discount validity
462 my $discount_term = $options{discount_term};
463 if ( $discount_term ) {
464 my $bill = ($self->cust_bill)[-1]
465 or return "Can't apply a term discount to an unbilled customer";
466 my $plan = FS::discount_plan->new(
468 months => $discount_term
469 ) or return "No discount available for term '$discount_term'";
471 if ( $plan->discounted_total != $options{amount} ) {
472 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
480 my $bop_content = $self->_bop_content(\%options);
481 return $bop_content unless ref($bop_content);
483 my @invoicing_list = $self->invoicing_list_emailonly;
484 if ( $conf->exists('emailinvoiceautoalways')
485 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
486 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
487 push @invoicing_list, $self->all_emails;
490 my $email = ($conf->exists('business-onlinepayment-email-override'))
491 ? $conf->config('business-onlinepayment-email-override')
492 : $invoicing_list[0];
497 if ( $namespace eq 'Business::OnlinePayment' ) {
499 if ( $options{method} eq 'CC' ) {
501 $content{card_number} = $options{payinfo};
502 $paydate = exists($options{'paydate'})
503 ? $options{'paydate'}
505 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
506 $content{expiration} = "$2/$1";
508 my $paycvv = exists($options{'paycvv'})
511 $content{cvv2} = $paycvv
514 my $paystart_month = exists($options{'paystart_month'})
515 ? $options{'paystart_month'}
516 : $self->paystart_month;
518 my $paystart_year = exists($options{'paystart_year'})
519 ? $options{'paystart_year'}
520 : $self->paystart_year;
522 $content{card_start} = "$paystart_month/$paystart_year"
523 if $paystart_month && $paystart_year;
525 my $payissue = exists($options{'payissue'})
526 ? $options{'payissue'}
528 $content{issue_number} = $payissue if $payissue;
530 if ( $self->_bop_recurring_billing(
531 'payinfo' => $options{'payinfo'},
532 'trans_is_recur' => $trans_is_recur,
536 $content{recurring_billing} = 'YES';
537 $content{acct_code} = 'rebill'
538 if $conf->exists('credit_card-recurring_billing_acct_code');
541 } elsif ( $options{method} eq 'ECHECK' ){
543 ( $content{account_number}, $content{routing_code} ) =
544 split('@', $options{payinfo});
545 $content{bank_name} = $options{payname};
546 $content{bank_state} = exists($options{'paystate'})
547 ? $options{'paystate'}
548 : $self->getfield('paystate');
549 $content{account_type}=
550 (exists($options{'paytype'}) && $options{'paytype'})
551 ? uc($options{'paytype'})
552 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
554 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
555 $content{account_name} = $self->company;
557 $content{account_name} = $self->getfield('first'). ' '.
558 $self->getfield('last');
561 $content{customer_org} = $self->company ? 'B' : 'I';
562 $content{state_id} = exists($options{'stateid'})
563 ? $options{'stateid'}
564 : $self->getfield('stateid');
565 $content{state_id_state} = exists($options{'stateid_state'})
566 ? $options{'stateid_state'}
567 : $self->getfield('stateid_state');
568 $content{customer_ssn} = exists($options{'ss'})
572 } elsif ( $options{method} eq 'LEC' ) {
573 $content{phone} = $options{payinfo};
575 die "unknown method ". $options{method};
578 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
581 die "unknown namespace $namespace";
588 my $balance = exists( $options{'balance'} )
589 ? $options{'balance'}
592 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
593 $self->select_for_update; #mutex ... just until we get our pending record in
594 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
596 #the checks here are intended to catch concurrent payments
597 #double-form-submission prevention is taken care of in cust_pay_pending::check
600 return "The customer's balance has changed; $options{method} transaction aborted."
601 if $self->balance < $balance;
603 #also check and make sure there aren't *other* pending payments for this cust
605 my @pending = qsearch('cust_pay_pending', {
606 'custnum' => $self->custnum,
607 'status' => { op=>'!=', value=>'done' }
610 #for third-party payments only, remove pending payments if they're in the
611 #'thirdparty' (waiting for customer action) state.
612 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
613 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
614 my $error = $_->delete;
615 warn "error deleting unfinished third-party payment ".
616 $_->paypendingnum . ": $error\n"
619 @pending = grep { $_->status ne 'thirdparty' } @pending;
622 return "A payment is already being processed for this customer (".
623 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
624 "); $options{method} transaction aborted."
627 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
629 my $cust_pay_pending = new FS::cust_pay_pending {
630 'custnum' => $self->custnum,
631 'paid' => $options{amount},
633 'payby' => $bop_method2payby{$options{method}},
634 'payinfo' => $options{payinfo},
635 'paymask' => $options{paymask},
636 'paydate' => $paydate,
637 'recurring_billing' => $content{recurring_billing},
638 'pkgnum' => $options{'pkgnum'},
640 'gatewaynum' => $payment_gateway->gatewaynum || '',
641 'session_id' => $options{session_id} || '',
642 'jobnum' => $options{depend_jobnum} || '',
644 $cust_pay_pending->payunique( $options{payunique} )
645 if defined($options{payunique}) && length($options{payunique});
647 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
649 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
650 return $cpp_new_err if $cpp_new_err;
652 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
654 warn Dumper($cust_pay_pending) if $DEBUG > 2;
656 my( $action1, $action2 ) =
657 split( /\s*\,\s*/, $payment_gateway->gateway_action );
659 my $transaction = new $namespace( $payment_gateway->gateway_module,
660 $self->_bop_options(\%options),
663 $transaction->content(
664 'type' => $options{method},
665 $self->_bop_auth(\%options),
666 'action' => $action1,
667 'description' => $options{'description'},
668 'amount' => $options{amount},
669 #'invoice_number' => $options{'invnum'},
670 'customer_id' => $self->custnum,
672 'reference' => $cust_pay_pending->paypendingnum, #for now
673 'callback_url' => $payment_gateway->gateway_callback_url,
674 'cancel_url' => $payment_gateway->gateway_cancel_url,
679 $cust_pay_pending->status('pending');
680 my $cpp_pending_err = $cust_pay_pending->replace;
681 return $cpp_pending_err if $cpp_pending_err;
683 warn Dumper($transaction) if $DEBUG > 2;
685 unless ( $BOP_TESTING ) {
686 $transaction->test_transaction(1)
687 if $conf->exists('business-onlinepayment-test_transaction');
688 $transaction->submit();
690 if ( $BOP_TESTING_SUCCESS ) {
691 $transaction->is_success(1);
692 $transaction->authorization('fake auth');
694 $transaction->is_success(0);
695 $transaction->error_message('fake failure');
699 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
701 $cust_pay_pending->status('thirdparty');
702 my $cpp_err = $cust_pay_pending->replace;
703 return { error => $cpp_err } if $cpp_err;
704 return { reference => $cust_pay_pending->paypendingnum,
705 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
707 } elsif ( $transaction->is_success() && $action2 ) {
709 $cust_pay_pending->status('authorized');
710 my $cpp_authorized_err = $cust_pay_pending->replace;
711 return $cpp_authorized_err if $cpp_authorized_err;
713 my $auth = $transaction->authorization;
714 my $ordernum = $transaction->can('order_number')
715 ? $transaction->order_number
719 new Business::OnlinePayment( $payment_gateway->gateway_module,
720 $self->_bop_options(\%options),
725 type => $options{method},
727 $self->_bop_auth(\%options),
728 order_number => $ordernum,
729 amount => $options{amount},
730 authorization => $auth,
731 description => $options{'description'},
734 foreach my $field (qw( authorization_source_code returned_ACI
735 transaction_identifier validation_code
736 transaction_sequence_num local_transaction_date
737 local_transaction_time AVS_result_code )) {
738 $capture{$field} = $transaction->$field() if $transaction->can($field);
741 $capture->content( %capture );
743 $capture->test_transaction(1)
744 if $conf->exists('business-onlinepayment-test_transaction');
747 unless ( $capture->is_success ) {
748 my $e = "Authorization successful but capture failed, custnum #".
749 $self->custnum. ': '. $capture->result_code.
750 ": ". $capture->error_message;
758 # remove paycvv after initial transaction
761 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
762 if ( length($self->paycvv)
763 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
765 my $error = $self->remove_cvv;
767 warn "WARNING: error removing cvv: $error\n";
776 if ( $transaction->can('card_token') && $transaction->card_token ) {
778 if ( $options{'payinfo'} eq $self->payinfo ) {
779 $self->payinfo($transaction->card_token);
780 my $error = $self->replace;
782 warn "WARNING: error storing token: $error, but proceeding anyway\n";
792 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
804 if (ref($_[0]) eq 'HASH') {
807 my ( $method, $amount ) = ( shift, shift );
809 $options{method} = $method;
810 $options{amount} = $amount;
813 if ( $options{'fake_failure'} ) {
814 return "Error: No error; test failure requested with fake_failure";
817 my $cust_pay = new FS::cust_pay ( {
818 'custnum' => $self->custnum,
819 'invnum' => $options{'invnum'},
820 'paid' => $options{amount},
822 'payby' => $bop_method2payby{$options{method}},
823 #'payinfo' => $payinfo,
824 'payinfo' => '4111111111111111',
825 #'paydate' => $paydate,
826 'paydate' => '2012-05-01',
827 'processor' => 'FakeProcessor',
829 'order_number' => '32',
831 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
834 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
835 warn " $_ => $options{$_}\n" foreach keys %options;
838 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
841 $cust_pay->invnum(''); #try again with no specific invnum
842 my $error2 = $cust_pay->insert( $options{'manual'} ?
843 ( 'manual' => 1 ) : ()
846 # gah, even with transactions.
847 my $e = 'WARNING: Card/ACH debited but database not updated - '.
848 "error inserting (fake!) payment: $error2".
849 " (previously tried insert with invnum #$options{'invnum'}" .
856 if ( $options{'paynum_ref'} ) {
857 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
865 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
867 # Wraps up processing of a realtime credit card, ACH (electronic check) or
868 # phone bill transaction.
870 sub _realtime_bop_result {
871 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
873 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
876 warn "$me _realtime_bop_result: pending transaction ".
877 $cust_pay_pending->paypendingnum. "\n";
878 warn " $_ => $options{$_}\n" foreach keys %options;
881 my $payment_gateway = $options{payment_gateway}
882 or return "no payment gateway in arguments to _realtime_bop_result";
884 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
885 my $cpp_captured_err = $cust_pay_pending->replace;
886 return $cpp_captured_err if $cpp_captured_err;
888 if ( $transaction->is_success() ) {
890 my $order_number = $transaction->order_number
891 if $transaction->can('order_number');
893 my $cust_pay = new FS::cust_pay ( {
894 'custnum' => $self->custnum,
895 'invnum' => $options{'invnum'},
896 'paid' => $cust_pay_pending->paid,
898 'payby' => $cust_pay_pending->payby,
899 'payinfo' => $options{'payinfo'},
900 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
901 'paydate' => $cust_pay_pending->paydate,
902 'pkgnum' => $cust_pay_pending->pkgnum,
903 'discount_term' => $options{'discount_term'},
904 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
905 'processor' => $payment_gateway->gateway_module,
906 'auth' => $transaction->authorization,
907 'order_number' => $order_number || '',
908 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
910 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
911 $cust_pay->payunique( $options{payunique} )
912 if defined($options{payunique}) && length($options{payunique});
914 my $oldAutoCommit = $FS::UID::AutoCommit;
915 local $FS::UID::AutoCommit = 0;
918 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
920 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
923 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
924 $cust_pay->invnum(''); #try again with no specific invnum
925 $cust_pay->paynum('');
926 my $error2 = $cust_pay->insert( $options{'manual'} ?
927 ( 'manual' => 1 ) : ()
930 # gah. but at least we have a record of the state we had to abort in
931 # from cust_pay_pending now.
932 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
933 my $e = "WARNING: $options{method} captured but payment not recorded -".
934 " error inserting payment (". $payment_gateway->gateway_module.
936 " (previously tried insert with invnum #$options{'invnum'}" .
937 ": $error ) - pending payment saved as paypendingnum ".
938 $cust_pay_pending->paypendingnum. "\n";
944 my $jobnum = $cust_pay_pending->jobnum;
946 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
948 unless ( $placeholder ) {
949 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
950 my $e = "WARNING: $options{method} captured but job $jobnum not ".
951 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
956 $error = $placeholder->delete;
959 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
960 my $e = "WARNING: $options{method} captured but could not delete ".
961 "job $jobnum for paypendingnum ".
962 $cust_pay_pending->paypendingnum. ": $error\n";
967 $cust_pay_pending->set('jobnum','');
971 if ( $options{'paynum_ref'} ) {
972 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
975 $cust_pay_pending->status('done');
976 $cust_pay_pending->statustext('captured');
977 $cust_pay_pending->paynum($cust_pay->paynum);
978 my $cpp_done_err = $cust_pay_pending->replace;
980 if ( $cpp_done_err ) {
982 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
983 my $e = "WARNING: $options{method} captured but payment not recorded - ".
984 "error updating status for paypendingnum ".
985 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
991 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
993 if ( $options{'apply'} ) {
994 my $apply_error = $self->apply_payments_and_credits;
995 if ( $apply_error ) {
996 warn "WARNING: error applying payment: $apply_error\n";
997 #but we still should return no error cause the payment otherwise went
1002 # have a CC surcharge portion --> one-time charge
1003 if ( $options{'cc_surcharge'} > 0 ) {
1004 # XXX: this whole block needs to be in a transaction?
1007 $invnum = $options{'invnum'} if $options{'invnum'};
1008 unless ( $invnum ) { # probably from a payment screen
1009 # do we have any open invoices? pick earliest
1010 # uses the fact that cust_main->cust_bill sorts by date ascending
1011 my @open = $self->open_cust_bill;
1012 $invnum = $open[0]->invnum if scalar(@open);
1015 unless ( $invnum ) { # still nothing? pick last closed invoice
1016 # again uses fact that cust_main->cust_bill sorts by date ascending
1017 my @closed = $self->cust_bill;
1018 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1021 unless ( $invnum ) {
1022 # XXX: unlikely case - pre-paying before any invoices generated
1023 # what it should do is create a new invoice and pick it
1024 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1029 my $charge_error = $self->charge({
1030 'amount' => $options{'cc_surcharge'},
1031 'pkg' => 'Credit Card Surcharge',
1033 'cust_pkg_ref' => \$cust_pkg,
1036 warn 'Unable to add CC surcharge cust_pkg';
1040 $cust_pkg->setup(time);
1041 my $cp_error = $cust_pkg->replace;
1043 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1047 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1048 unless ( $cust_bill ) {
1049 warn "race condition + invoice deletion just happened";
1054 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1056 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1060 return ''; #no error
1066 my $perror = $transaction->error_message;
1067 #$payment_gateway->gateway_module. " error: ".
1068 # removed for conciseness
1070 my $jobnum = $cust_pay_pending->jobnum;
1072 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1074 if ( $placeholder ) {
1075 my $error = $placeholder->depended_delete;
1076 $error ||= $placeholder->delete;
1077 $cust_pay_pending->set('jobnum','');
1078 warn "error removing provisioning jobs after declined paypendingnum ".
1079 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1081 my $e = "error finding job $jobnum for declined paypendingnum ".
1082 $cust_pay_pending->paypendingnum. "\n";
1088 unless ( $transaction->error_message ) {
1091 if ( $transaction->can('response_page') ) {
1093 'page' => ( $transaction->can('response_page')
1094 ? $transaction->response_page
1097 'code' => ( $transaction->can('response_code')
1098 ? $transaction->response_code
1101 'headers' => ( $transaction->can('response_headers')
1102 ? $transaction->response_headers
1108 "No additional debugging information available for ".
1109 $payment_gateway->gateway_module;
1112 $perror .= "No error_message returned from ".
1113 $payment_gateway->gateway_module. " -- ".
1114 ( ref($t_response) ? Dumper($t_response) : $t_response );
1118 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1119 && $conf->exists('emaildecline', $self->agentnum)
1120 && grep { $_ ne 'POST' } $self->invoicing_list
1121 && ! grep { $transaction->error_message =~ /$_/ }
1122 $conf->config('emaildecline-exclude', $self->agentnum)
1125 # Send a decline alert to the customer.
1126 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1129 # include the raw error message in the transaction state
1130 $cust_pay_pending->setfield('error', $transaction->error_message);
1131 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1132 $error = $msg_template->send( 'cust_main' => $self,
1133 'object' => $cust_pay_pending );
1137 $perror .= " (also received error sending decline notification: $error)"
1142 $cust_pay_pending->status('done');
1143 $cust_pay_pending->statustext($perror);
1144 #'declined:': no, that's failure_status
1145 if ( $transaction->can('failure_status') ) {
1146 $cust_pay_pending->failure_status( $transaction->failure_status );
1148 my $cpp_done_err = $cust_pay_pending->replace;
1149 if ( $cpp_done_err ) {
1150 my $e = "WARNING: $options{method} declined but pending payment not ".
1151 "resolved - error updating status for paypendingnum ".
1152 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1154 $perror = "$e ($perror)";
1162 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1164 Verifies successful third party processing of a realtime credit card,
1165 ACH (electronic check) or phone bill transaction via a
1166 Business::OnlineThirdPartyPayment realtime gateway. See
1167 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1169 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1171 The additional options I<payname>, I<city>, I<state>,
1172 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1173 if set, will override the value from the customer record.
1175 I<description> is a free-text field passed to the gateway. It defaults to
1176 "Internet services".
1178 If an I<invnum> is specified, this payment (if successful) is applied to the
1179 specified invoice. If you don't specify an I<invnum> you might want to
1180 call the B<apply_payments> method.
1182 I<quiet> can be set true to surpress email decline notices.
1184 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1185 resulting paynum, if any.
1187 I<payunique> is a unique identifier for this payment.
1189 Returns a hashref containing elements bill_error (which will be undefined
1190 upon success) and session_id of any associated session.
1194 sub realtime_botpp_capture {
1195 my( $self, $cust_pay_pending, %options ) = @_;
1197 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1200 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1201 warn " $_ => $options{$_}\n" foreach keys %options;
1204 eval "use Business::OnlineThirdPartyPayment";
1208 # select the gateway
1211 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1213 my $payment_gateway;
1214 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1215 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1216 { gatewaynum => $gatewaynum }
1218 : $self->agent->payment_gateway( 'method' => $method,
1219 # 'invnum' => $cust_pay_pending->invnum,
1220 # 'payinfo' => $cust_pay_pending->payinfo,
1223 $options{payment_gateway} = $payment_gateway; # for the helper subs
1229 my @invoicing_list = $self->invoicing_list_emailonly;
1230 if ( $conf->exists('emailinvoiceautoalways')
1231 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1232 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1233 push @invoicing_list, $self->all_emails;
1236 my $email = ($conf->exists('business-onlinepayment-email-override'))
1237 ? $conf->config('business-onlinepayment-email-override')
1238 : $invoicing_list[0];
1242 $content{email_customer} =
1243 ( $conf->exists('business-onlinepayment-email_customer')
1244 || $conf->exists('business-onlinepayment-email-override') );
1247 # run transaction(s)
1251 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1252 $self->_bop_options(\%options),
1255 $transaction->reference({ %options });
1257 $transaction->content(
1259 $self->_bop_auth(\%options),
1260 'action' => 'Post Authorization',
1261 'description' => $options{'description'},
1262 'amount' => $cust_pay_pending->paid,
1263 #'invoice_number' => $options{'invnum'},
1264 'customer_id' => $self->custnum,
1265 'reference' => $cust_pay_pending->paypendingnum,
1267 'phone' => $self->daytime || $self->night,
1269 # plus whatever is required for bogus capture avoidance
1272 $transaction->submit();
1275 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1277 if ( $options{'apply'} ) {
1278 my $apply_error = $self->apply_payments_and_credits;
1279 if ( $apply_error ) {
1280 warn "WARNING: error applying payment: $apply_error\n";
1285 bill_error => $error,
1286 session_id => $cust_pay_pending->session_id,
1291 =item default_payment_gateway
1293 DEPRECATED -- use agent->payment_gateway
1297 sub default_payment_gateway {
1298 my( $self, $method ) = @_;
1300 die "Real-time processing not enabled\n"
1301 unless $conf->exists('business-onlinepayment');
1303 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1306 my $bop_config = 'business-onlinepayment';
1307 $bop_config .= '-ach'
1308 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1309 my ( $processor, $login, $password, $action, @bop_options ) =
1310 $conf->config($bop_config);
1311 $action ||= 'normal authorization';
1312 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1313 die "No real-time processor is enabled - ".
1314 "did you set the business-onlinepayment configuration value?\n"
1317 ( $processor, $login, $password, $action, @bop_options )
1320 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1322 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1323 via a Business::OnlinePayment realtime gateway. See
1324 L<http://420.am/business-onlinepayment> for supported gateways.
1326 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1328 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1330 Most gateways require a reference to an original payment transaction to refund,
1331 so you probably need to specify a I<paynum>.
1333 I<amount> defaults to the original amount of the payment if not specified.
1335 I<reasonnum> specified an existing refund reason for the refund
1337 I<paydate> specifies the expiration date for a credit card overriding the
1338 value from the customer record or the payment record. Specified as yyyy-mm-dd
1340 Implementation note: If I<amount> is unspecified or equal to the amount of the
1341 orignal payment, first an attempt is made to "void" the transaction via
1342 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1343 the normal attempt is made to "refund" ("credit") the transaction via the
1344 gateway is attempted. No attempt to "void" the transaction is made if the
1345 gateway has introspection data and doesn't support void.
1347 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1348 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1349 #if set, will override the value from the customer record.
1351 #If an I<invnum> is specified, this payment (if successful) is applied to the
1352 #specified invoice. If you don't specify an I<invnum> you might want to
1353 #call the B<apply_payments> method.
1357 #some false laziness w/realtime_bop, not enough to make it worth merging
1358 #but some useful small subs should be pulled out
1359 sub realtime_refund_bop {
1362 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1365 if (ref($_[0]) eq 'HASH') {
1366 %options = %{$_[0]};
1370 $options{method} = $method;
1374 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1375 warn " $_ => $options{$_}\n" foreach keys %options;
1378 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1383 # look up the original payment and optionally a gateway for that payment
1387 my $amount = $options{'amount'};
1389 my( $processor, $login, $password, @bop_options, $namespace ) ;
1390 my( $auth, $order_number ) = ( '', '', '' );
1391 my $gatewaynum = '';
1393 if ( $options{'paynum'} ) {
1395 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1396 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1397 or return "Unknown paynum $options{'paynum'}";
1398 $amount ||= $cust_pay->paid;
1400 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1401 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1403 if ( $cust_pay->get('processor') ) {
1404 ($gatewaynum, $processor, $auth, $order_number) =
1406 $cust_pay->gatewaynum,
1407 $cust_pay->processor,
1409 $cust_pay->order_number,
1412 # this payment wasn't upgraded, which probably means this won't work,
1414 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1415 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1416 $cust_pay->paybatch;
1417 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1420 if ( $gatewaynum ) { #gateway for the payment to be refunded
1422 my $payment_gateway =
1423 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1424 die "payment gateway $gatewaynum not found"
1425 unless $payment_gateway;
1427 $processor = $payment_gateway->gateway_module;
1428 $login = $payment_gateway->gateway_username;
1429 $password = $payment_gateway->gateway_password;
1430 $namespace = $payment_gateway->gateway_namespace;
1431 @bop_options = $payment_gateway->options;
1433 } else { #try the default gateway
1436 my $payment_gateway =
1437 $self->agent->payment_gateway('method' => $options{method});
1439 ( $conf_processor, $login, $password, $namespace ) =
1440 map { my $method = "gateway_$_"; $payment_gateway->$method }
1441 qw( module username password namespace );
1443 @bop_options = $payment_gateway->gatewaynum
1444 ? $payment_gateway->options
1445 : @{ $payment_gateway->get('options') };
1447 return "processor of payment $options{'paynum'} $processor does not".
1448 " match default processor $conf_processor"
1449 unless $processor eq $conf_processor;
1454 } else { # didn't specify a paynum, so look for agent gateway overrides
1455 # like a normal transaction
1457 my $payment_gateway =
1458 $self->agent->payment_gateway( 'method' => $options{method},
1459 #'payinfo' => $payinfo,
1461 my( $processor, $login, $password, $namespace ) =
1462 map { my $method = "gateway_$_"; $payment_gateway->$method }
1463 qw( module username password namespace );
1465 my @bop_options = $payment_gateway->gatewaynum
1466 ? $payment_gateway->options
1467 : @{ $payment_gateway->get('options') };
1470 return "neither amount nor paynum specified" unless $amount;
1472 eval "use $namespace";
1477 'type' => $options{method},
1479 'password' => $password,
1480 'order_number' => $order_number,
1481 'amount' => $amount,
1483 $content{authorization} = $auth
1484 if length($auth); #echeck/ACH transactions have an order # but no auth
1485 #(at least with authorize.net)
1487 my $currency = $conf->exists('business-onlinepayment-currency')
1488 && $conf->config('business-onlinepayment-currency');
1489 $content{currency} = $currency if $currency;
1491 my $disable_void_after;
1492 if ($conf->exists('disable_void_after')
1493 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1494 $disable_void_after = $1;
1497 #first try void if applicable
1498 my $void = new Business::OnlinePayment( $processor, @bop_options );
1501 if ($void->can('info')) {
1503 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1504 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1505 my %supported_actions = $void->info('supported_actions');
1507 if ( %supported_actions && $paytype
1508 && defined($supported_actions{$paytype})
1509 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1512 if ( $cust_pay && $cust_pay->paid == $amount
1514 ( not defined($disable_void_after) )
1515 || ( time < ($cust_pay->_date + $disable_void_after ) )
1519 warn " attempting void\n" if $DEBUG > 1;
1520 if ( $void->can('info') ) {
1521 if ( $cust_pay->payby eq 'CARD'
1522 && $void->info('CC_void_requires_card') )
1524 $content{'card_number'} = $cust_pay->payinfo;
1525 } elsif ( $cust_pay->payby eq 'CHEK'
1526 && $void->info('ECHECK_void_requires_account') )
1528 ( $content{'account_number'}, $content{'routing_code'} ) =
1529 split('@', $cust_pay->payinfo);
1530 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1533 $void->content( 'action' => 'void', %content );
1534 $void->test_transaction(1)
1535 if $conf->exists('business-onlinepayment-test_transaction');
1537 if ( $void->is_success ) {
1538 # specified as a refund reason, but now we want a payment void reason
1539 # extract just the reason text, let cust_pay::void handle new_or_existing
1540 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1542 $error = 'Reason could not be loaded' unless $reason;
1543 $error = $cust_pay->void($reason->reason) unless $error;
1545 # gah, even with transactions.
1546 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1547 "error voiding payment: $error";
1551 warn " void successful\n" if $DEBUG > 1;
1556 warn " void unsuccessful, trying refund\n"
1560 my $address = $self->address1;
1561 $address .= ", ". $self->address2 if $self->address2;
1563 my($payname, $payfirst, $paylast);
1564 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1565 $payname = $self->payname;
1566 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1567 or return "Illegal payname $payname";
1568 ($payfirst, $paylast) = ($1, $2);
1570 $payfirst = $self->getfield('first');
1571 $paylast = $self->getfield('last');
1572 $payname = "$payfirst $paylast";
1575 my @invoicing_list = $self->invoicing_list_emailonly;
1576 if ( $conf->exists('emailinvoiceautoalways')
1577 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1578 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1579 push @invoicing_list, $self->all_emails;
1582 my $email = ($conf->exists('business-onlinepayment-email-override'))
1583 ? $conf->config('business-onlinepayment-email-override')
1584 : $invoicing_list[0];
1586 my $payip = exists($options{'payip'})
1589 $content{customer_ip} = $payip
1593 if ( $options{method} eq 'CC' ) {
1596 $content{card_number} = $payinfo = $cust_pay->payinfo;
1597 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1598 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1599 ($content{expiration} = "$2/$1"); # where available
1601 $content{card_number} = $payinfo = $self->payinfo;
1602 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1603 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1604 $content{expiration} = "$2/$1";
1607 } elsif ( $options{method} eq 'ECHECK' ) {
1610 $payinfo = $cust_pay->payinfo;
1612 $payinfo = $self->payinfo;
1614 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1615 $content{bank_name} = $self->payname;
1616 $content{account_type} = 'CHECKING';
1617 $content{account_name} = $payname;
1618 $content{customer_org} = $self->company ? 'B' : 'I';
1619 $content{customer_ssn} = $self->ss;
1620 } elsif ( $options{method} eq 'LEC' ) {
1621 $content{phone} = $payinfo = $self->payinfo;
1625 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1626 my %sub_content = $refund->content(
1627 'action' => 'credit',
1628 'customer_id' => $self->custnum,
1629 'last_name' => $paylast,
1630 'first_name' => $payfirst,
1632 'address' => $address,
1633 'city' => $self->city,
1634 'state' => $self->state,
1635 'zip' => $self->zip,
1636 'country' => $self->country,
1638 'phone' => $self->daytime || $self->night,
1641 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1643 $refund->test_transaction(1)
1644 if $conf->exists('business-onlinepayment-test_transaction');
1647 return "$processor error: ". $refund->error_message
1648 unless $refund->is_success();
1650 $order_number = $refund->order_number if $refund->can('order_number');
1652 # change this to just use $cust_pay->delete_cust_bill_pay?
1653 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1654 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1655 last unless @cust_bill_pay;
1656 my $cust_bill_pay = pop @cust_bill_pay;
1657 my $error = $cust_bill_pay->delete;
1661 my $cust_refund = new FS::cust_refund ( {
1662 'custnum' => $self->custnum,
1663 'paynum' => $options{'paynum'},
1664 'source_paynum' => $options{'paynum'},
1665 'refund' => $amount,
1667 'payby' => $bop_method2payby{$options{method}},
1668 'payinfo' => $payinfo,
1669 'reasonnum' => $options{'reasonnum'},
1670 'gatewaynum' => $gatewaynum, # may be null
1671 'processor' => $processor,
1672 'auth' => $refund->authorization,
1673 'order_number' => $order_number,
1675 my $error = $cust_refund->insert;
1677 $cust_refund->paynum(''); #try again with no specific paynum
1678 $cust_refund->source_paynum('');
1679 my $error2 = $cust_refund->insert;
1681 # gah, even with transactions.
1682 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1683 "error inserting refund ($processor): $error2".
1684 " (previously tried insert with paynum #$options{'paynum'}" .
1703 L<FS::cust_main>, L<FS::cust_main::Billing>