1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.28;
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
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_collect [ OPTION => VALUE ... ]
50 Attempt to collect the customer's current balance with a realtime credit
51 card, electronic check, or phone bill transaction (see realtime_bop() below).
53 Returns the result of realtime_bop(): nothing, an error message, or a
54 hashref of state information for a third-party transaction.
56 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
58 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
59 then it is deduced from the customer record.
61 If no I<amount> is specified, then the customer balance is used.
63 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
64 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
65 if set, will override the value from the customer record.
67 I<description> is a free-text field passed to the gateway. It defaults to
68 the value defined by the business-onlinepayment-description configuration
69 option, or "Internet services" if that is unset.
71 If an I<invnum> is specified, this payment (if successful) is applied to the
74 I<apply> will automatically apply a resulting payment.
76 I<quiet> can be set true to suppress email decline notices.
78 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
79 resulting paynum, if any.
81 I<payunique> is a unique identifier for this payment.
83 I<session_id> is a session identifier associated with this payment.
85 I<depend_jobnum> allows payment capture to unlock export jobs
89 sub realtime_collect {
90 my( $self, %options ) = @_;
92 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
95 warn "$me realtime_collect:\n";
96 warn " $_ => $options{$_}\n" foreach keys %options;
99 $options{amount} = $self->balance unless exists( $options{amount} );
100 $options{method} = FS::payby->payby2bop($self->payby)
101 unless exists( $options{method} );
103 return $self->realtime_bop({%options});
107 =item realtime_bop { [ ARG => VALUE ... ] }
109 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
110 via a Business::OnlinePayment realtime gateway. See
111 L<http://420.am/business-onlinepayment> for supported gateways.
113 Required arguments in the hashref are I<method>, and I<amount>
115 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
117 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
119 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
120 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
121 if set, will override the value from the customer record.
123 I<description> is a free-text field passed to the gateway. It defaults to
124 the value defined by the business-onlinepayment-description configuration
125 option, or "Internet services" if that is unset.
127 If an I<invnum> is specified, this payment (if successful) is applied to the
128 specified invoice. If the customer has exactly one open invoice, that
129 invoice number will be assumed. If you don't specify an I<invnum> you might
130 want to call the B<apply_payments> method or set the I<apply> option.
132 I<no_invnum> can be set to true to prevent that default invnum from being set.
134 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
136 I<no_auto_apply> can be set to true to set that flag on the resulting payment
137 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
138 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
140 I<quiet> can be set true to surpress email decline notices.
142 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
143 resulting paynum, if any.
145 I<payunique> is a unique identifier for this payment.
147 I<session_id> is a session identifier associated with this payment.
149 I<depend_jobnum> allows payment capture to unlock export jobs
151 I<discount_term> attempts to take a discount by prepaying for discount_term.
152 The payment will fail if I<amount> is incorrect for this discount term.
154 A direct (Business::OnlinePayment) transaction will return nothing on success,
155 or an error message on failure.
157 A third-party transaction will return a hashref containing:
159 - popup_url: the URL to which a browser should be redirected to complete
161 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
162 - reference: a reference ID for the transaction, to show the customer.
164 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
168 # some helper routines
169 sub _bop_recurring_billing {
170 my( $self, %opt ) = @_;
172 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
174 if ( defined($method) && $method eq 'transaction_is_recur' ) {
176 return 1 if $opt{'trans_is_recur'};
180 # return 1 if the payinfo has been used for another payment
181 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
189 sub _payment_gateway {
190 my ($self, $options) = @_;
192 if ( $options->{'selfservice'} ) {
193 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
195 return $options->{payment_gateway} ||=
196 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
200 if ( $options->{'fake_gatewaynum'} ) {
201 $options->{payment_gateway} =
202 qsearchs('payment_gateway',
203 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
207 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
208 unless exists($options->{payment_gateway});
210 $options->{payment_gateway};
214 my ($self, $options) = @_;
217 'login' => $options->{payment_gateway}->gateway_username,
218 'password' => $options->{payment_gateway}->gateway_password,
223 my ($self, $options) = @_;
225 $options->{payment_gateway}->gatewaynum
226 ? $options->{payment_gateway}->options
227 : @{ $options->{payment_gateway}->get('options') };
232 my ($self, $options) = @_;
234 unless ( $options->{'description'} ) {
235 if ( $conf->exists('business-onlinepayment-description') ) {
236 my $dtempl = $conf->config('business-onlinepayment-description');
238 my $agent = $self->agent->agent;
240 $options->{'description'} = eval qq("$dtempl");
242 $options->{'description'} = 'Internet services';
246 unless ( exists( $options->{'payinfo'} ) ) {
247 $options->{'payinfo'} = $self->payinfo;
248 $options->{'paymask'} = $self->paymask;
251 # Default invoice number if the customer has exactly one open invoice.
252 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
253 $options->{'invnum'} = '';
254 my @open = $self->open_cust_bill;
255 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
258 $options->{payname} = $self->payname unless exists( $options->{payname} );
262 my ($self, $options) = @_;
265 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
266 $content{customer_ip} = $payip if length($payip);
268 $content{invoice_number} = $options->{'invnum'}
269 if exists($options->{'invnum'}) && length($options->{'invnum'});
271 $content{email_customer} =
272 ( $conf->exists('business-onlinepayment-email_customer')
273 || $conf->exists('business-onlinepayment-email-override') );
275 my ($payname, $payfirst, $paylast);
276 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
277 ($payname = $options->{payname}) =~
278 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
279 or return "Illegal payname $payname";
280 ($payfirst, $paylast) = ($1, $2);
282 $payfirst = $self->getfield('first');
283 $paylast = $self->getfield('last');
284 $payname = "$payfirst $paylast";
287 $content{last_name} = $paylast;
288 $content{first_name} = $payfirst;
290 $content{name} = $payname;
292 $content{address} = exists($options->{'address1'})
293 ? $options->{'address1'}
295 my $address2 = exists($options->{'address2'})
296 ? $options->{'address2'}
298 $content{address} .= ", ". $address2 if length($address2);
300 $content{city} = exists($options->{city})
303 $content{state} = exists($options->{state})
306 $content{zip} = exists($options->{zip})
309 $content{country} = exists($options->{country})
310 ? $options->{country}
313 #3.0 is a good a time as any to get rid of this... add a config to pass it
314 # if anyone still needs it
315 #$content{referer} = 'http://cleanwhisker.420.am/';
317 $content{phone} = $self->daytime || $self->night;
319 my $currency = $conf->exists('business-onlinepayment-currency')
320 && $conf->config('business-onlinepayment-currency');
321 $content{currency} = $currency if $currency;
326 my %bop_method2payby = (
336 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
339 if (ref($_[0]) eq 'HASH') {
342 my ( $method, $amount ) = ( shift, shift );
344 $options{method} = $method;
345 $options{amount} = $amount;
350 # optional credit card surcharge
353 my $cc_surcharge = 0;
354 my $cc_surcharge_pct = 0;
355 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
356 if $conf->config('credit-card-surcharge-percentage')
357 && $options{method} eq 'CC';
359 # always add cc surcharge if called from event
360 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
361 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
362 $options{'amount'} += $cc_surcharge;
363 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
365 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
366 # payment screen), so consider the given
367 # amount as post-surcharge
368 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
371 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
372 $options{'cc_surcharge'} = $cc_surcharge;
376 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
377 warn " cc_surcharge = $cc_surcharge\n";
380 warn " $_ => $options{$_}\n" foreach keys %options;
383 return $self->fake_bop(\%options) if $options{'fake'};
385 $self->_bop_defaults(\%options);
388 # set trans_is_recur based on invnum if there is one
391 my $trans_is_recur = 0;
392 if ( $options{'invnum'} ) {
394 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
395 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
401 $cust_bill->cust_bill_pkg;
404 if grep { $_->freq ne '0' } @part_pkg;
412 my $payment_gateway = $self->_payment_gateway( \%options );
413 my $namespace = $payment_gateway->gateway_namespace;
415 eval "use $namespace";
419 # check for banned credit card/ACH
422 my $ban = FS::banned_pay->ban_search(
423 'payby' => $bop_method2payby{$options{method}},
424 'payinfo' => $options{payinfo},
426 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
429 # check for term discount validity
432 my $discount_term = $options{discount_term};
433 if ( $discount_term ) {
434 my $bill = ($self->cust_bill)[-1]
435 or return "Can't apply a term discount to an unbilled customer";
436 my $plan = FS::discount_plan->new(
438 months => $discount_term
439 ) or return "No discount available for term '$discount_term'";
441 if ( $plan->discounted_total != $options{amount} ) {
442 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
450 my $bop_content = $self->_bop_content(\%options);
451 return $bop_content unless ref($bop_content);
453 my @invoicing_list = $self->invoicing_list_emailonly;
454 if ( $conf->exists('emailinvoiceautoalways')
455 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
456 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
457 push @invoicing_list, $self->all_emails;
460 my $email = ($conf->exists('business-onlinepayment-email-override'))
461 ? $conf->config('business-onlinepayment-email-override')
462 : $invoicing_list[0];
467 if ( $namespace eq 'Business::OnlinePayment' ) {
469 if ( $options{method} eq 'CC' ) {
471 $content{card_number} = $options{payinfo};
472 $paydate = exists($options{'paydate'})
473 ? $options{'paydate'}
475 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
476 $content{expiration} = "$2/$1";
478 my $paycvv = exists($options{'paycvv'})
481 $content{cvv2} = $paycvv
484 my $paystart_month = exists($options{'paystart_month'})
485 ? $options{'paystart_month'}
486 : $self->paystart_month;
488 my $paystart_year = exists($options{'paystart_year'})
489 ? $options{'paystart_year'}
490 : $self->paystart_year;
492 $content{card_start} = "$paystart_month/$paystart_year"
493 if $paystart_month && $paystart_year;
495 my $payissue = exists($options{'payissue'})
496 ? $options{'payissue'}
498 $content{issue_number} = $payissue if $payissue;
500 if ( $self->_bop_recurring_billing(
501 'payinfo' => $options{'payinfo'},
502 'trans_is_recur' => $trans_is_recur,
506 $content{recurring_billing} = 'YES';
507 $content{acct_code} = 'rebill'
508 if $conf->exists('credit_card-recurring_billing_acct_code');
511 } elsif ( $options{method} eq 'ECHECK' ){
513 ( $content{account_number}, $content{routing_code} ) =
514 split('@', $options{payinfo});
515 $content{bank_name} = $options{payname};
516 $content{bank_state} = exists($options{'paystate'})
517 ? $options{'paystate'}
518 : $self->getfield('paystate');
519 $content{account_type}=
520 (exists($options{'paytype'}) && $options{'paytype'})
521 ? uc($options{'paytype'})
522 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
524 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
525 $content{account_name} = $self->company;
527 $content{account_name} = $self->getfield('first'). ' '.
528 $self->getfield('last');
531 $content{customer_org} = $self->company ? 'B' : 'I';
532 $content{state_id} = exists($options{'stateid'})
533 ? $options{'stateid'}
534 : $self->getfield('stateid');
535 $content{state_id_state} = exists($options{'stateid_state'})
536 ? $options{'stateid_state'}
537 : $self->getfield('stateid_state');
538 $content{customer_ssn} = exists($options{'ss'})
542 } elsif ( $options{method} eq 'LEC' ) {
543 $content{phone} = $options{payinfo};
545 die "unknown method ". $options{method};
548 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
551 die "unknown namespace $namespace";
558 my $balance = exists( $options{'balance'} )
559 ? $options{'balance'}
562 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
563 $self->select_for_update; #mutex ... just until we get our pending record in
564 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
566 #the checks here are intended to catch concurrent payments
567 #double-form-submission prevention is taken care of in cust_pay_pending::check
570 return "The customer's balance has changed; $options{method} transaction aborted."
571 if $self->balance < $balance;
573 #also check and make sure there aren't *other* pending payments for this cust
575 my @pending = qsearch('cust_pay_pending', {
576 'custnum' => $self->custnum,
577 'status' => { op=>'!=', value=>'done' }
580 #for third-party payments only, remove pending payments if they're in the
581 #'thirdparty' (waiting for customer action) state.
582 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
583 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
584 my $error = $_->delete;
585 warn "error deleting unfinished third-party payment ".
586 $_->paypendingnum . ": $error\n"
589 @pending = grep { $_->status ne 'thirdparty' } @pending;
592 return "A payment is already being processed for this customer (".
593 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
594 "); $options{method} transaction aborted."
597 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
599 my $cust_pay_pending = new FS::cust_pay_pending {
600 'custnum' => $self->custnum,
601 'paid' => $options{amount},
603 'payby' => $bop_method2payby{$options{method}},
604 'payinfo' => $options{payinfo},
605 'paymask' => $options{paymask},
606 'paydate' => $paydate,
607 'recurring_billing' => $content{recurring_billing},
608 'pkgnum' => $options{'pkgnum'},
610 'gatewaynum' => $payment_gateway->gatewaynum || '',
611 'session_id' => $options{session_id} || '',
612 'jobnum' => $options{depend_jobnum} || '',
614 $cust_pay_pending->payunique( $options{payunique} )
615 if defined($options{payunique}) && length($options{payunique});
617 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
619 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
620 return $cpp_new_err if $cpp_new_err;
622 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
624 warn Dumper($cust_pay_pending) if $DEBUG > 2;
626 my( $action1, $action2 ) =
627 split( /\s*\,\s*/, $payment_gateway->gateway_action );
629 my $transaction = new $namespace( $payment_gateway->gateway_module,
630 $self->_bop_options(\%options),
633 $transaction->content(
634 'type' => $options{method},
635 $self->_bop_auth(\%options),
636 'action' => $action1,
637 'description' => $options{'description'},
638 'amount' => $options{amount},
639 #'invoice_number' => $options{'invnum'},
640 'customer_id' => $self->custnum,
642 'reference' => $cust_pay_pending->paypendingnum, #for now
643 'callback_url' => $payment_gateway->gateway_callback_url,
644 'cancel_url' => $payment_gateway->gateway_cancel_url,
649 $cust_pay_pending->status('pending');
650 my $cpp_pending_err = $cust_pay_pending->replace;
651 return $cpp_pending_err if $cpp_pending_err;
653 warn Dumper($transaction) if $DEBUG > 2;
655 unless ( $BOP_TESTING ) {
656 $transaction->test_transaction(1)
657 if $conf->exists('business-onlinepayment-test_transaction');
658 $transaction->submit();
660 if ( $BOP_TESTING_SUCCESS ) {
661 $transaction->is_success(1);
662 $transaction->authorization('fake auth');
664 $transaction->is_success(0);
665 $transaction->error_message('fake failure');
669 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
671 $cust_pay_pending->status('thirdparty');
672 my $cpp_err = $cust_pay_pending->replace;
673 return { error => $cpp_err } if $cpp_err;
674 return { reference => $cust_pay_pending->paypendingnum,
675 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
677 } elsif ( $transaction->is_success() && $action2 ) {
679 $cust_pay_pending->status('authorized');
680 my $cpp_authorized_err = $cust_pay_pending->replace;
681 return $cpp_authorized_err if $cpp_authorized_err;
683 my $auth = $transaction->authorization;
684 my $ordernum = $transaction->can('order_number')
685 ? $transaction->order_number
689 new Business::OnlinePayment( $payment_gateway->gateway_module,
690 $self->_bop_options(\%options),
695 type => $options{method},
697 $self->_bop_auth(\%options),
698 order_number => $ordernum,
699 amount => $options{amount},
700 authorization => $auth,
701 description => $options{'description'},
704 foreach my $field (qw( authorization_source_code returned_ACI
705 transaction_identifier validation_code
706 transaction_sequence_num local_transaction_date
707 local_transaction_time AVS_result_code )) {
708 $capture{$field} = $transaction->$field() if $transaction->can($field);
711 $capture->content( %capture );
713 $capture->test_transaction(1)
714 if $conf->exists('business-onlinepayment-test_transaction');
717 unless ( $capture->is_success ) {
718 my $e = "Authorization successful but capture failed, custnum #".
719 $self->custnum. ': '. $capture->result_code.
720 ": ". $capture->error_message;
728 # remove paycvv after initial transaction
731 #false laziness w/misc/process/payment.cgi - check both to make sure working
733 if ( length($self->paycvv)
734 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
736 my $error = $self->remove_cvv;
738 warn "WARNING: error removing cvv: $error\n";
747 if ( $transaction->can('card_token') && $transaction->card_token ) {
749 if ( $options{'payinfo'} eq $self->payinfo ) {
750 $self->payinfo($transaction->card_token);
751 my $error = $self->replace;
753 warn "WARNING: error storing token: $error, but proceeding anyway\n";
763 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
775 if (ref($_[0]) eq 'HASH') {
778 my ( $method, $amount ) = ( shift, shift );
780 $options{method} = $method;
781 $options{amount} = $amount;
784 if ( $options{'fake_failure'} ) {
785 return "Error: No error; test failure requested with fake_failure";
788 my $cust_pay = new FS::cust_pay ( {
789 'custnum' => $self->custnum,
790 'invnum' => $options{'invnum'},
791 'paid' => $options{amount},
793 'payby' => $bop_method2payby{$options{method}},
794 #'payinfo' => $payinfo,
795 'payinfo' => '4111111111111111',
796 #'paydate' => $paydate,
797 'paydate' => '2012-05-01',
798 'processor' => 'FakeProcessor',
800 'order_number' => '32',
802 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
805 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
806 warn " $_ => $options{$_}\n" foreach keys %options;
809 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
812 $cust_pay->invnum(''); #try again with no specific invnum
813 my $error2 = $cust_pay->insert( $options{'manual'} ?
814 ( 'manual' => 1 ) : ()
817 # gah, even with transactions.
818 my $e = 'WARNING: Card/ACH debited but database not updated - '.
819 "error inserting (fake!) payment: $error2".
820 " (previously tried insert with invnum #$options{'invnum'}" .
827 if ( $options{'paynum_ref'} ) {
828 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
836 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
838 # Wraps up processing of a realtime credit card, ACH (electronic check) or
839 # phone bill transaction.
841 sub _realtime_bop_result {
842 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
844 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
847 warn "$me _realtime_bop_result: pending transaction ".
848 $cust_pay_pending->paypendingnum. "\n";
849 warn " $_ => $options{$_}\n" foreach keys %options;
852 my $payment_gateway = $options{payment_gateway}
853 or return "no payment gateway in arguments to _realtime_bop_result";
855 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
856 my $cpp_captured_err = $cust_pay_pending->replace;
857 return $cpp_captured_err if $cpp_captured_err;
859 if ( $transaction->is_success() ) {
861 my $order_number = $transaction->order_number
862 if $transaction->can('order_number');
864 my $cust_pay = new FS::cust_pay ( {
865 'custnum' => $self->custnum,
866 'invnum' => $options{'invnum'},
867 'paid' => $cust_pay_pending->paid,
869 'payby' => $cust_pay_pending->payby,
870 'payinfo' => $options{'payinfo'},
871 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
872 'paydate' => $cust_pay_pending->paydate,
873 'pkgnum' => $cust_pay_pending->pkgnum,
874 'discount_term' => $options{'discount_term'},
875 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
876 'processor' => $payment_gateway->gateway_module,
877 'auth' => $transaction->authorization,
878 'order_number' => $order_number || '',
879 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
881 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
882 $cust_pay->payunique( $options{payunique} )
883 if defined($options{payunique}) && length($options{payunique});
885 my $oldAutoCommit = $FS::UID::AutoCommit;
886 local $FS::UID::AutoCommit = 0;
889 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
891 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
894 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
895 $cust_pay->invnum(''); #try again with no specific invnum
896 $cust_pay->paynum('');
897 my $error2 = $cust_pay->insert( $options{'manual'} ?
898 ( 'manual' => 1 ) : ()
901 # gah. but at least we have a record of the state we had to abort in
902 # from cust_pay_pending now.
903 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
904 my $e = "WARNING: $options{method} captured but payment not recorded -".
905 " error inserting payment (". $payment_gateway->gateway_module.
907 " (previously tried insert with invnum #$options{'invnum'}" .
908 ": $error ) - pending payment saved as paypendingnum ".
909 $cust_pay_pending->paypendingnum. "\n";
915 my $jobnum = $cust_pay_pending->jobnum;
917 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
919 unless ( $placeholder ) {
920 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
921 my $e = "WARNING: $options{method} captured but job $jobnum not ".
922 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
927 $error = $placeholder->delete;
930 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
931 my $e = "WARNING: $options{method} captured but could not delete ".
932 "job $jobnum for paypendingnum ".
933 $cust_pay_pending->paypendingnum. ": $error\n";
940 if ( $options{'paynum_ref'} ) {
941 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
944 $cust_pay_pending->status('done');
945 $cust_pay_pending->statustext('captured');
946 $cust_pay_pending->paynum($cust_pay->paynum);
947 my $cpp_done_err = $cust_pay_pending->replace;
949 if ( $cpp_done_err ) {
951 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
952 my $e = "WARNING: $options{method} captured but payment not recorded - ".
953 "error updating status for paypendingnum ".
954 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
960 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
962 if ( $options{'apply'} ) {
963 my $apply_error = $self->apply_payments_and_credits;
964 if ( $apply_error ) {
965 warn "WARNING: error applying payment: $apply_error\n";
966 #but we still should return no error cause the payment otherwise went
971 # have a CC surcharge portion --> one-time charge
972 if ( $options{'cc_surcharge'} > 0 ) {
973 # XXX: this whole block needs to be in a transaction?
976 $invnum = $options{'invnum'} if $options{'invnum'};
977 unless ( $invnum ) { # probably from a payment screen
978 # do we have any open invoices? pick earliest
979 # uses the fact that cust_main->cust_bill sorts by date ascending
980 my @open = $self->open_cust_bill;
981 $invnum = $open[0]->invnum if scalar(@open);
984 unless ( $invnum ) { # still nothing? pick last closed invoice
985 # again uses fact that cust_main->cust_bill sorts by date ascending
986 my @closed = $self->cust_bill;
987 $invnum = $closed[$#closed]->invnum if scalar(@closed);
991 # XXX: unlikely case - pre-paying before any invoices generated
992 # what it should do is create a new invoice and pick it
993 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
998 my $charge_error = $self->charge({
999 'amount' => $options{'cc_surcharge'},
1000 'pkg' => 'Credit Card Surcharge',
1002 'cust_pkg_ref' => \$cust_pkg,
1005 warn 'Unable to add CC surcharge cust_pkg';
1009 $cust_pkg->setup(time);
1010 my $cp_error = $cust_pkg->replace;
1012 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1016 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1017 unless ( $cust_bill ) {
1018 warn "race condition + invoice deletion just happened";
1023 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1025 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1029 return ''; #no error
1035 my $perror = $payment_gateway->gateway_module. " error: ".
1036 $transaction->error_message;
1038 my $jobnum = $cust_pay_pending->jobnum;
1040 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1042 if ( $placeholder ) {
1043 my $error = $placeholder->depended_delete;
1044 $error ||= $placeholder->delete;
1045 warn "error removing provisioning jobs after declined paypendingnum ".
1046 $cust_pay_pending->paypendingnum. ": $error\n";
1048 my $e = "error finding job $jobnum for declined paypendingnum ".
1049 $cust_pay_pending->paypendingnum. "\n";
1055 unless ( $transaction->error_message ) {
1058 if ( $transaction->can('response_page') ) {
1060 'page' => ( $transaction->can('response_page')
1061 ? $transaction->response_page
1064 'code' => ( $transaction->can('response_code')
1065 ? $transaction->response_code
1068 'headers' => ( $transaction->can('response_headers')
1069 ? $transaction->response_headers
1075 "No additional debugging information available for ".
1076 $payment_gateway->gateway_module;
1079 $perror .= "No error_message returned from ".
1080 $payment_gateway->gateway_module. " -- ".
1081 ( ref($t_response) ? Dumper($t_response) : $t_response );
1085 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1086 && $conf->exists('emaildecline', $self->agentnum)
1087 && grep { $_ ne 'POST' } $self->invoicing_list
1088 && ! grep { $transaction->error_message =~ /$_/ }
1089 $conf->config('emaildecline-exclude', $self->agentnum)
1092 # Send a decline alert to the customer.
1093 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1096 # include the raw error message in the transaction state
1097 $cust_pay_pending->setfield('error', $transaction->error_message);
1098 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1099 $error = $msg_template->send( 'cust_main' => $self,
1100 'object' => $cust_pay_pending );
1104 my @templ = $conf->config('declinetemplate');
1105 my $template = new Text::Template (
1107 SOURCE => [ map "$_\n", @templ ],
1108 ) or return "($perror) can't create template: $Text::Template::ERROR";
1109 $template->compile()
1110 or return "($perror) can't compile template: $Text::Template::ERROR";
1114 scalar( $conf->config('company_name', $self->agentnum ) ),
1115 'company_address' =>
1116 join("\n", $conf->config('company_address', $self->agentnum ) ),
1117 'error' => $transaction->error_message,
1120 my $error = send_email(
1121 'from' => $conf->invoice_from_full( $self->agentnum ),
1122 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1123 'subject' => 'Your payment could not be processed',
1124 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1128 $perror .= " (also received error sending decline notification: $error)"
1133 $cust_pay_pending->status('done');
1134 $cust_pay_pending->statustext("declined: $perror");
1135 my $cpp_done_err = $cust_pay_pending->replace;
1136 if ( $cpp_done_err ) {
1137 my $e = "WARNING: $options{method} declined but pending payment not ".
1138 "resolved - error updating status for paypendingnum ".
1139 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1141 $perror = "$e ($perror)";
1149 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1151 Verifies successful third party processing of a realtime credit card,
1152 ACH (electronic check) or phone bill transaction via a
1153 Business::OnlineThirdPartyPayment realtime gateway. See
1154 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1156 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1158 The additional options I<payname>, I<city>, I<state>,
1159 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1160 if set, will override the value from the customer record.
1162 I<description> is a free-text field passed to the gateway. It defaults to
1163 "Internet services".
1165 If an I<invnum> is specified, this payment (if successful) is applied to the
1166 specified invoice. If you don't specify an I<invnum> you might want to
1167 call the B<apply_payments> method.
1169 I<quiet> can be set true to surpress email decline notices.
1171 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1172 resulting paynum, if any.
1174 I<payunique> is a unique identifier for this payment.
1176 Returns a hashref containing elements bill_error (which will be undefined
1177 upon success) and session_id of any associated session.
1181 sub realtime_botpp_capture {
1182 my( $self, $cust_pay_pending, %options ) = @_;
1184 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1187 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1188 warn " $_ => $options{$_}\n" foreach keys %options;
1191 eval "use Business::OnlineThirdPartyPayment";
1195 # select the gateway
1198 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1200 my $payment_gateway;
1201 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1202 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1203 { gatewaynum => $gatewaynum }
1205 : $self->agent->payment_gateway( 'method' => $method,
1206 # 'invnum' => $cust_pay_pending->invnum,
1207 # 'payinfo' => $cust_pay_pending->payinfo,
1210 $options{payment_gateway} = $payment_gateway; # for the helper subs
1216 my @invoicing_list = $self->invoicing_list_emailonly;
1217 if ( $conf->exists('emailinvoiceautoalways')
1218 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1219 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1220 push @invoicing_list, $self->all_emails;
1223 my $email = ($conf->exists('business-onlinepayment-email-override'))
1224 ? $conf->config('business-onlinepayment-email-override')
1225 : $invoicing_list[0];
1229 $content{email_customer} =
1230 ( $conf->exists('business-onlinepayment-email_customer')
1231 || $conf->exists('business-onlinepayment-email-override') );
1234 # run transaction(s)
1238 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1239 $self->_bop_options(\%options),
1242 $transaction->reference({ %options });
1244 $transaction->content(
1246 $self->_bop_auth(\%options),
1247 'action' => 'Post Authorization',
1248 'description' => $options{'description'},
1249 'amount' => $cust_pay_pending->paid,
1250 #'invoice_number' => $options{'invnum'},
1251 'customer_id' => $self->custnum,
1253 #3.0 is a good a time as any to get rid of this... add a config to pass it
1254 # if anyone still needs it
1255 #'referer' => 'http://cleanwhisker.420.am/',
1257 'reference' => $cust_pay_pending->paypendingnum,
1259 'phone' => $self->daytime || $self->night,
1261 # plus whatever is required for bogus capture avoidance
1264 $transaction->submit();
1267 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1269 if ( $options{'apply'} ) {
1270 my $apply_error = $self->apply_payments_and_credits;
1271 if ( $apply_error ) {
1272 warn "WARNING: error applying payment: $apply_error\n";
1277 bill_error => $error,
1278 session_id => $cust_pay_pending->session_id,
1283 =item default_payment_gateway
1285 DEPRECATED -- use agent->payment_gateway
1289 sub default_payment_gateway {
1290 my( $self, $method ) = @_;
1292 die "Real-time processing not enabled\n"
1293 unless $conf->exists('business-onlinepayment');
1295 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1298 my $bop_config = 'business-onlinepayment';
1299 $bop_config .= '-ach'
1300 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1301 my ( $processor, $login, $password, $action, @bop_options ) =
1302 $conf->config($bop_config);
1303 $action ||= 'normal authorization';
1304 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1305 die "No real-time processor is enabled - ".
1306 "did you set the business-onlinepayment configuration value?\n"
1309 ( $processor, $login, $password, $action, @bop_options )
1312 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1314 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1315 via a Business::OnlinePayment realtime gateway. See
1316 L<http://420.am/business-onlinepayment> for supported gateways.
1318 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1320 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1322 Most gateways require a reference to an original payment transaction to refund,
1323 so you probably need to specify a I<paynum>.
1325 I<amount> defaults to the original amount of the payment if not specified.
1327 I<reasonnum> specifies a reason for the refund.
1329 I<paydate> specifies the expiration date for a credit card overriding the
1330 value from the customer record or the payment record. Specified as yyyy-mm-dd
1332 Implementation note: If I<amount> is unspecified or equal to the amount of the
1333 orignal payment, first an attempt is made to "void" the transaction via
1334 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1335 the normal attempt is made to "refund" ("credit") the transaction via the
1336 gateway is attempted. No attempt to "void" the transaction is made if the
1337 gateway has introspection data and doesn't support void.
1339 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1340 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1341 #if set, will override the value from the customer record.
1343 #If an I<invnum> is specified, this payment (if successful) is applied to the
1344 #specified invoice. If you don't specify an I<invnum> you might want to
1345 #call the B<apply_payments> method.
1349 #some false laziness w/realtime_bop, not enough to make it worth merging
1350 #but some useful small subs should be pulled out
1351 sub realtime_refund_bop {
1354 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1357 if (ref($_[0]) eq 'HASH') {
1358 %options = %{$_[0]};
1362 $options{method} = $method;
1365 my ($reason, $reason_text);
1366 if ( $options{'reasonnum'} ) {
1367 # do this here, because we need the plain text reason string in case we
1369 $reason = FS::reason->by_key($options{'reasonnum'});
1370 $reason_text = $reason->reason;
1372 # support old 'reason' string parameter in case it's still used,
1373 # or else set a default
1374 $reason_text = $options{'reason'} || 'card or ACH refund';
1376 $reason = FS::reason->new_or_existing(
1377 reason => $reason_text,
1378 type => 'Refund reason',
1382 return "failed to add refund reason: $@";
1387 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1388 warn " $_ => $options{$_}\n" foreach keys %options;
1394 # look up the original payment and optionally a gateway for that payment
1398 my $amount = $options{'amount'};
1400 my( $processor, $login, $password, @bop_options, $namespace ) ;
1401 my( $auth, $order_number ) = ( '', '', '' );
1402 my $gatewaynum = '';
1404 if ( $options{'paynum'} ) {
1406 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1407 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1408 or return "Unknown paynum $options{'paynum'}";
1409 $amount ||= $cust_pay->paid;
1411 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1412 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1414 if ( $cust_pay->get('processor') ) {
1415 ($gatewaynum, $processor, $auth, $order_number) =
1417 $cust_pay->gatewaynum,
1418 $cust_pay->processor,
1420 $cust_pay->order_number,
1423 # this payment wasn't upgraded, which probably means this won't work,
1425 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1426 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1427 $cust_pay->paybatch;
1428 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1431 if ( $gatewaynum ) { #gateway for the payment to be refunded
1433 my $payment_gateway =
1434 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1435 die "payment gateway $gatewaynum not found"
1436 unless $payment_gateway;
1438 $processor = $payment_gateway->gateway_module;
1439 $login = $payment_gateway->gateway_username;
1440 $password = $payment_gateway->gateway_password;
1441 $namespace = $payment_gateway->gateway_namespace;
1442 @bop_options = $payment_gateway->options;
1444 } else { #try the default gateway
1447 my $payment_gateway =
1448 $self->agent->payment_gateway('method' => $options{method});
1450 ( $conf_processor, $login, $password, $namespace ) =
1451 map { my $method = "gateway_$_"; $payment_gateway->$method }
1452 qw( module username password namespace );
1454 @bop_options = $payment_gateway->gatewaynum
1455 ? $payment_gateway->options
1456 : @{ $payment_gateway->get('options') };
1458 return "processor of payment $options{'paynum'} $processor does not".
1459 " match default processor $conf_processor"
1460 unless $processor eq $conf_processor;
1465 } else { # didn't specify a paynum, so look for agent gateway overrides
1466 # like a normal transaction
1468 my $payment_gateway =
1469 $self->agent->payment_gateway( 'method' => $options{method},
1470 #'payinfo' => $payinfo,
1472 my( $processor, $login, $password, $namespace ) =
1473 map { my $method = "gateway_$_"; $payment_gateway->$method }
1474 qw( module username password namespace );
1476 my @bop_options = $payment_gateway->gatewaynum
1477 ? $payment_gateway->options
1478 : @{ $payment_gateway->get('options') };
1481 return "neither amount nor paynum specified" unless $amount;
1483 eval "use $namespace";
1488 'type' => $options{method},
1490 'password' => $password,
1491 'order_number' => $order_number,
1492 'amount' => $amount,
1494 #3.0 is a good a time as any to get rid of this... add a config to pass it
1495 # if anyone still needs it
1496 #'referer' => 'http://cleanwhisker.420.am/',
1498 $content{authorization} = $auth
1499 if length($auth); #echeck/ACH transactions have an order # but no auth
1500 #(at least with authorize.net)
1502 my $currency = $conf->exists('business-onlinepayment-currency')
1503 && $conf->config('business-onlinepayment-currency');
1504 $content{currency} = $currency if $currency;
1506 my $disable_void_after;
1507 if ($conf->exists('disable_void_after')
1508 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1509 $disable_void_after = $1;
1512 #first try void if applicable
1513 my $void = new Business::OnlinePayment( $processor, @bop_options );
1516 if ($void->can('info')) {
1518 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1519 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1520 my %supported_actions = $void->info('supported_actions');
1522 if ( %supported_actions && $paytype
1523 && defined($supported_actions{$paytype})
1524 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1527 if ( $cust_pay && $cust_pay->paid == $amount
1529 ( not defined($disable_void_after) )
1530 || ( time < ($cust_pay->_date + $disable_void_after ) )
1534 warn " attempting void\n" if $DEBUG > 1;
1535 if ( $void->can('info') ) {
1536 if ( $cust_pay->payby eq 'CARD'
1537 && $void->info('CC_void_requires_card') )
1539 $content{'card_number'} = $cust_pay->payinfo;
1540 } elsif ( $cust_pay->payby eq 'CHEK'
1541 && $void->info('ECHECK_void_requires_account') )
1543 ( $content{'account_number'}, $content{'routing_code'} ) =
1544 split('@', $cust_pay->payinfo);
1545 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1548 $void->content( 'action' => 'void', %content );
1549 $void->test_transaction(1)
1550 if $conf->exists('business-onlinepayment-test_transaction');
1552 if ( $void->is_success ) {
1553 my $error = $cust_pay->void($reason_text);
1555 # gah, even with transactions.
1556 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1557 "error voiding payment: $error";
1561 warn " void successful\n" if $DEBUG > 1;
1566 warn " void unsuccessful, trying refund\n"
1570 my $address = $self->address1;
1571 $address .= ", ". $self->address2 if $self->address2;
1573 my($payname, $payfirst, $paylast);
1574 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1575 $payname = $self->payname;
1576 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1577 or return "Illegal payname $payname";
1578 ($payfirst, $paylast) = ($1, $2);
1580 $payfirst = $self->getfield('first');
1581 $paylast = $self->getfield('last');
1582 $payname = "$payfirst $paylast";
1585 my @invoicing_list = $self->invoicing_list_emailonly;
1586 if ( $conf->exists('emailinvoiceautoalways')
1587 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1588 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1589 push @invoicing_list, $self->all_emails;
1592 my $email = ($conf->exists('business-onlinepayment-email-override'))
1593 ? $conf->config('business-onlinepayment-email-override')
1594 : $invoicing_list[0];
1596 my $payip = exists($options{'payip'})
1599 $content{customer_ip} = $payip
1603 if ( $options{method} eq 'CC' ) {
1606 $content{card_number} = $payinfo = $cust_pay->payinfo;
1607 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1608 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1609 ($content{expiration} = "$2/$1"); # where available
1611 $content{card_number} = $payinfo = $self->payinfo;
1612 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1613 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1614 $content{expiration} = "$2/$1";
1617 } elsif ( $options{method} eq 'ECHECK' ) {
1620 $payinfo = $cust_pay->payinfo;
1622 $payinfo = $self->payinfo;
1624 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1625 $content{bank_name} = $self->payname;
1626 $content{account_type} = 'CHECKING';
1627 $content{account_name} = $payname;
1628 $content{customer_org} = $self->company ? 'B' : 'I';
1629 $content{customer_ssn} = $self->ss;
1630 } elsif ( $options{method} eq 'LEC' ) {
1631 $content{phone} = $payinfo = $self->payinfo;
1635 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1636 my %sub_content = $refund->content(
1637 'action' => 'credit',
1638 'customer_id' => $self->custnum,
1639 'last_name' => $paylast,
1640 'first_name' => $payfirst,
1642 'address' => $address,
1643 'city' => $self->city,
1644 'state' => $self->state,
1645 'zip' => $self->zip,
1646 'country' => $self->country,
1648 'phone' => $self->daytime || $self->night,
1651 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1653 $refund->test_transaction(1)
1654 if $conf->exists('business-onlinepayment-test_transaction');
1657 return "$processor error: ". $refund->error_message
1658 unless $refund->is_success();
1660 $order_number = $refund->order_number if $refund->can('order_number');
1662 # change this to just use $cust_pay->delete_cust_bill_pay?
1663 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1664 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1665 last unless @cust_bill_pay;
1666 my $cust_bill_pay = pop @cust_bill_pay;
1667 my $error = $cust_bill_pay->delete;
1671 my $cust_refund = new FS::cust_refund ( {
1672 'custnum' => $self->custnum,
1673 'paynum' => $options{'paynum'},
1674 'source_paynum' => $options{'paynum'},
1675 'refund' => $amount,
1677 'payby' => $bop_method2payby{$options{method}},
1678 'payinfo' => $payinfo,
1679 'reasonnum' => $reason->reasonnum,
1680 'gatewaynum' => $gatewaynum, # may be null
1681 'processor' => $processor,
1682 'auth' => $refund->authorization,
1683 'order_number' => $order_number,
1685 my $error = $cust_refund->insert;
1687 $cust_refund->paynum(''); #try again with no specific paynum
1688 $cust_refund->source_paynum('');
1689 my $error2 = $cust_refund->insert;
1691 # gah, even with transactions.
1692 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1693 "error inserting refund ($processor): $error2".
1694 " (previously tried insert with paynum #$options{'paynum'}" .
1713 L<FS::cust_main>, L<FS::cust_main::Billing>