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<apply> can be set to true to run B<apply_payments_and_credits> on success.
134 I<no_auto_apply> can be set to true to prevent resulting payment from being automatically applied.
136 I<quiet> can be set true to surpress email decline notices.
138 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
139 resulting paynum, if any.
141 I<payunique> is a unique identifier for this payment.
143 I<session_id> is a session identifier associated with this payment.
145 I<depend_jobnum> allows payment capture to unlock export jobs
147 I<discount_term> attempts to take a discount by prepaying for discount_term.
148 The payment will fail if I<amount> is incorrect for this discount term.
150 A direct (Business::OnlinePayment) transaction will return nothing on success,
151 or an error message on failure.
153 A third-party transaction will return a hashref containing:
155 - popup_url: the URL to which a browser should be redirected to complete
157 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
158 - reference: a reference ID for the transaction, to show the customer.
160 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
164 # some helper routines
165 sub _bop_recurring_billing {
166 my( $self, %opt ) = @_;
168 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
170 if ( defined($method) && $method eq 'transaction_is_recur' ) {
172 return 1 if $opt{'trans_is_recur'};
176 # return 1 if the payinfo has been used for another payment
177 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
185 sub _payment_gateway {
186 my ($self, $options) = @_;
188 if ( $options->{'selfservice'} ) {
189 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
191 return $options->{payment_gateway} ||=
192 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
196 if ( $options->{'fake_gatewaynum'} ) {
197 $options->{payment_gateway} =
198 qsearchs('payment_gateway',
199 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
203 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
204 unless exists($options->{payment_gateway});
206 $options->{payment_gateway};
210 my ($self, $options) = @_;
213 'login' => $options->{payment_gateway}->gateway_username,
214 'password' => $options->{payment_gateway}->gateway_password,
219 my ($self, $options) = @_;
221 $options->{payment_gateway}->gatewaynum
222 ? $options->{payment_gateway}->options
223 : @{ $options->{payment_gateway}->get('options') };
228 my ($self, $options) = @_;
230 unless ( $options->{'description'} ) {
231 if ( $conf->exists('business-onlinepayment-description') ) {
232 my $dtempl = $conf->config('business-onlinepayment-description');
234 my $agent = $self->agent->agent;
236 $options->{'description'} = eval qq("$dtempl");
238 $options->{'description'} = 'Internet services';
242 unless ( exists( $options->{'payinfo'} ) ) {
243 $options->{'payinfo'} = $self->payinfo;
244 $options->{'paymask'} = $self->paymask;
247 # Default invoice number if the customer has exactly one open invoice.
248 if( ! $options->{'invnum'} ) {
249 $options->{'invnum'} = '';
250 my @open = $self->open_cust_bill;
251 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
254 $options->{payname} = $self->payname unless exists( $options->{payname} );
258 my ($self, $options) = @_;
261 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
262 $content{customer_ip} = $payip if length($payip);
264 $content{invoice_number} = $options->{'invnum'}
265 if exists($options->{'invnum'}) && length($options->{'invnum'});
267 $content{email_customer} =
268 ( $conf->exists('business-onlinepayment-email_customer')
269 || $conf->exists('business-onlinepayment-email-override') );
271 my ($payname, $payfirst, $paylast);
272 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
273 ($payname = $options->{payname}) =~
274 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
275 or return "Illegal payname $payname";
276 ($payfirst, $paylast) = ($1, $2);
278 $payfirst = $self->getfield('first');
279 $paylast = $self->getfield('last');
280 $payname = "$payfirst $paylast";
283 $content{last_name} = $paylast;
284 $content{first_name} = $payfirst;
286 $content{name} = $payname;
288 $content{address} = exists($options->{'address1'})
289 ? $options->{'address1'}
291 my $address2 = exists($options->{'address2'})
292 ? $options->{'address2'}
294 $content{address} .= ", ". $address2 if length($address2);
296 $content{city} = exists($options->{city})
299 $content{state} = exists($options->{state})
302 $content{zip} = exists($options->{zip})
305 $content{country} = exists($options->{country})
306 ? $options->{country}
309 #3.0 is a good a time as any to get rid of this... add a config to pass it
310 # if anyone still needs it
311 #$content{referer} = 'http://cleanwhisker.420.am/';
313 $content{phone} = $self->daytime || $self->night;
315 my $currency = $conf->exists('business-onlinepayment-currency')
316 && $conf->config('business-onlinepayment-currency');
317 $content{currency} = $currency if $currency;
322 my %bop_method2payby = (
332 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
335 if (ref($_[0]) eq 'HASH') {
338 my ( $method, $amount ) = ( shift, shift );
340 $options{method} = $method;
341 $options{amount} = $amount;
346 # optional credit card surcharge
349 my $cc_surcharge = 0;
350 my $cc_surcharge_pct = 0;
351 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
352 if $conf->config('credit-card-surcharge-percentage')
353 && $options{method} eq 'CC';
355 # always add cc surcharge if called from event
356 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
357 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
358 $options{'amount'} += $cc_surcharge;
359 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
361 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
362 # payment screen), so consider the given
363 # amount as post-surcharge
364 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
367 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
368 $options{'cc_surcharge'} = $cc_surcharge;
372 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
373 warn " cc_surcharge = $cc_surcharge\n";
376 warn " $_ => $options{$_}\n" foreach keys %options;
379 return $self->fake_bop(\%options) if $options{'fake'};
381 $self->_bop_defaults(\%options);
384 # set trans_is_recur based on invnum if there is one
387 my $trans_is_recur = 0;
388 if ( $options{'invnum'} ) {
390 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
391 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
397 $cust_bill->cust_bill_pkg;
400 if grep { $_->freq ne '0' } @part_pkg;
408 my $payment_gateway = $self->_payment_gateway( \%options );
409 my $namespace = $payment_gateway->gateway_namespace;
411 eval "use $namespace";
415 # check for banned credit card/ACH
418 my $ban = FS::banned_pay->ban_search(
419 'payby' => $bop_method2payby{$options{method}},
420 'payinfo' => $options{payinfo},
422 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
425 # check for term discount validity
428 my $discount_term = $options{discount_term};
429 if ( $discount_term ) {
430 my $bill = ($self->cust_bill)[-1]
431 or return "Can't apply a term discount to an unbilled customer";
432 my $plan = FS::discount_plan->new(
434 months => $discount_term
435 ) or return "No discount available for term '$discount_term'";
437 if ( $plan->discounted_total != $options{amount} ) {
438 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
446 my $bop_content = $self->_bop_content(\%options);
447 return $bop_content unless ref($bop_content);
449 my @invoicing_list = $self->invoicing_list_emailonly;
450 if ( $conf->exists('emailinvoiceautoalways')
451 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
452 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
453 push @invoicing_list, $self->all_emails;
456 my $email = ($conf->exists('business-onlinepayment-email-override'))
457 ? $conf->config('business-onlinepayment-email-override')
458 : $invoicing_list[0];
463 if ( $namespace eq 'Business::OnlinePayment' ) {
465 if ( $options{method} eq 'CC' ) {
467 $content{card_number} = $options{payinfo};
468 $paydate = exists($options{'paydate'})
469 ? $options{'paydate'}
471 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
472 $content{expiration} = "$2/$1";
474 my $paycvv = exists($options{'paycvv'})
477 $content{cvv2} = $paycvv
480 my $paystart_month = exists($options{'paystart_month'})
481 ? $options{'paystart_month'}
482 : $self->paystart_month;
484 my $paystart_year = exists($options{'paystart_year'})
485 ? $options{'paystart_year'}
486 : $self->paystart_year;
488 $content{card_start} = "$paystart_month/$paystart_year"
489 if $paystart_month && $paystart_year;
491 my $payissue = exists($options{'payissue'})
492 ? $options{'payissue'}
494 $content{issue_number} = $payissue if $payissue;
496 if ( $self->_bop_recurring_billing(
497 'payinfo' => $options{'payinfo'},
498 'trans_is_recur' => $trans_is_recur,
502 $content{recurring_billing} = 'YES';
503 $content{acct_code} = 'rebill'
504 if $conf->exists('credit_card-recurring_billing_acct_code');
507 } elsif ( $options{method} eq 'ECHECK' ){
509 ( $content{account_number}, $content{routing_code} ) =
510 split('@', $options{payinfo});
511 $content{bank_name} = $options{payname};
512 $content{bank_state} = exists($options{'paystate'})
513 ? $options{'paystate'}
514 : $self->getfield('paystate');
515 $content{account_type}=
516 (exists($options{'paytype'}) && $options{'paytype'})
517 ? uc($options{'paytype'})
518 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
520 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
521 $content{account_name} = $self->company;
523 $content{account_name} = $self->getfield('first'). ' '.
524 $self->getfield('last');
527 $content{customer_org} = $self->company ? 'B' : 'I';
528 $content{state_id} = exists($options{'stateid'})
529 ? $options{'stateid'}
530 : $self->getfield('stateid');
531 $content{state_id_state} = exists($options{'stateid_state'})
532 ? $options{'stateid_state'}
533 : $self->getfield('stateid_state');
534 $content{customer_ssn} = exists($options{'ss'})
538 } elsif ( $options{method} eq 'LEC' ) {
539 $content{phone} = $options{payinfo};
541 die "unknown method ". $options{method};
544 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
547 die "unknown namespace $namespace";
554 my $balance = exists( $options{'balance'} )
555 ? $options{'balance'}
558 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
559 $self->select_for_update; #mutex ... just until we get our pending record in
560 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
562 #the checks here are intended to catch concurrent payments
563 #double-form-submission prevention is taken care of in cust_pay_pending::check
566 return "The customer's balance has changed; $options{method} transaction aborted."
567 if $self->balance < $balance;
569 #also check and make sure there aren't *other* pending payments for this cust
571 my @pending = qsearch('cust_pay_pending', {
572 'custnum' => $self->custnum,
573 'status' => { op=>'!=', value=>'done' }
576 #for third-party payments only, remove pending payments if they're in the
577 #'thirdparty' (waiting for customer action) state.
578 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
579 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
580 my $error = $_->delete;
581 warn "error deleting unfinished third-party payment ".
582 $_->paypendingnum . ": $error\n"
585 @pending = grep { $_->status ne 'thirdparty' } @pending;
588 return "A payment is already being processed for this customer (".
589 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
590 "); $options{method} transaction aborted."
593 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
595 my $cust_pay_pending = new FS::cust_pay_pending {
596 'custnum' => $self->custnum,
597 'paid' => $options{amount},
599 'payby' => $bop_method2payby{$options{method}},
600 'payinfo' => $options{payinfo},
601 'paymask' => $options{paymask},
602 'paydate' => $paydate,
603 'recurring_billing' => $content{recurring_billing},
604 'pkgnum' => $options{'pkgnum'},
606 'gatewaynum' => $payment_gateway->gatewaynum || '',
607 'session_id' => $options{session_id} || '',
608 'jobnum' => $options{depend_jobnum} || '',
610 $cust_pay_pending->payunique( $options{payunique} )
611 if defined($options{payunique}) && length($options{payunique});
613 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
615 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
616 return $cpp_new_err if $cpp_new_err;
618 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
620 warn Dumper($cust_pay_pending) if $DEBUG > 2;
622 my( $action1, $action2 ) =
623 split( /\s*\,\s*/, $payment_gateway->gateway_action );
625 my $transaction = new $namespace( $payment_gateway->gateway_module,
626 $self->_bop_options(\%options),
629 $transaction->content(
630 'type' => $options{method},
631 $self->_bop_auth(\%options),
632 'action' => $action1,
633 'description' => $options{'description'},
634 'amount' => $options{amount},
635 #'invoice_number' => $options{'invnum'},
636 'customer_id' => $self->custnum,
638 'reference' => $cust_pay_pending->paypendingnum, #for now
639 'callback_url' => $payment_gateway->gateway_callback_url,
640 'cancel_url' => $payment_gateway->gateway_cancel_url,
645 $cust_pay_pending->status('pending');
646 my $cpp_pending_err = $cust_pay_pending->replace;
647 return $cpp_pending_err if $cpp_pending_err;
649 warn Dumper($transaction) if $DEBUG > 2;
651 unless ( $BOP_TESTING ) {
652 $transaction->test_transaction(1)
653 if $conf->exists('business-onlinepayment-test_transaction');
654 $transaction->submit();
656 if ( $BOP_TESTING_SUCCESS ) {
657 $transaction->is_success(1);
658 $transaction->authorization('fake auth');
660 $transaction->is_success(0);
661 $transaction->error_message('fake failure');
665 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
667 $cust_pay_pending->status('thirdparty');
668 my $cpp_err = $cust_pay_pending->replace;
669 return { error => $cpp_err } if $cpp_err;
670 return { reference => $cust_pay_pending->paypendingnum,
671 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
673 } elsif ( $transaction->is_success() && $action2 ) {
675 $cust_pay_pending->status('authorized');
676 my $cpp_authorized_err = $cust_pay_pending->replace;
677 return $cpp_authorized_err if $cpp_authorized_err;
679 my $auth = $transaction->authorization;
680 my $ordernum = $transaction->can('order_number')
681 ? $transaction->order_number
685 new Business::OnlinePayment( $payment_gateway->gateway_module,
686 $self->_bop_options(\%options),
691 type => $options{method},
693 $self->_bop_auth(\%options),
694 order_number => $ordernum,
695 amount => $options{amount},
696 authorization => $auth,
697 description => $options{'description'},
700 foreach my $field (qw( authorization_source_code returned_ACI
701 transaction_identifier validation_code
702 transaction_sequence_num local_transaction_date
703 local_transaction_time AVS_result_code )) {
704 $capture{$field} = $transaction->$field() if $transaction->can($field);
707 $capture->content( %capture );
709 $capture->test_transaction(1)
710 if $conf->exists('business-onlinepayment-test_transaction');
713 unless ( $capture->is_success ) {
714 my $e = "Authorization successful but capture failed, custnum #".
715 $self->custnum. ': '. $capture->result_code.
716 ": ". $capture->error_message;
724 # remove paycvv after initial transaction
727 #false laziness w/misc/process/payment.cgi - check both to make sure working
729 if ( length($self->paycvv)
730 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
732 my $error = $self->remove_cvv;
734 warn "WARNING: error removing cvv: $error\n";
743 if ( $transaction->can('card_token') && $transaction->card_token ) {
745 if ( $options{'payinfo'} eq $self->payinfo ) {
746 $self->payinfo($transaction->card_token);
747 my $error = $self->replace;
749 warn "WARNING: error storing token: $error, but proceeding anyway\n";
759 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
771 if (ref($_[0]) eq 'HASH') {
774 my ( $method, $amount ) = ( shift, shift );
776 $options{method} = $method;
777 $options{amount} = $amount;
780 if ( $options{'fake_failure'} ) {
781 return "Error: No error; test failure requested with fake_failure";
784 my $cust_pay = new FS::cust_pay ( {
785 'custnum' => $self->custnum,
786 'invnum' => $options{'invnum'},
787 'paid' => $options{amount},
789 'payby' => $bop_method2payby{$options{method}},
790 #'payinfo' => $payinfo,
791 'payinfo' => '4111111111111111',
792 #'paydate' => $paydate,
793 'paydate' => '2012-05-01',
794 'processor' => 'FakeProcessor',
796 'order_number' => '32',
798 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
801 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
802 warn " $_ => $options{$_}\n" foreach keys %options;
805 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
808 $cust_pay->invnum(''); #try again with no specific invnum
809 my $error2 = $cust_pay->insert( $options{'manual'} ?
810 ( 'manual' => 1 ) : ()
813 # gah, even with transactions.
814 my $e = 'WARNING: Card/ACH debited but database not updated - '.
815 "error inserting (fake!) payment: $error2".
816 " (previously tried insert with invnum #$options{'invnum'}" .
823 if ( $options{'paynum_ref'} ) {
824 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
832 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
834 # Wraps up processing of a realtime credit card, ACH (electronic check) or
835 # phone bill transaction.
837 sub _realtime_bop_result {
838 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
840 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
843 warn "$me _realtime_bop_result: pending transaction ".
844 $cust_pay_pending->paypendingnum. "\n";
845 warn " $_ => $options{$_}\n" foreach keys %options;
848 my $payment_gateway = $options{payment_gateway}
849 or return "no payment gateway in arguments to _realtime_bop_result";
851 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
852 my $cpp_captured_err = $cust_pay_pending->replace;
853 return $cpp_captured_err if $cpp_captured_err;
855 if ( $transaction->is_success() ) {
857 my $order_number = $transaction->order_number
858 if $transaction->can('order_number');
860 my $cust_pay = new FS::cust_pay ( {
861 'custnum' => $self->custnum,
862 'invnum' => $options{'invnum'},
863 'paid' => $cust_pay_pending->paid,
865 'payby' => $cust_pay_pending->payby,
866 'payinfo' => $options{'payinfo'},
867 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
868 'paydate' => $cust_pay_pending->paydate,
869 'pkgnum' => $cust_pay_pending->pkgnum,
870 'discount_term' => $options{'discount_term'},
871 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
872 'processor' => $payment_gateway->gateway_module,
873 'auth' => $transaction->authorization,
874 'order_number' => $order_number || '',
875 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
877 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
878 $cust_pay->payunique( $options{payunique} )
879 if defined($options{payunique}) && length($options{payunique});
881 my $oldAutoCommit = $FS::UID::AutoCommit;
882 local $FS::UID::AutoCommit = 0;
885 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
887 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
890 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
891 $cust_pay->invnum(''); #try again with no specific invnum
892 $cust_pay->paynum('');
893 my $error2 = $cust_pay->insert( $options{'manual'} ?
894 ( 'manual' => 1 ) : ()
897 # gah. but at least we have a record of the state we had to abort in
898 # from cust_pay_pending now.
899 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
900 my $e = "WARNING: $options{method} captured but payment not recorded -".
901 " error inserting payment (". $payment_gateway->gateway_module.
903 " (previously tried insert with invnum #$options{'invnum'}" .
904 ": $error ) - pending payment saved as paypendingnum ".
905 $cust_pay_pending->paypendingnum. "\n";
911 my $jobnum = $cust_pay_pending->jobnum;
913 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
915 unless ( $placeholder ) {
916 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
917 my $e = "WARNING: $options{method} captured but job $jobnum not ".
918 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
923 $error = $placeholder->delete;
926 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
927 my $e = "WARNING: $options{method} captured but could not delete ".
928 "job $jobnum for paypendingnum ".
929 $cust_pay_pending->paypendingnum. ": $error\n";
936 if ( $options{'paynum_ref'} ) {
937 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
940 $cust_pay_pending->status('done');
941 $cust_pay_pending->statustext('captured');
942 $cust_pay_pending->paynum($cust_pay->paynum);
943 my $cpp_done_err = $cust_pay_pending->replace;
945 if ( $cpp_done_err ) {
947 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
948 my $e = "WARNING: $options{method} captured but payment not recorded - ".
949 "error updating status for paypendingnum ".
950 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
956 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
958 if ( $options{'apply'} ) {
959 my $apply_error = $self->apply_payments_and_credits;
960 if ( $apply_error ) {
961 warn "WARNING: error applying payment: $apply_error\n";
962 #but we still should return no error cause the payment otherwise went
967 # have a CC surcharge portion --> one-time charge
968 if ( $options{'cc_surcharge'} > 0 ) {
969 # XXX: this whole block needs to be in a transaction?
972 $invnum = $options{'invnum'} if $options{'invnum'};
973 unless ( $invnum ) { # probably from a payment screen
974 # do we have any open invoices? pick earliest
975 # uses the fact that cust_main->cust_bill sorts by date ascending
976 my @open = $self->open_cust_bill;
977 $invnum = $open[0]->invnum if scalar(@open);
980 unless ( $invnum ) { # still nothing? pick last closed invoice
981 # again uses fact that cust_main->cust_bill sorts by date ascending
982 my @closed = $self->cust_bill;
983 $invnum = $closed[$#closed]->invnum if scalar(@closed);
987 # XXX: unlikely case - pre-paying before any invoices generated
988 # what it should do is create a new invoice and pick it
989 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
994 my $charge_error = $self->charge({
995 'amount' => $options{'cc_surcharge'},
996 'pkg' => 'Credit Card Surcharge',
998 'cust_pkg_ref' => \$cust_pkg,
1001 warn 'Unable to add CC surcharge cust_pkg';
1005 $cust_pkg->setup(time);
1006 my $cp_error = $cust_pkg->replace;
1008 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1012 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1013 unless ( $cust_bill ) {
1014 warn "race condition + invoice deletion just happened";
1019 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1021 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1025 return ''; #no error
1031 my $perror = $payment_gateway->gateway_module. " error: ".
1032 $transaction->error_message;
1034 my $jobnum = $cust_pay_pending->jobnum;
1036 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1038 if ( $placeholder ) {
1039 my $error = $placeholder->depended_delete;
1040 $error ||= $placeholder->delete;
1041 warn "error removing provisioning jobs after declined paypendingnum ".
1042 $cust_pay_pending->paypendingnum. ": $error\n";
1044 my $e = "error finding job $jobnum for declined paypendingnum ".
1045 $cust_pay_pending->paypendingnum. "\n";
1051 unless ( $transaction->error_message ) {
1054 if ( $transaction->can('response_page') ) {
1056 'page' => ( $transaction->can('response_page')
1057 ? $transaction->response_page
1060 'code' => ( $transaction->can('response_code')
1061 ? $transaction->response_code
1064 'headers' => ( $transaction->can('response_headers')
1065 ? $transaction->response_headers
1071 "No additional debugging information available for ".
1072 $payment_gateway->gateway_module;
1075 $perror .= "No error_message returned from ".
1076 $payment_gateway->gateway_module. " -- ".
1077 ( ref($t_response) ? Dumper($t_response) : $t_response );
1081 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1082 && $conf->exists('emaildecline', $self->agentnum)
1083 && grep { $_ ne 'POST' } $self->invoicing_list
1084 && ! grep { $transaction->error_message =~ /$_/ }
1085 $conf->config('emaildecline-exclude', $self->agentnum)
1088 # Send a decline alert to the customer.
1089 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1092 # include the raw error message in the transaction state
1093 $cust_pay_pending->setfield('error', $transaction->error_message);
1094 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1095 $error = $msg_template->send( 'cust_main' => $self,
1096 'object' => $cust_pay_pending );
1100 my @templ = $conf->config('declinetemplate');
1101 my $template = new Text::Template (
1103 SOURCE => [ map "$_\n", @templ ],
1104 ) or return "($perror) can't create template: $Text::Template::ERROR";
1105 $template->compile()
1106 or return "($perror) can't compile template: $Text::Template::ERROR";
1110 scalar( $conf->config('company_name', $self->agentnum ) ),
1111 'company_address' =>
1112 join("\n", $conf->config('company_address', $self->agentnum ) ),
1113 'error' => $transaction->error_message,
1116 my $error = send_email(
1117 'from' => $conf->invoice_from_full( $self->agentnum ),
1118 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1119 'subject' => 'Your payment could not be processed',
1120 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1124 $perror .= " (also received error sending decline notification: $error)"
1129 $cust_pay_pending->status('done');
1130 $cust_pay_pending->statustext("declined: $perror");
1131 my $cpp_done_err = $cust_pay_pending->replace;
1132 if ( $cpp_done_err ) {
1133 my $e = "WARNING: $options{method} declined but pending payment not ".
1134 "resolved - error updating status for paypendingnum ".
1135 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1137 $perror = "$e ($perror)";
1145 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1147 Verifies successful third party processing of a realtime credit card,
1148 ACH (electronic check) or phone bill transaction via a
1149 Business::OnlineThirdPartyPayment realtime gateway. See
1150 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1152 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1154 The additional options I<payname>, I<city>, I<state>,
1155 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1156 if set, will override the value from the customer record.
1158 I<description> is a free-text field passed to the gateway. It defaults to
1159 "Internet services".
1161 If an I<invnum> is specified, this payment (if successful) is applied to the
1162 specified invoice. If you don't specify an I<invnum> you might want to
1163 call the B<apply_payments> method.
1165 I<quiet> can be set true to surpress email decline notices.
1167 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1168 resulting paynum, if any.
1170 I<payunique> is a unique identifier for this payment.
1172 Returns a hashref containing elements bill_error (which will be undefined
1173 upon success) and session_id of any associated session.
1177 sub realtime_botpp_capture {
1178 my( $self, $cust_pay_pending, %options ) = @_;
1180 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1183 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1184 warn " $_ => $options{$_}\n" foreach keys %options;
1187 eval "use Business::OnlineThirdPartyPayment";
1191 # select the gateway
1194 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1196 my $payment_gateway;
1197 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1198 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1199 { gatewaynum => $gatewaynum }
1201 : $self->agent->payment_gateway( 'method' => $method,
1202 # 'invnum' => $cust_pay_pending->invnum,
1203 # 'payinfo' => $cust_pay_pending->payinfo,
1206 $options{payment_gateway} = $payment_gateway; # for the helper subs
1212 my @invoicing_list = $self->invoicing_list_emailonly;
1213 if ( $conf->exists('emailinvoiceautoalways')
1214 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1215 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1216 push @invoicing_list, $self->all_emails;
1219 my $email = ($conf->exists('business-onlinepayment-email-override'))
1220 ? $conf->config('business-onlinepayment-email-override')
1221 : $invoicing_list[0];
1225 $content{email_customer} =
1226 ( $conf->exists('business-onlinepayment-email_customer')
1227 || $conf->exists('business-onlinepayment-email-override') );
1230 # run transaction(s)
1234 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1235 $self->_bop_options(\%options),
1238 $transaction->reference({ %options });
1240 $transaction->content(
1242 $self->_bop_auth(\%options),
1243 'action' => 'Post Authorization',
1244 'description' => $options{'description'},
1245 'amount' => $cust_pay_pending->paid,
1246 #'invoice_number' => $options{'invnum'},
1247 'customer_id' => $self->custnum,
1249 #3.0 is a good a time as any to get rid of this... add a config to pass it
1250 # if anyone still needs it
1251 #'referer' => 'http://cleanwhisker.420.am/',
1253 'reference' => $cust_pay_pending->paypendingnum,
1255 'phone' => $self->daytime || $self->night,
1257 # plus whatever is required for bogus capture avoidance
1260 $transaction->submit();
1263 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1265 if ( $options{'apply'} ) {
1266 my $apply_error = $self->apply_payments_and_credits;
1267 if ( $apply_error ) {
1268 warn "WARNING: error applying payment: $apply_error\n";
1273 bill_error => $error,
1274 session_id => $cust_pay_pending->session_id,
1279 =item default_payment_gateway
1281 DEPRECATED -- use agent->payment_gateway
1285 sub default_payment_gateway {
1286 my( $self, $method ) = @_;
1288 die "Real-time processing not enabled\n"
1289 unless $conf->exists('business-onlinepayment');
1291 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1294 my $bop_config = 'business-onlinepayment';
1295 $bop_config .= '-ach'
1296 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1297 my ( $processor, $login, $password, $action, @bop_options ) =
1298 $conf->config($bop_config);
1299 $action ||= 'normal authorization';
1300 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1301 die "No real-time processor is enabled - ".
1302 "did you set the business-onlinepayment configuration value?\n"
1305 ( $processor, $login, $password, $action, @bop_options )
1308 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1310 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1311 via a Business::OnlinePayment realtime gateway. See
1312 L<http://420.am/business-onlinepayment> for supported gateways.
1314 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1316 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1318 Most gateways require a reference to an original payment transaction to refund,
1319 so you probably need to specify a I<paynum>.
1321 I<amount> defaults to the original amount of the payment if not specified.
1323 I<reasonnum> specifies a reason for the refund.
1325 I<paydate> specifies the expiration date for a credit card overriding the
1326 value from the customer record or the payment record. Specified as yyyy-mm-dd
1328 Implementation note: If I<amount> is unspecified or equal to the amount of the
1329 orignal payment, first an attempt is made to "void" the transaction via
1330 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1331 the normal attempt is made to "refund" ("credit") the transaction via the
1332 gateway is attempted. No attempt to "void" the transaction is made if the
1333 gateway has introspection data and doesn't support void.
1335 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1336 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1337 #if set, will override the value from the customer record.
1339 #If an I<invnum> is specified, this payment (if successful) is applied to the
1340 #specified invoice. If you don't specify an I<invnum> you might want to
1341 #call the B<apply_payments> method.
1345 #some false laziness w/realtime_bop, not enough to make it worth merging
1346 #but some useful small subs should be pulled out
1347 sub realtime_refund_bop {
1350 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1353 if (ref($_[0]) eq 'HASH') {
1354 %options = %{$_[0]};
1358 $options{method} = $method;
1361 my ($reason, $reason_text);
1362 if ( $options{'reasonnum'} ) {
1363 # do this here, because we need the plain text reason string in case we
1365 $reason = FS::reason->by_key($options{'reasonnum'});
1366 $reason_text = $reason->reason;
1368 # support old 'reason' string parameter in case it's still used,
1369 # or else set a default
1370 $reason_text = $options{'reason'} || 'card or ACH refund';
1372 $reason = FS::reason->new_or_existing(
1373 reason => $reason_text,
1374 type => 'Refund reason',
1378 return "failed to add refund reason: $@";
1383 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1384 warn " $_ => $options{$_}\n" foreach keys %options;
1390 # look up the original payment and optionally a gateway for that payment
1394 my $amount = $options{'amount'};
1396 my( $processor, $login, $password, @bop_options, $namespace ) ;
1397 my( $auth, $order_number ) = ( '', '', '' );
1398 my $gatewaynum = '';
1400 if ( $options{'paynum'} ) {
1402 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1403 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1404 or return "Unknown paynum $options{'paynum'}";
1405 $amount ||= $cust_pay->paid;
1407 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1408 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1410 if ( $cust_pay->get('processor') ) {
1411 ($gatewaynum, $processor, $auth, $order_number) =
1413 $cust_pay->gatewaynum,
1414 $cust_pay->processor,
1416 $cust_pay->order_number,
1419 # this payment wasn't upgraded, which probably means this won't work,
1421 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1422 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1423 $cust_pay->paybatch;
1424 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1427 if ( $gatewaynum ) { #gateway for the payment to be refunded
1429 my $payment_gateway =
1430 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1431 die "payment gateway $gatewaynum not found"
1432 unless $payment_gateway;
1434 $processor = $payment_gateway->gateway_module;
1435 $login = $payment_gateway->gateway_username;
1436 $password = $payment_gateway->gateway_password;
1437 $namespace = $payment_gateway->gateway_namespace;
1438 @bop_options = $payment_gateway->options;
1440 } else { #try the default gateway
1443 my $payment_gateway =
1444 $self->agent->payment_gateway('method' => $options{method});
1446 ( $conf_processor, $login, $password, $namespace ) =
1447 map { my $method = "gateway_$_"; $payment_gateway->$method }
1448 qw( module username password namespace );
1450 @bop_options = $payment_gateway->gatewaynum
1451 ? $payment_gateway->options
1452 : @{ $payment_gateway->get('options') };
1454 return "processor of payment $options{'paynum'} $processor does not".
1455 " match default processor $conf_processor"
1456 unless $processor eq $conf_processor;
1461 } else { # didn't specify a paynum, so look for agent gateway overrides
1462 # like a normal transaction
1464 my $payment_gateway =
1465 $self->agent->payment_gateway( 'method' => $options{method},
1466 #'payinfo' => $payinfo,
1468 my( $processor, $login, $password, $namespace ) =
1469 map { my $method = "gateway_$_"; $payment_gateway->$method }
1470 qw( module username password namespace );
1472 my @bop_options = $payment_gateway->gatewaynum
1473 ? $payment_gateway->options
1474 : @{ $payment_gateway->get('options') };
1477 return "neither amount nor paynum specified" unless $amount;
1479 eval "use $namespace";
1484 'type' => $options{method},
1486 'password' => $password,
1487 'order_number' => $order_number,
1488 'amount' => $amount,
1490 #3.0 is a good a time as any to get rid of this... add a config to pass it
1491 # if anyone still needs it
1492 #'referer' => 'http://cleanwhisker.420.am/',
1494 $content{authorization} = $auth
1495 if length($auth); #echeck/ACH transactions have an order # but no auth
1496 #(at least with authorize.net)
1498 my $currency = $conf->exists('business-onlinepayment-currency')
1499 && $conf->config('business-onlinepayment-currency');
1500 $content{currency} = $currency if $currency;
1502 my $disable_void_after;
1503 if ($conf->exists('disable_void_after')
1504 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1505 $disable_void_after = $1;
1508 #first try void if applicable
1509 my $void = new Business::OnlinePayment( $processor, @bop_options );
1512 if ($void->can('info')) {
1514 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1515 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1516 my %supported_actions = $void->info('supported_actions');
1518 if ( %supported_actions && $paytype
1519 && defined($supported_actions{$paytype})
1520 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1523 if ( $cust_pay && $cust_pay->paid == $amount
1525 ( not defined($disable_void_after) )
1526 || ( time < ($cust_pay->_date + $disable_void_after ) )
1530 warn " attempting void\n" if $DEBUG > 1;
1531 if ( $void->can('info') ) {
1532 if ( $cust_pay->payby eq 'CARD'
1533 && $void->info('CC_void_requires_card') )
1535 $content{'card_number'} = $cust_pay->payinfo;
1536 } elsif ( $cust_pay->payby eq 'CHEK'
1537 && $void->info('ECHECK_void_requires_account') )
1539 ( $content{'account_number'}, $content{'routing_code'} ) =
1540 split('@', $cust_pay->payinfo);
1541 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1544 $void->content( 'action' => 'void', %content );
1545 $void->test_transaction(1)
1546 if $conf->exists('business-onlinepayment-test_transaction');
1548 if ( $void->is_success ) {
1549 my $error = $cust_pay->void($reason_text);
1551 # gah, even with transactions.
1552 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1553 "error voiding payment: $error";
1557 warn " void successful\n" if $DEBUG > 1;
1562 warn " void unsuccessful, trying refund\n"
1566 my $address = $self->address1;
1567 $address .= ", ". $self->address2 if $self->address2;
1569 my($payname, $payfirst, $paylast);
1570 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1571 $payname = $self->payname;
1572 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1573 or return "Illegal payname $payname";
1574 ($payfirst, $paylast) = ($1, $2);
1576 $payfirst = $self->getfield('first');
1577 $paylast = $self->getfield('last');
1578 $payname = "$payfirst $paylast";
1581 my @invoicing_list = $self->invoicing_list_emailonly;
1582 if ( $conf->exists('emailinvoiceautoalways')
1583 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1584 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1585 push @invoicing_list, $self->all_emails;
1588 my $email = ($conf->exists('business-onlinepayment-email-override'))
1589 ? $conf->config('business-onlinepayment-email-override')
1590 : $invoicing_list[0];
1592 my $payip = exists($options{'payip'})
1595 $content{customer_ip} = $payip
1599 if ( $options{method} eq 'CC' ) {
1602 $content{card_number} = $payinfo = $cust_pay->payinfo;
1603 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1604 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1605 ($content{expiration} = "$2/$1"); # where available
1607 $content{card_number} = $payinfo = $self->payinfo;
1608 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1609 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1610 $content{expiration} = "$2/$1";
1613 } elsif ( $options{method} eq 'ECHECK' ) {
1616 $payinfo = $cust_pay->payinfo;
1618 $payinfo = $self->payinfo;
1620 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1621 $content{bank_name} = $self->payname;
1622 $content{account_type} = 'CHECKING';
1623 $content{account_name} = $payname;
1624 $content{customer_org} = $self->company ? 'B' : 'I';
1625 $content{customer_ssn} = $self->ss;
1626 } elsif ( $options{method} eq 'LEC' ) {
1627 $content{phone} = $payinfo = $self->payinfo;
1631 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1632 my %sub_content = $refund->content(
1633 'action' => 'credit',
1634 'customer_id' => $self->custnum,
1635 'last_name' => $paylast,
1636 'first_name' => $payfirst,
1638 'address' => $address,
1639 'city' => $self->city,
1640 'state' => $self->state,
1641 'zip' => $self->zip,
1642 'country' => $self->country,
1644 'phone' => $self->daytime || $self->night,
1647 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1649 $refund->test_transaction(1)
1650 if $conf->exists('business-onlinepayment-test_transaction');
1653 return "$processor error: ". $refund->error_message
1654 unless $refund->is_success();
1656 $order_number = $refund->order_number if $refund->can('order_number');
1658 # change this to just use $cust_pay->delete_cust_bill_pay?
1659 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1660 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1661 last unless @cust_bill_pay;
1662 my $cust_bill_pay = pop @cust_bill_pay;
1663 my $error = $cust_bill_pay->delete;
1667 my $cust_refund = new FS::cust_refund ( {
1668 'custnum' => $self->custnum,
1669 'paynum' => $options{'paynum'},
1670 'source_paynum' => $options{'paynum'},
1671 'refund' => $amount,
1673 'payby' => $bop_method2payby{$options{method}},
1674 'payinfo' => $payinfo,
1675 'reasonnum' => $reason->reasonnum,
1676 'gatewaynum' => $gatewaynum, # may be null
1677 'processor' => $processor,
1678 'auth' => $refund->authorization,
1679 'order_number' => $order_number,
1681 my $error = $cust_refund->insert;
1683 $cust_refund->paynum(''); #try again with no specific paynum
1684 $cust_refund->source_paynum('');
1685 my $error2 = $cust_refund->insert;
1687 # gah, even with transactions.
1688 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1689 "error inserting refund ($processor): $error2".
1690 " (previously tried insert with paynum #$options{'paynum'}" .
1709 L<FS::cust_main>, L<FS::cust_main::Billing>