1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
8 use Business::CreditCard 0.28;
10 use FS::Record qw( qsearch qsearchs );
13 use FS::cust_pay_pending;
14 use FS::cust_bill_pay;
18 $realtime_bop_decline_quiet = 0;
20 # 1 is mostly method/subroutine entry and options
21 # 2 traces progress of some operations
22 # 3 is even more information including possibly sensitive data
24 $me = '[FS::cust_main::Billing_Realtime]';
27 our $BOP_TESTING_SUCCESS = 1;
29 install_callback FS::UID sub {
31 #yes, need it for stuff below (prolly should be cached)
36 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
42 These methods are available on FS::cust_main objects.
48 =item realtime_cust_payby
52 sub realtime_cust_payby {
53 my( $self, %options ) = @_;
55 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
57 $options{amount} = $self->balance unless exists( $options{amount} );
59 my @cust_payby = $self->cust_payby('CARD','CHEK');
62 foreach my $cust_payby (@cust_payby) {
63 $error = $cust_payby->realtime_bop( %options, );
67 #XXX what about the earlier errors?
73 =item realtime_collect [ OPTION => VALUE ... ]
75 Attempt to collect the customer's current balance with a realtime credit
76 card or electronic check transaction (see realtime_bop() below).
78 Returns the result of realtime_bop(): nothing, an error message, or a
79 hashref of state information for a third-party transaction.
81 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
83 I<method> is one of: I<CC> or I<ECHECK>. If none is specified
84 then it is deduced from the customer record.
86 If no I<amount> is specified, then the customer balance is used.
88 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
89 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
90 if set, will override the value from the customer record.
92 I<description> is a free-text field passed to the gateway. It defaults to
93 the value defined by the business-onlinepayment-description configuration
94 option, or "Internet services" if that is unset.
96 If an I<invnum> is specified, this payment (if successful) is applied to the
99 I<apply> will automatically apply a resulting payment.
101 I<quiet> can be set true to suppress email decline notices.
103 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
104 resulting paynum, if any.
106 I<payunique> is a unique identifier for this payment.
108 I<session_id> is a session identifier associated with this payment.
110 I<depend_jobnum> allows payment capture to unlock export jobs
114 sub realtime_collect {
115 my( $self, %options ) = @_;
117 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
120 warn "$me realtime_collect:\n";
121 warn " $_ => $options{$_}\n" foreach keys %options;
124 $options{amount} = $self->balance unless exists( $options{amount} );
125 return '' unless $options{amount} > 0;
127 $options{method} = FS::payby->payby2bop($self->payby)
128 unless exists( $options{method} );
130 return $self->realtime_bop({%options});
134 =item realtime_bop { [ ARG => VALUE ... ] }
136 Runs a realtime credit card or ACH (electronic check) transaction
137 via a Business::OnlinePayment realtime gateway. See
138 L<http://420.am/business-onlinepayment> for supported gateways.
140 Required arguments in the hashref are I<method>, and I<amount>
142 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
144 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
146 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
147 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
148 if set, will override the value from the customer record.
150 I<description> is a free-text field passed to the gateway. It defaults to
151 the value defined by the business-onlinepayment-description configuration
152 option, or "Internet services" if that is unset.
154 If an I<invnum> is specified, this payment (if successful) is applied to the
155 specified invoice. If the customer has exactly one open invoice, that
156 invoice number will be assumed. If you don't specify an I<invnum> you might
157 want to call the B<apply_payments> method or set the I<apply> option.
159 I<no_invnum> can be set to true to prevent that default invnum from being set.
161 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
163 I<no_auto_apply> can be set to true to set that flag on the resulting payment
164 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
165 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
167 I<quiet> can be set true to surpress email decline notices.
169 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
170 resulting paynum, if any.
172 I<payunique> is a unique identifier for this payment.
174 I<session_id> is a session identifier associated with this payment.
176 I<depend_jobnum> allows payment capture to unlock export jobs
178 I<discount_term> attempts to take a discount by prepaying for discount_term.
179 The payment will fail if I<amount> is incorrect for this discount term.
181 A direct (Business::OnlinePayment) transaction will return nothing on success,
182 or an error message on failure.
184 A third-party transaction will return a hashref containing:
186 - popup_url: the URL to which a browser should be redirected to complete
188 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
189 - reference: a reference ID for the transaction, to show the customer.
191 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
195 # some helper routines
197 # _bop_recurring_billing: Checks whether this payment should have the
198 # recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
199 # vSecure, etc.). This works in two different modes:
200 # - actual_oncard (default): treat the payment as recurring if the customer
201 # has made a payment using this card before.
202 # - transaction_is_recur: treat the payment as recurring if the invoice
203 # being paid has any recurring package charges.
205 sub _bop_recurring_billing {
206 my( $self, %opt ) = @_;
208 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
210 if ( defined($method) && $method eq 'transaction_is_recur' ) {
212 return 1 if $opt{'trans_is_recur'};
216 # return 1 if the payinfo has been used for another payment
217 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
225 sub _payment_gateway {
226 my ($self, $options) = @_;
228 if ( $options->{'selfservice'} ) {
229 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
231 return $options->{payment_gateway} ||=
232 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
236 if ( $options->{'fake_gatewaynum'} ) {
237 $options->{payment_gateway} =
238 qsearchs('payment_gateway',
239 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
243 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
244 unless exists($options->{payment_gateway});
246 $options->{payment_gateway};
250 my ($self, $options) = @_;
253 'login' => $options->{payment_gateway}->gateway_username,
254 'password' => $options->{payment_gateway}->gateway_password,
259 my ($self, $options) = @_;
261 $options->{payment_gateway}->gatewaynum
262 ? $options->{payment_gateway}->options
263 : @{ $options->{payment_gateway}->get('options') };
268 my ($self, $options) = @_;
270 unless ( $options->{'description'} ) {
271 if ( $conf->exists('business-onlinepayment-description') ) {
272 my $dtempl = $conf->config('business-onlinepayment-description');
274 my $agent = $self->agent->agent;
276 $options->{'description'} = eval qq("$dtempl");
278 $options->{'description'} = 'Internet services';
282 unless ( exists( $options->{'payinfo'} ) ) {
283 $options->{'payinfo'} = $self->payinfo;
284 $options->{'paymask'} = $self->paymask;
287 # Default invoice number if the customer has exactly one open invoice.
288 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
289 $options->{'invnum'} = '';
290 my @open = $self->open_cust_bill;
291 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
294 $options->{payname} = $self->payname unless exists( $options->{payname} );
298 my ($self, $options) = @_;
301 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
302 $content{customer_ip} = $payip if length($payip);
304 $content{invoice_number} = $options->{'invnum'}
305 if exists($options->{'invnum'}) && length($options->{'invnum'});
307 $content{email_customer} =
308 ( $conf->exists('business-onlinepayment-email_customer')
309 || $conf->exists('business-onlinepayment-email-override') );
311 my ($payname, $payfirst, $paylast);
312 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
313 ($payname = $options->{payname}) =~
314 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
315 or return "Illegal payname $payname";
316 ($payfirst, $paylast) = ($1, $2);
318 $payfirst = $self->getfield('first');
319 $paylast = $self->getfield('last');
320 $payname = "$payfirst $paylast";
323 $content{last_name} = $paylast;
324 $content{first_name} = $payfirst;
326 $content{name} = $payname;
328 $content{address} = exists($options->{'address1'})
329 ? $options->{'address1'}
331 my $address2 = exists($options->{'address2'})
332 ? $options->{'address2'}
334 $content{address} .= ", ". $address2 if length($address2);
336 $content{city} = exists($options->{city})
339 $content{state} = exists($options->{state})
342 $content{zip} = exists($options->{zip})
345 $content{country} = exists($options->{country})
346 ? $options->{country}
349 $content{phone} = $self->daytime || $self->night;
351 my $currency = $conf->exists('business-onlinepayment-currency')
352 && $conf->config('business-onlinepayment-currency');
353 $content{currency} = $currency if $currency;
358 my %bop_method2payby = (
367 confess "Can't call realtime_bop within another transaction ".
368 '($FS::UID::AutoCommit is false)'
369 unless $FS::UID::AutoCommit;
371 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
374 if (ref($_[0]) eq 'HASH') {
377 my ( $method, $amount ) = ( shift, shift );
379 $options{method} = $method;
380 $options{amount} = $amount;
385 # optional credit card surcharge
388 my $cc_surcharge = 0;
389 my $cc_surcharge_pct = 0;
390 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
391 if $conf->config('credit-card-surcharge-percentage')
392 && $options{method} eq 'CC';
394 # always add cc surcharge if called from event
395 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
396 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
397 $options{'amount'} += $cc_surcharge;
398 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
400 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
401 # payment screen), so consider the given
402 # amount as post-surcharge
403 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
406 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
407 $options{'cc_surcharge'} = $cc_surcharge;
411 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
412 warn " cc_surcharge = $cc_surcharge\n";
415 warn " $_ => $options{$_}\n" foreach keys %options;
418 return $self->fake_bop(\%options) if $options{'fake'};
420 $self->_bop_defaults(\%options);
423 # set trans_is_recur based on invnum if there is one
426 my $trans_is_recur = 0;
427 if ( $options{'invnum'} ) {
429 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
430 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
436 $cust_bill->cust_bill_pkg;
439 if grep { $_->freq ne '0' } @part_pkg;
447 my $payment_gateway = $self->_payment_gateway( \%options );
448 my $namespace = $payment_gateway->gateway_namespace;
450 eval "use $namespace";
454 # check for banned credit card/ACH
457 my $ban = FS::banned_pay->ban_search(
458 'payby' => $bop_method2payby{$options{method}},
459 'payinfo' => $options{payinfo},
461 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
464 # check for term discount validity
467 my $discount_term = $options{discount_term};
468 if ( $discount_term ) {
469 my $bill = ($self->cust_bill)[-1]
470 or return "Can't apply a term discount to an unbilled customer";
471 my $plan = FS::discount_plan->new(
473 months => $discount_term
474 ) or return "No discount available for term '$discount_term'";
476 if ( $plan->discounted_total != $options{amount} ) {
477 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
485 my $bop_content = $self->_bop_content(\%options);
486 return $bop_content unless ref($bop_content);
488 my @invoicing_list = $self->invoicing_list_emailonly;
489 if ( $conf->exists('emailinvoiceautoalways')
490 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
491 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
492 push @invoicing_list, $self->all_emails;
495 my $email = ($conf->exists('business-onlinepayment-email-override'))
496 ? $conf->config('business-onlinepayment-email-override')
497 : $invoicing_list[0];
502 if ( $namespace eq 'Business::OnlinePayment' ) {
504 if ( $options{method} eq 'CC' ) {
506 $content{card_number} = $options{payinfo};
507 $paydate = exists($options{'paydate'})
508 ? $options{'paydate'}
510 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
511 $content{expiration} = "$2/$1";
513 my $paycvv = exists($options{'paycvv'})
516 $content{cvv2} = $paycvv
519 my $paystart_month = exists($options{'paystart_month'})
520 ? $options{'paystart_month'}
521 : $self->paystart_month;
523 my $paystart_year = exists($options{'paystart_year'})
524 ? $options{'paystart_year'}
525 : $self->paystart_year;
527 $content{card_start} = "$paystart_month/$paystart_year"
528 if $paystart_month && $paystart_year;
530 my $payissue = exists($options{'payissue'})
531 ? $options{'payissue'}
533 $content{issue_number} = $payissue if $payissue;
535 if ( $self->_bop_recurring_billing(
536 'payinfo' => $options{'payinfo'},
537 'trans_is_recur' => $trans_is_recur,
541 $content{recurring_billing} = 'YES';
542 $content{acct_code} = 'rebill'
543 if $conf->exists('credit_card-recurring_billing_acct_code');
546 } elsif ( $options{method} eq 'ECHECK' ){
548 ( $content{account_number}, $content{routing_code} ) =
549 split('@', $options{payinfo});
550 $content{bank_name} = $options{payname};
551 $content{bank_state} = exists($options{'paystate'})
552 ? $options{'paystate'}
553 : $self->getfield('paystate');
554 $content{account_type}=
555 (exists($options{'paytype'}) && $options{'paytype'})
556 ? uc($options{'paytype'})
557 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
559 $content{company} = $self->company if $self->company;
561 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
562 $content{account_name} = $self->company;
564 $content{account_name} = $self->getfield('first'). ' '.
565 $self->getfield('last');
568 $content{customer_org} = $self->company ? 'B' : 'I';
569 $content{state_id} = exists($options{'stateid'})
570 ? $options{'stateid'}
571 : $self->getfield('stateid');
572 $content{state_id_state} = exists($options{'stateid_state'})
573 ? $options{'stateid_state'}
574 : $self->getfield('stateid_state');
575 $content{customer_ssn} = exists($options{'ss'})
580 die "unknown method ". $options{method};
583 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
586 die "unknown namespace $namespace";
593 my $balance = exists( $options{'balance'} )
594 ? $options{'balance'}
597 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
598 $self->select_for_update; #mutex ... just until we get our pending record in
599 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
601 #the checks here are intended to catch concurrent payments
602 #double-form-submission prevention is taken care of in cust_pay_pending::check
605 return "The customer's balance has changed; $options{method} transaction aborted."
606 if $self->balance < $balance;
608 #also check and make sure there aren't *other* pending payments for this cust
610 my @pending = qsearch('cust_pay_pending', {
611 'custnum' => $self->custnum,
612 'status' => { op=>'!=', value=>'done' }
615 #for third-party payments only, remove pending payments if they're in the
616 #'thirdparty' (waiting for customer action) state.
617 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
618 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
619 my $error = $_->delete;
620 warn "error deleting unfinished third-party payment ".
621 $_->paypendingnum . ": $error\n"
624 @pending = grep { $_->status ne 'thirdparty' } @pending;
627 return "A payment is already being processed for this customer (".
628 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
629 "); $options{method} transaction aborted."
632 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
634 my $cust_pay_pending = new FS::cust_pay_pending {
635 'custnum' => $self->custnum,
636 'paid' => $options{amount},
638 'payby' => $bop_method2payby{$options{method}},
639 'payinfo' => $options{payinfo},
640 'paymask' => $options{paymask},
641 'paydate' => $paydate,
642 'recurring_billing' => $content{recurring_billing},
643 'pkgnum' => $options{'pkgnum'},
645 'gatewaynum' => $payment_gateway->gatewaynum || '',
646 'session_id' => $options{session_id} || '',
647 'jobnum' => $options{depend_jobnum} || '',
649 $cust_pay_pending->payunique( $options{payunique} )
650 if defined($options{payunique}) && length($options{payunique});
652 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
654 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
655 return $cpp_new_err if $cpp_new_err;
657 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
659 warn Dumper($cust_pay_pending) if $DEBUG > 2;
661 my( $action1, $action2 ) =
662 split( /\s*\,\s*/, $payment_gateway->gateway_action );
664 my $transaction = new $namespace( $payment_gateway->gateway_module,
665 $self->_bop_options(\%options),
668 $transaction->content(
669 'type' => $options{method},
670 $self->_bop_auth(\%options),
671 'action' => $action1,
672 'description' => $options{'description'},
673 'amount' => $options{amount},
674 #'invoice_number' => $options{'invnum'},
675 'customer_id' => $self->custnum,
677 'reference' => $cust_pay_pending->paypendingnum, #for now
678 'callback_url' => $payment_gateway->gateway_callback_url,
679 'cancel_url' => $payment_gateway->gateway_cancel_url,
684 $cust_pay_pending->status('pending');
685 my $cpp_pending_err = $cust_pay_pending->replace;
686 return $cpp_pending_err if $cpp_pending_err;
688 warn Dumper($transaction) if $DEBUG > 2;
690 unless ( $BOP_TESTING ) {
691 $transaction->test_transaction(1)
692 if $conf->exists('business-onlinepayment-test_transaction');
693 $transaction->submit();
695 if ( $BOP_TESTING_SUCCESS ) {
696 $transaction->is_success(1);
697 $transaction->authorization('fake auth');
699 $transaction->is_success(0);
700 $transaction->error_message('fake failure');
704 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
706 $cust_pay_pending->status('thirdparty');
707 my $cpp_err = $cust_pay_pending->replace;
708 return { error => $cpp_err } if $cpp_err;
709 return { reference => $cust_pay_pending->paypendingnum,
710 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
712 } elsif ( $transaction->is_success() && $action2 ) {
714 $cust_pay_pending->status('authorized');
715 my $cpp_authorized_err = $cust_pay_pending->replace;
716 return $cpp_authorized_err if $cpp_authorized_err;
718 my $auth = $transaction->authorization;
719 my $ordernum = $transaction->can('order_number')
720 ? $transaction->order_number
724 new Business::OnlinePayment( $payment_gateway->gateway_module,
725 $self->_bop_options(\%options),
730 type => $options{method},
732 $self->_bop_auth(\%options),
733 order_number => $ordernum,
734 amount => $options{amount},
735 authorization => $auth,
736 description => $options{'description'},
739 foreach my $field (qw( authorization_source_code returned_ACI
740 transaction_identifier validation_code
741 transaction_sequence_num local_transaction_date
742 local_transaction_time AVS_result_code )) {
743 $capture{$field} = $transaction->$field() if $transaction->can($field);
746 $capture->content( %capture );
748 $capture->test_transaction(1)
749 if $conf->exists('business-onlinepayment-test_transaction');
752 unless ( $capture->is_success ) {
753 my $e = "Authorization successful but capture failed, custnum #".
754 $self->custnum. ': '. $capture->result_code.
755 ": ". $capture->error_message;
763 # remove paycvv after initial transaction
766 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
767 if ( length($self->paycvv)
768 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
770 my $error = $self->remove_cvv;
772 warn "WARNING: error removing cvv: $error\n";
781 if ( $transaction->can('card_token') && $transaction->card_token ) {
783 if ( $options{'payinfo'} eq $self->payinfo ) {
784 $self->payinfo($transaction->card_token);
785 my $error = $self->replace;
787 warn "WARNING: error storing token: $error, but proceeding anyway\n";
797 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
809 if (ref($_[0]) eq 'HASH') {
812 my ( $method, $amount ) = ( shift, shift );
814 $options{method} = $method;
815 $options{amount} = $amount;
818 if ( $options{'fake_failure'} ) {
819 return "Error: No error; test failure requested with fake_failure";
822 my $cust_pay = new FS::cust_pay ( {
823 'custnum' => $self->custnum,
824 'invnum' => $options{'invnum'},
825 'paid' => $options{amount},
827 'payby' => $bop_method2payby{$options{method}},
828 #'payinfo' => $payinfo,
829 'payinfo' => '4111111111111111',
830 #'paydate' => $paydate,
831 'paydate' => '2012-05-01',
832 'processor' => 'FakeProcessor',
834 'order_number' => '32',
836 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
839 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
840 warn " $_ => $options{$_}\n" foreach keys %options;
843 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
846 $cust_pay->invnum(''); #try again with no specific invnum
847 my $error2 = $cust_pay->insert( $options{'manual'} ?
848 ( 'manual' => 1 ) : ()
851 # gah, even with transactions.
852 my $e = 'WARNING: Card/ACH debited but database not updated - '.
853 "error inserting (fake!) payment: $error2".
854 " (previously tried insert with invnum #$options{'invnum'}" .
861 if ( $options{'paynum_ref'} ) {
862 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
870 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
872 # Wraps up processing of a realtime credit card or ACH (electronic check)
875 sub _realtime_bop_result {
876 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
878 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
881 warn "$me _realtime_bop_result: pending transaction ".
882 $cust_pay_pending->paypendingnum. "\n";
883 warn " $_ => $options{$_}\n" foreach keys %options;
886 my $payment_gateway = $options{payment_gateway}
887 or return "no payment gateway in arguments to _realtime_bop_result";
889 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
890 my $cpp_captured_err = $cust_pay_pending->replace;
891 return $cpp_captured_err if $cpp_captured_err;
893 if ( $transaction->is_success() ) {
895 my $order_number = $transaction->order_number
896 if $transaction->can('order_number');
898 my $cust_pay = new FS::cust_pay ( {
899 'custnum' => $self->custnum,
900 'invnum' => $options{'invnum'},
901 'paid' => $cust_pay_pending->paid,
903 'payby' => $cust_pay_pending->payby,
904 'payinfo' => $options{'payinfo'},
905 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
906 'paydate' => $cust_pay_pending->paydate,
907 'pkgnum' => $cust_pay_pending->pkgnum,
908 'discount_term' => $options{'discount_term'},
909 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
910 'processor' => $payment_gateway->gateway_module,
911 'auth' => $transaction->authorization,
912 'order_number' => $order_number || '',
913 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
915 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
916 $cust_pay->payunique( $options{payunique} )
917 if defined($options{payunique}) && length($options{payunique});
919 my $oldAutoCommit = $FS::UID::AutoCommit;
920 local $FS::UID::AutoCommit = 0;
923 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
925 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
928 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
929 $cust_pay->invnum(''); #try again with no specific invnum
930 $cust_pay->paynum('');
931 my $error2 = $cust_pay->insert( $options{'manual'} ?
932 ( 'manual' => 1 ) : ()
935 # gah. but at least we have a record of the state we had to abort in
936 # from cust_pay_pending now.
937 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
938 my $e = "WARNING: $options{method} captured but payment not recorded -".
939 " error inserting payment (". $payment_gateway->gateway_module.
941 " (previously tried insert with invnum #$options{'invnum'}" .
942 ": $error ) - pending payment saved as paypendingnum ".
943 $cust_pay_pending->paypendingnum. "\n";
949 my $jobnum = $cust_pay_pending->jobnum;
951 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
953 unless ( $placeholder ) {
954 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
955 my $e = "WARNING: $options{method} captured but job $jobnum not ".
956 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
961 $error = $placeholder->delete;
964 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
965 my $e = "WARNING: $options{method} captured but could not delete ".
966 "job $jobnum for paypendingnum ".
967 $cust_pay_pending->paypendingnum. ": $error\n";
972 $cust_pay_pending->set('jobnum','');
976 if ( $options{'paynum_ref'} ) {
977 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
980 $cust_pay_pending->status('done');
981 $cust_pay_pending->statustext('captured');
982 $cust_pay_pending->paynum($cust_pay->paynum);
983 my $cpp_done_err = $cust_pay_pending->replace;
985 if ( $cpp_done_err ) {
987 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
988 my $e = "WARNING: $options{method} captured but payment not recorded - ".
989 "error updating status for paypendingnum ".
990 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
996 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
998 if ( $options{'apply'} ) {
999 my $apply_error = $self->apply_payments_and_credits;
1000 if ( $apply_error ) {
1001 warn "WARNING: error applying payment: $apply_error\n";
1002 #but we still should return no error cause the payment otherwise went
1007 # have a CC surcharge portion --> one-time charge
1008 if ( $options{'cc_surcharge'} > 0 ) {
1009 # XXX: this whole block needs to be in a transaction?
1012 $invnum = $options{'invnum'} if $options{'invnum'};
1013 unless ( $invnum ) { # probably from a payment screen
1014 # do we have any open invoices? pick earliest
1015 # uses the fact that cust_main->cust_bill sorts by date ascending
1016 my @open = $self->open_cust_bill;
1017 $invnum = $open[0]->invnum if scalar(@open);
1020 unless ( $invnum ) { # still nothing? pick last closed invoice
1021 # again uses fact that cust_main->cust_bill sorts by date ascending
1022 my @closed = $self->cust_bill;
1023 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1026 unless ( $invnum ) {
1027 # XXX: unlikely case - pre-paying before any invoices generated
1028 # what it should do is create a new invoice and pick it
1029 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1034 my $charge_error = $self->charge({
1035 'amount' => $options{'cc_surcharge'},
1036 'pkg' => 'Credit Card Surcharge',
1038 'cust_pkg_ref' => \$cust_pkg,
1041 warn 'Unable to add CC surcharge cust_pkg';
1045 $cust_pkg->setup(time);
1046 my $cp_error = $cust_pkg->replace;
1048 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1052 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1053 unless ( $cust_bill ) {
1054 warn "race condition + invoice deletion just happened";
1059 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1061 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1065 return ''; #no error
1071 my $perror = $transaction->error_message;
1072 #$payment_gateway->gateway_module. " error: ".
1073 # removed for conciseness
1075 my $jobnum = $cust_pay_pending->jobnum;
1077 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1079 if ( $placeholder ) {
1080 my $error = $placeholder->depended_delete;
1081 $error ||= $placeholder->delete;
1082 $cust_pay_pending->set('jobnum','');
1083 warn "error removing provisioning jobs after declined paypendingnum ".
1084 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1086 my $e = "error finding job $jobnum for declined paypendingnum ".
1087 $cust_pay_pending->paypendingnum. "\n";
1093 unless ( $transaction->error_message ) {
1096 if ( $transaction->can('response_page') ) {
1098 'page' => ( $transaction->can('response_page')
1099 ? $transaction->response_page
1102 'code' => ( $transaction->can('response_code')
1103 ? $transaction->response_code
1106 'headers' => ( $transaction->can('response_headers')
1107 ? $transaction->response_headers
1113 "No additional debugging information available for ".
1114 $payment_gateway->gateway_module;
1117 $perror .= "No error_message returned from ".
1118 $payment_gateway->gateway_module. " -- ".
1119 ( ref($t_response) ? Dumper($t_response) : $t_response );
1123 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1124 && $conf->exists('emaildecline', $self->agentnum)
1125 && grep { $_ ne 'POST' } $self->invoicing_list
1126 && ! grep { $transaction->error_message =~ /$_/ }
1127 $conf->config('emaildecline-exclude', $self->agentnum)
1130 # Send a decline alert to the customer.
1131 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1134 # include the raw error message in the transaction state
1135 $cust_pay_pending->setfield('error', $transaction->error_message);
1136 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1137 $error = $msg_template->send( 'cust_main' => $self,
1138 'object' => $cust_pay_pending );
1142 $perror .= " (also received error sending decline notification: $error)"
1147 $cust_pay_pending->status('done');
1148 $cust_pay_pending->statustext($perror);
1149 #'declined:': no, that's failure_status
1150 if ( $transaction->can('failure_status') ) {
1151 $cust_pay_pending->failure_status( $transaction->failure_status );
1153 my $cpp_done_err = $cust_pay_pending->replace;
1154 if ( $cpp_done_err ) {
1155 my $e = "WARNING: $options{method} declined but pending payment not ".
1156 "resolved - error updating status for paypendingnum ".
1157 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1159 $perror = "$e ($perror)";
1167 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1169 Verifies successful third party processing of a realtime credit card or
1170 ACH (electronic check) transaction via a
1171 Business::OnlineThirdPartyPayment realtime gateway. See
1172 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1174 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1176 The additional options I<payname>, I<city>, I<state>,
1177 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1178 if set, will override the value from the customer record.
1180 I<description> is a free-text field passed to the gateway. It defaults to
1181 "Internet services".
1183 If an I<invnum> is specified, this payment (if successful) is applied to the
1184 specified invoice. If you don't specify an I<invnum> you might want to
1185 call the B<apply_payments> method.
1187 I<quiet> can be set true to surpress email decline notices.
1189 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1190 resulting paynum, if any.
1192 I<payunique> is a unique identifier for this payment.
1194 Returns a hashref containing elements bill_error (which will be undefined
1195 upon success) and session_id of any associated session.
1199 sub realtime_botpp_capture {
1200 my( $self, $cust_pay_pending, %options ) = @_;
1202 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1205 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1206 warn " $_ => $options{$_}\n" foreach keys %options;
1209 eval "use Business::OnlineThirdPartyPayment";
1213 # select the gateway
1216 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1218 my $payment_gateway;
1219 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1220 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1221 { gatewaynum => $gatewaynum }
1223 : $self->agent->payment_gateway( 'method' => $method,
1224 # 'invnum' => $cust_pay_pending->invnum,
1225 # 'payinfo' => $cust_pay_pending->payinfo,
1228 $options{payment_gateway} = $payment_gateway; # for the helper subs
1234 my @invoicing_list = $self->invoicing_list_emailonly;
1235 if ( $conf->exists('emailinvoiceautoalways')
1236 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1237 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1238 push @invoicing_list, $self->all_emails;
1241 my $email = ($conf->exists('business-onlinepayment-email-override'))
1242 ? $conf->config('business-onlinepayment-email-override')
1243 : $invoicing_list[0];
1247 $content{email_customer} =
1248 ( $conf->exists('business-onlinepayment-email_customer')
1249 || $conf->exists('business-onlinepayment-email-override') );
1252 # run transaction(s)
1256 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1257 $self->_bop_options(\%options),
1260 $transaction->reference({ %options });
1262 $transaction->content(
1264 $self->_bop_auth(\%options),
1265 'action' => 'Post Authorization',
1266 'description' => $options{'description'},
1267 'amount' => $cust_pay_pending->paid,
1268 #'invoice_number' => $options{'invnum'},
1269 'customer_id' => $self->custnum,
1270 'reference' => $cust_pay_pending->paypendingnum,
1272 'phone' => $self->daytime || $self->night,
1274 # plus whatever is required for bogus capture avoidance
1277 $transaction->submit();
1280 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1282 if ( $options{'apply'} ) {
1283 my $apply_error = $self->apply_payments_and_credits;
1284 if ( $apply_error ) {
1285 warn "WARNING: error applying payment: $apply_error\n";
1290 bill_error => $error,
1291 session_id => $cust_pay_pending->session_id,
1296 =item default_payment_gateway
1298 DEPRECATED -- use agent->payment_gateway
1302 sub default_payment_gateway {
1303 my( $self, $method ) = @_;
1305 die "Real-time processing not enabled\n"
1306 unless $conf->exists('business-onlinepayment');
1308 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1311 my $bop_config = 'business-onlinepayment';
1312 $bop_config .= '-ach'
1313 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1314 my ( $processor, $login, $password, $action, @bop_options ) =
1315 $conf->config($bop_config);
1316 $action ||= 'normal authorization';
1317 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1318 die "No real-time processor is enabled - ".
1319 "did you set the business-onlinepayment configuration value?\n"
1322 ( $processor, $login, $password, $action, @bop_options )
1325 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1327 Refunds a realtime credit card or ACH (electronic check) transaction
1328 via a Business::OnlinePayment realtime gateway. See
1329 L<http://420.am/business-onlinepayment> for supported gateways.
1331 Available methods are: I<CC> or I<ECHECK>
1333 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1335 Most gateways require a reference to an original payment transaction to refund,
1336 so you probably need to specify a I<paynum>.
1338 I<amount> defaults to the original amount of the payment if not specified.
1340 I<reasonnum> specified an existing refund reason for the refund
1342 I<paydate> specifies the expiration date for a credit card overriding the
1343 value from the customer record or the payment record. Specified as yyyy-mm-dd
1345 Implementation note: If I<amount> is unspecified or equal to the amount of the
1346 orignal payment, first an attempt is made to "void" the transaction via
1347 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1348 the normal attempt is made to "refund" ("credit") the transaction via the
1349 gateway is attempted. No attempt to "void" the transaction is made if the
1350 gateway has introspection data and doesn't support void.
1352 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1353 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1354 #if set, will override the value from the customer record.
1356 #If an I<invnum> is specified, this payment (if successful) is applied to the
1357 #specified invoice. If you don't specify an I<invnum> you might want to
1358 #call the B<apply_payments> method.
1362 #some false laziness w/realtime_bop, not enough to make it worth merging
1363 #but some useful small subs should be pulled out
1364 sub realtime_refund_bop {
1367 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1370 if (ref($_[0]) eq 'HASH') {
1371 %options = %{$_[0]};
1375 $options{method} = $method;
1379 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1380 warn " $_ => $options{$_}\n" foreach keys %options;
1383 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1388 # look up the original payment and optionally a gateway for that payment
1392 my $amount = $options{'amount'};
1394 my( $processor, $login, $password, @bop_options, $namespace ) ;
1395 my( $auth, $order_number ) = ( '', '', '' );
1396 my $gatewaynum = '';
1398 if ( $options{'paynum'} ) {
1400 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1401 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1402 or return "Unknown paynum $options{'paynum'}";
1403 $amount ||= $cust_pay->paid;
1405 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1406 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1408 if ( $cust_pay->get('processor') ) {
1409 ($gatewaynum, $processor, $auth, $order_number) =
1411 $cust_pay->gatewaynum,
1412 $cust_pay->processor,
1414 $cust_pay->order_number,
1417 # this payment wasn't upgraded, which probably means this won't work,
1419 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1420 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1421 $cust_pay->paybatch;
1422 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1425 if ( $gatewaynum ) { #gateway for the payment to be refunded
1427 my $payment_gateway =
1428 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1429 die "payment gateway $gatewaynum not found"
1430 unless $payment_gateway;
1432 $processor = $payment_gateway->gateway_module;
1433 $login = $payment_gateway->gateway_username;
1434 $password = $payment_gateway->gateway_password;
1435 $namespace = $payment_gateway->gateway_namespace;
1436 @bop_options = $payment_gateway->options;
1438 } else { #try the default gateway
1441 my $payment_gateway =
1442 $self->agent->payment_gateway('method' => $options{method});
1444 ( $conf_processor, $login, $password, $namespace ) =
1445 map { my $method = "gateway_$_"; $payment_gateway->$method }
1446 qw( module username password namespace );
1448 @bop_options = $payment_gateway->gatewaynum
1449 ? $payment_gateway->options
1450 : @{ $payment_gateway->get('options') };
1452 return "processor of payment $options{'paynum'} $processor does not".
1453 " match default processor $conf_processor"
1454 unless $processor eq $conf_processor;
1459 } else { # didn't specify a paynum, so look for agent gateway overrides
1460 # like a normal transaction
1462 my $payment_gateway =
1463 $self->agent->payment_gateway( 'method' => $options{method},
1464 #'payinfo' => $payinfo,
1466 my( $processor, $login, $password, $namespace ) =
1467 map { my $method = "gateway_$_"; $payment_gateway->$method }
1468 qw( module username password namespace );
1470 my @bop_options = $payment_gateway->gatewaynum
1471 ? $payment_gateway->options
1472 : @{ $payment_gateway->get('options') };
1475 return "neither amount nor paynum specified" unless $amount;
1477 eval "use $namespace";
1482 'type' => $options{method},
1484 'password' => $password,
1485 'order_number' => $order_number,
1486 'amount' => $amount,
1488 $content{authorization} = $auth
1489 if length($auth); #echeck/ACH transactions have an order # but no auth
1490 #(at least with authorize.net)
1492 my $currency = $conf->exists('business-onlinepayment-currency')
1493 && $conf->config('business-onlinepayment-currency');
1494 $content{currency} = $currency if $currency;
1496 my $disable_void_after;
1497 if ($conf->exists('disable_void_after')
1498 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1499 $disable_void_after = $1;
1502 #first try void if applicable
1503 my $void = new Business::OnlinePayment( $processor, @bop_options );
1506 if ($void->can('info')) {
1508 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1509 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1510 my %supported_actions = $void->info('supported_actions');
1512 if ( %supported_actions && $paytype
1513 && defined($supported_actions{$paytype})
1514 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1517 if ( $cust_pay && $cust_pay->paid == $amount
1519 ( not defined($disable_void_after) )
1520 || ( time < ($cust_pay->_date + $disable_void_after ) )
1524 warn " attempting void\n" if $DEBUG > 1;
1525 if ( $void->can('info') ) {
1526 if ( $cust_pay->payby eq 'CARD'
1527 && $void->info('CC_void_requires_card') )
1529 $content{'card_number'} = $cust_pay->payinfo;
1530 } elsif ( $cust_pay->payby eq 'CHEK'
1531 && $void->info('ECHECK_void_requires_account') )
1533 ( $content{'account_number'}, $content{'routing_code'} ) =
1534 split('@', $cust_pay->payinfo);
1535 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1538 $void->content( 'action' => 'void', %content );
1539 $void->test_transaction(1)
1540 if $conf->exists('business-onlinepayment-test_transaction');
1542 if ( $void->is_success ) {
1543 # specified as a refund reason, but now we want a payment void reason
1544 # extract just the reason text, let cust_pay::void handle new_or_existing
1545 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1547 $error = 'Reason could not be loaded' unless $reason;
1548 $error = $cust_pay->void($reason->reason) unless $error;
1550 # gah, even with transactions.
1551 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1552 "error voiding payment: $error";
1556 warn " void successful\n" if $DEBUG > 1;
1561 warn " void unsuccessful, trying refund\n"
1565 my $address = $self->address1;
1566 $address .= ", ". $self->address2 if $self->address2;
1568 my($payname, $payfirst, $paylast);
1569 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1570 $payname = $self->payname;
1571 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1572 or return "Illegal payname $payname";
1573 ($payfirst, $paylast) = ($1, $2);
1575 $payfirst = $self->getfield('first');
1576 $paylast = $self->getfield('last');
1577 $payname = "$payfirst $paylast";
1580 my @invoicing_list = $self->invoicing_list_emailonly;
1581 if ( $conf->exists('emailinvoiceautoalways')
1582 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1583 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1584 push @invoicing_list, $self->all_emails;
1587 my $email = ($conf->exists('business-onlinepayment-email-override'))
1588 ? $conf->config('business-onlinepayment-email-override')
1589 : $invoicing_list[0];
1591 my $payip = exists($options{'payip'})
1594 $content{customer_ip} = $payip
1598 if ( $options{method} eq 'CC' ) {
1601 $content{card_number} = $payinfo = $cust_pay->payinfo;
1602 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1603 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1604 ($content{expiration} = "$2/$1"); # where available
1606 $content{card_number} = $payinfo = $self->payinfo;
1607 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1608 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1609 $content{expiration} = "$2/$1";
1612 } elsif ( $options{method} eq 'ECHECK' ) {
1615 $payinfo = $cust_pay->payinfo;
1617 $payinfo = $self->payinfo;
1619 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1620 $content{bank_name} = $self->payname;
1621 $content{account_type} = 'CHECKING';
1622 $content{account_name} = $payname;
1623 $content{customer_org} = $self->company ? 'B' : 'I';
1624 $content{customer_ssn} = $self->ss;
1629 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1630 my %sub_content = $refund->content(
1631 'action' => 'credit',
1632 'customer_id' => $self->custnum,
1633 'last_name' => $paylast,
1634 'first_name' => $payfirst,
1636 'address' => $address,
1637 'city' => $self->city,
1638 'state' => $self->state,
1639 'zip' => $self->zip,
1640 'country' => $self->country,
1642 'phone' => $self->daytime || $self->night,
1645 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1647 $refund->test_transaction(1)
1648 if $conf->exists('business-onlinepayment-test_transaction');
1651 return "$processor error: ". $refund->error_message
1652 unless $refund->is_success();
1654 $order_number = $refund->order_number if $refund->can('order_number');
1656 # change this to just use $cust_pay->delete_cust_bill_pay?
1657 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1658 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1659 last unless @cust_bill_pay;
1660 my $cust_bill_pay = pop @cust_bill_pay;
1661 my $error = $cust_bill_pay->delete;
1665 my $cust_refund = new FS::cust_refund ( {
1666 'custnum' => $self->custnum,
1667 'paynum' => $options{'paynum'},
1668 'source_paynum' => $options{'paynum'},
1669 'refund' => $amount,
1671 'payby' => $bop_method2payby{$options{method}},
1672 'payinfo' => $payinfo,
1673 'reasonnum' => $options{'reasonnum'},
1674 'gatewaynum' => $gatewaynum, # may be null
1675 'processor' => $processor,
1676 'auth' => $refund->authorization,
1677 'order_number' => $order_number,
1679 my $error = $cust_refund->insert;
1681 $cust_refund->paynum(''); #try again with no specific paynum
1682 $cust_refund->source_paynum('');
1683 my $error2 = $cust_refund->insert;
1685 # gah, even with transactions.
1686 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1687 "error inserting refund ($processor): $error2".
1688 " (previously tried insert with paynum #$options{'paynum'}" .
1699 =item realtime_verify_bop [ OPTION => VALUE ... ]
1701 Runs an authorization-only transaction for $1 against this credit card (if
1702 successful, immediatly reverses the authorization).
1704 Returns the empty string if the authorization was sucessful, or an error
1711 I<paydate> specifies the expiration date for a credit card overriding the
1712 value from the customer record or the payment record. Specified as yyyy-mm-dd
1714 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1715 #I<zip> are also available. Any of these options,
1716 #if set, will override the value from the customer record.
1720 #Available methods are: I<CC> or I<ECHECK>
1722 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1723 #it worth merging but some useful small subs should be pulled out
1724 sub realtime_verify_bop {
1727 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1730 if (ref($_[0]) eq 'HASH') {
1731 %options = %{$_[0]};
1737 warn "$me realtime_verify_bop\n";
1738 warn " $_ => $options{$_}\n" foreach keys %options;
1745 my $payment_gateway = $self->_payment_gateway( \%options );
1746 my $namespace = $payment_gateway->gateway_namespace;
1748 eval "use $namespace";
1752 # check for banned credit card/ACH
1755 my $ban = FS::banned_pay->ban_search(
1756 'payby' => $bop_method2payby{'CC'},
1757 'payinfo' => $options{payinfo},
1759 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1765 my $bop_content = $self->_bop_content(\%options);
1766 return $bop_content unless ref($bop_content);
1768 my @invoicing_list = $self->invoicing_list_emailonly;
1769 if ( $conf->exists('emailinvoiceautoalways')
1770 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1771 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1772 push @invoicing_list, $self->all_emails;
1775 my $email = ($conf->exists('business-onlinepayment-email-override'))
1776 ? $conf->config('business-onlinepayment-email-override')
1777 : $invoicing_list[0];
1782 if ( $namespace eq 'Business::OnlinePayment' ) {
1784 if ( $options{method} eq 'CC' ) {
1786 $content{card_number} = $options{payinfo};
1787 $paydate = exists($options{'paydate'})
1788 ? $options{'paydate'}
1790 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1791 $content{expiration} = "$2/$1";
1793 my $paycvv = exists($options{'paycvv'})
1794 ? $options{'paycvv'}
1796 $content{cvv2} = $paycvv
1799 my $paystart_month = exists($options{'paystart_month'})
1800 ? $options{'paystart_month'}
1801 : $self->paystart_month;
1803 my $paystart_year = exists($options{'paystart_year'})
1804 ? $options{'paystart_year'}
1805 : $self->paystart_year;
1807 $content{card_start} = "$paystart_month/$paystart_year"
1808 if $paystart_month && $paystart_year;
1810 my $payissue = exists($options{'payissue'})
1811 ? $options{'payissue'}
1813 $content{issue_number} = $payissue if $payissue;
1815 } elsif ( $options{method} eq 'ECHECK' ){
1817 #nop for checks (though it shouldn't be called...)
1820 die "unknown method ". $options{method};
1823 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1826 die "unknown namespace $namespace";
1830 # run transaction(s)
1833 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
1834 $self->select_for_update; #mutex ... just until we get our pending record in
1835 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
1837 #the checks here are intended to catch concurrent payments
1838 #double-form-submission prevention is taken care of in cust_pay_pending::check
1840 #also check and make sure there aren't *other* pending payments for this cust
1842 my @pending = qsearch('cust_pay_pending', {
1843 'custnum' => $self->custnum,
1844 'status' => { op=>'!=', value=>'done' }
1847 return "A payment is already being processed for this customer (".
1848 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
1849 "); verification transaction aborted."
1850 if scalar(@pending);
1852 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
1854 my $cust_pay_pending = new FS::cust_pay_pending {
1855 'custnum' => $self->custnum,
1858 'payby' => $bop_method2payby{'CC'},
1859 'payinfo' => $options{payinfo},
1860 'paymask' => $options{paymask},
1861 'paydate' => $paydate,
1862 #'recurring_billing' => $content{recurring_billing},
1863 'pkgnum' => $options{'pkgnum'},
1865 'gatewaynum' => $payment_gateway->gatewaynum || '',
1866 'session_id' => $options{session_id} || '',
1867 #'jobnum' => $options{depend_jobnum} || '',
1869 $cust_pay_pending->payunique( $options{payunique} )
1870 if defined($options{payunique}) && length($options{payunique});
1872 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1874 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
1875 return $cpp_new_err if $cpp_new_err;
1877 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1879 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1881 my $transaction = new $namespace( $payment_gateway->gateway_module,
1882 $self->_bop_options(\%options),
1885 $transaction->content(
1887 $self->_bop_auth(\%options),
1888 'action' => 'Authorization Only',
1889 'description' => $options{'description'},
1891 #'invoice_number' => $options{'invnum'},
1892 'customer_id' => $self->custnum,
1894 'reference' => $cust_pay_pending->paypendingnum, #for now
1895 'callback_url' => $payment_gateway->gateway_callback_url,
1896 'cancel_url' => $payment_gateway->gateway_cancel_url,
1901 $cust_pay_pending->status('pending');
1902 my $cpp_pending_err = $cust_pay_pending->replace;
1903 return $cpp_pending_err if $cpp_pending_err;
1905 warn Dumper($transaction) if $DEBUG > 2;
1907 unless ( $BOP_TESTING ) {
1908 $transaction->test_transaction(1)
1909 if $conf->exists('business-onlinepayment-test_transaction');
1910 $transaction->submit();
1912 if ( $BOP_TESTING_SUCCESS ) {
1913 $transaction->is_success(1);
1914 $transaction->authorization('fake auth');
1916 $transaction->is_success(0);
1917 $transaction->error_message('fake failure');
1921 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1923 if ( $transaction->is_success() ) {
1925 $cust_pay_pending->status('authorized');
1926 my $cpp_authorized_err = $cust_pay_pending->replace;
1927 return $cpp_authorized_err if $cpp_authorized_err;
1929 my $auth = $transaction->authorization;
1930 my $ordernum = $transaction->can('order_number')
1931 ? $transaction->order_number
1934 my $reverse = new $namespace( $payment_gateway->gateway_module,
1935 $self->_bop_options(\%options),
1938 $reverse->content( 'action' => 'Reverse Authorization',
1939 $self->_bop_auth(\%options),
1943 'authorization' => $transaction->authorization,
1944 'order_number' => $ordernum,
1947 'result_code' => $transaction->result_code,
1948 'txn_date' => $transaction->txn_date,
1952 $reverse->test_transaction(1)
1953 if $conf->exists('business-onlinepayment-test_transaction');
1956 if ( $reverse->is_success ) {
1958 $cust_pay_pending->status('done');
1959 my $cpp_authorized_err = $cust_pay_pending->replace;
1960 return $cpp_authorized_err if $cpp_authorized_err;
1964 my $e = "Authorization successful but reversal failed, custnum #".
1965 $self->custnum. ': '. $reverse->result_code.
1966 ": ". $reverse->error_message;
1973 ### Address Verification ###
1975 # Single-letter codes vary by cardtype.
1977 # Erring on the side of accepting cards if avs is not available,
1978 # only rejecting if avs occurred and there's been an explicit mismatch
1980 # Charts below taken from vSecure documentation,
1981 # shows codes for Amex/Dscv/MC/Visa
1983 # ACCEPTABLE AVS RESPONSES:
1984 # Both Address and 5-digit postal code match Y A Y Y
1985 # Both address and 9-digit postal code match Y A X Y
1986 # United Kingdom – Address and postal code match _ _ _ F
1987 # International transaction – Address and postal code match _ _ _ D/M
1989 # ACCEPTABLE, BUT ISSUE A WARNING:
1990 # Ineligible transaction; or message contains a content error _ _ _ E
1991 # System unavailable; retry R U R R
1992 # Information unavailable U W U U
1993 # Issuer does not support AVS S U S S
1994 # AVS is not applicable _ _ _ S
1995 # Incompatible formats – Not verified _ _ _ C
1996 # Incompatible formats – Address not verified; postal code matches _ _ _ P
1997 # International transaction – address not verified _ G _ G/I
1999 # UNACCEPTABLE AVS RESPONSES:
2000 # Only Address matches A Y A A
2001 # Only 5-digit postal code matches Z Z Z Z
2002 # Only 9-digit postal code matches Z Z W W
2003 # Neither address nor postal code matches N N N N
2005 if (my $avscode = uc($transaction->avs_code)) {
2007 # map codes to accept/warn/reject
2009 'American Express card' => {
2018 'Discover card' => {
2057 my $cardtype = cardtype($content{card_number});
2058 if ($avs->{$cardtype}) {
2059 my $avsact = $avs->{$cardtype}->{$avscode};
2061 if ($avsact eq 'r') {
2062 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2063 } elsif ($avsact eq 'w') {
2064 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2065 } elsif (!$avsact) {
2066 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2067 } # else $avsact eq 'a'
2069 $log->warning($warning);
2072 } # else $cardtype avs handling not implemented
2073 } # else !$transaction->avs_code
2075 } else { # is not success
2077 # status is 'done' not 'declined', as in _realtime_bop_result
2078 $cust_pay_pending->status('done');
2079 $cust_pay_pending->statustext( $transaction->error_message || 'Unknown error' );
2080 # could also record failure_status here,
2081 # but it's not supported by B::OP::vSecureProcessing...
2082 # need a B::OP module with (reverse) auth only to test it with
2083 my $cpp_declined_err = $cust_pay_pending->replace;
2084 return $cpp_declined_err if $cpp_declined_err;
2092 if ( $transaction->can('card_token') && $transaction->card_token ) {
2094 if ( $options{'payinfo'} eq $self->payinfo ) {
2095 $self->payinfo($transaction->card_token);
2096 my $error = $self->replace;
2098 my $warning = "WARNING: error storing token: $error, but proceeding anyway\n";
2099 $log->warning($warning);
2110 $transaction->is_success() ? '' : $transaction->error_message();
2122 L<FS::cust_main>, L<FS::cust_main::Billing>