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.35;
9 use FS::UID qw( dbh myconnect );
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 # Currently only used by ClientAPI
115 # NOT 4.x COMPATIBLE (see below)
116 sub realtime_collect {
117 my( $self, %options ) = @_;
119 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
122 warn "$me realtime_collect:\n";
123 warn " $_ => $options{$_}\n" foreach keys %options;
126 $options{amount} = $self->balance unless exists( $options{amount} );
127 return '' unless $options{amount} > 0;
129 #### NOT 4.x COMPATIBLE
130 $options{method} = FS::payby->payby2bop($self->payby)
131 unless exists( $options{method} );
133 return $self->realtime_bop({%options});
137 =item realtime_bop { [ ARG => VALUE ... ] }
139 Runs a realtime credit card or ACH (electronic check) transaction
140 via a Business::OnlinePayment realtime gateway. See
141 L<http://420.am/business-onlinepayment> for supported gateways.
143 Required arguments in the hashref are I<amount> and either
144 I<cust_payby> or I<method>, I<payinfo> and (as applicable for method)
145 I<payname>, I<address1>, I<address2>, I<city>, I<state>, I<zip> and I<paydate>.
147 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
149 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
151 I<description> is a free-text field passed to the gateway. It defaults to
152 the value defined by the business-onlinepayment-description configuration
153 option, or "Internet services" if that is unset.
155 If an I<invnum> is specified, this payment (if successful) is applied to the
156 specified invoice. If the customer has exactly one open invoice, that
157 invoice number will be assumed. If you don't specify an I<invnum> you might
158 want to call the B<apply_payments> method or set the I<apply> option.
160 I<no_invnum> can be set to true to prevent that default invnum from being set.
162 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
164 I<no_auto_apply> can be set to true to set that flag on the resulting payment
165 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
166 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
168 I<quiet> can be set true to surpress email decline notices.
170 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
171 resulting paynum, if any.
173 I<payunique> is a unique identifier for this payment.
175 I<session_id> is a session identifier associated with this payment.
177 I<depend_jobnum> allows payment capture to unlock export jobs
179 I<discount_term> attempts to take a discount by prepaying for discount_term.
180 The payment will fail if I<amount> is incorrect for this discount term.
182 A direct (Business::OnlinePayment) transaction will return nothing on success,
183 or an error message on failure.
185 A third-party transaction will return a hashref containing:
187 - popup_url: the URL to which a browser should be redirected to complete
189 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
190 - reference: a reference ID for the transaction, to show the customer.
192 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
196 # some helper routines
198 # _bop_recurring_billing: Checks whether this payment should have the
199 # recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
200 # vSecure, etc.). This works in two different modes:
201 # - actual_oncard (default): treat the payment as recurring if the customer
202 # has made a payment using this card before.
203 # - transaction_is_recur: treat the payment as recurring if the invoice
204 # being paid has any recurring package charges.
206 sub _bop_recurring_billing {
207 my( $self, %opt ) = @_;
209 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
211 if ( defined($method) && $method eq 'transaction_is_recur' ) {
213 return 1 if $opt{'trans_is_recur'};
217 # return 1 if the payinfo has been used for another payment
218 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
226 #can run safely as class method if opt payment_gateway already exists
227 sub _payment_gateway {
228 my ($self, $options) = @_;
230 if ( $options->{'fake_gatewaynum'} ) {
231 $options->{payment_gateway} =
232 qsearchs('payment_gateway',
233 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
237 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
238 unless exists($options->{payment_gateway});
240 $options->{payment_gateway};
248 'login' => $options->{payment_gateway}->gateway_username,
249 'password' => $options->{payment_gateway}->gateway_password,
257 $options->{payment_gateway}->gatewaynum
258 ? $options->{payment_gateway}->options
259 : @{ $options->{payment_gateway}->get('options') };
264 my ($self, $options) = @_;
266 unless ( $options->{'description'} ) {
267 if ( $conf->exists('business-onlinepayment-description') ) {
268 my $dtempl = $conf->config('business-onlinepayment-description');
270 my $agent = $self->agent->agent;
272 $options->{'description'} = eval qq("$dtempl");
274 $options->{'description'} = 'Internet services';
278 # Default invoice number if the customer has exactly one open invoice.
279 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
280 $options->{'invnum'} = '';
281 my @open = $self->open_cust_bill;
282 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
288 sub _bop_cust_payby_options {
290 my $cust_payby = $options->{'cust_payby'};
293 $options->{'method'} = FS::payby->payby2bop( $cust_payby->payby );
295 if ($cust_payby->payby =~ /^(CARD|DCRD)$/) {
296 # false laziness with cust_payby->check
297 # which might not have been run yet
299 if ( $cust_payby->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
300 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
301 } elsif ( $cust_payby->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
302 ( $m, $y ) = ( $2, "19$1" );
303 } elsif ( $cust_payby->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
304 ( $m, $y ) = ( $3, "20$2" );
306 return "Illegal expiration date: ". $cust_payby->paydate;
308 $m = sprintf('%02d',$m);
309 $options->{paydate} = "$y-$m-01";
311 $options->{paydate} = '';
314 $options->{$_} = $cust_payby->$_()
315 for qw( payinfo paycvv paymask paystart_month paystart_year
316 payissue payname paystate paytype payip );
318 if ( $cust_payby->locationnum ) {
319 my $cust_location = $cust_payby->cust_location;
320 $options->{$_} = $cust_location->$_() for qw( address1 address2 city state zip );
325 # can be called as class method,
326 # but can't load default name/phone fields as class method
328 my ($self, $options) = @_;
331 my $payip = $options->{'payip'};
332 $content{customer_ip} = $payip if length($payip);
334 $content{invoice_number} = $options->{'invnum'}
335 if exists($options->{'invnum'}) && length($options->{'invnum'});
337 $content{email_customer} =
338 ( $conf->exists('business-onlinepayment-email_customer')
339 || $conf->exists('business-onlinepayment-email-override') );
341 my ($payname, $payfirst, $paylast);
342 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
343 ($payname = $options->{payname}) =~
344 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
345 or return "Illegal payname $payname";
346 ($payfirst, $paylast) = ($1, $2);
347 } elsif (ref($self)) { # can't set payname if called as class method
348 $payfirst = $self->getfield('first');
349 $paylast = $self->getfield('last');
350 $payname = "$payfirst $paylast";
353 $content{last_name} = $paylast if $paylast;
354 $content{first_name} = $payfirst if $payfirst;
356 $content{name} = $payname if $payname;
358 $content{address} = $options->{'address1'};
359 my $address2 = $options->{'address2'};
360 $content{address} .= ", ". $address2 if length($address2);
362 $content{city} = $options->{'city'};
363 $content{state} = $options->{'state'};
364 $content{zip} = $options->{'zip'};
365 $content{country} = $options->{'country'};
367 # can't set phone if called as class method
368 $content{phone} = $self->daytime || $self->night
371 my $currency = $conf->exists('business-onlinepayment-currency')
372 && $conf->config('business-onlinepayment-currency');
373 $content{currency} = $currency if $currency;
378 # updates payinfo and cust_payby options with token from transaction
379 # can be called as a class method
381 my ($self,$transaction,$options) = @_;
382 if ( $transaction->can('card_token')
383 and $transaction->card_token
384 and !$self->tokenized($options->{'payinfo'})
386 $options->{'payinfo'} = $transaction->card_token;
387 $options->{'cust_payby'}->payinfo($transaction->card_token) if $options->{'cust_payby'};
388 return $transaction->card_token;
393 my %bop_method2payby = (
402 confess "Can't call realtime_bop within another transaction ".
403 '($FS::UID::AutoCommit is false)'
404 unless $FS::UID::AutoCommit;
406 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
408 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_bop');
411 if (ref($_[0]) eq 'HASH') {
414 my ( $method, $amount ) = ( shift, shift );
416 $options{method} = $method;
417 $options{amount} = $amount;
420 # set fields from passed cust_payby
421 _bop_cust_payby_options(\%options);
423 # possibly run a separate transaction to tokenize card number,
424 # so that we never store tokenized card info in cust_pay_pending
425 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
426 my $token_error = $self->realtime_tokenize(\%options);
427 return $token_error if $token_error;
428 # in theory, all cust_payby will be tokenized during original save,
429 # so we shouldn't get here with opt cust_payby...but just in case...
430 if ($options{'cust_payby'} && $self->tokenized($options{'payinfo'})) {
431 $token_error = $options{'cust_payby'}->replace;
432 return $token_error if $token_error;
434 return "Cannot tokenize card info"
435 if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'});
439 # optional credit card surcharge
442 my $cc_surcharge = 0;
443 my $cc_surcharge_pct = 0;
444 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum)
445 if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
446 && $options{method} eq 'CC';
448 # always add cc surcharge if called from event
449 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
450 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
451 $options{'amount'} += $cc_surcharge;
452 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
454 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
455 # payment screen), so consider the given
456 # amount as post-surcharge
457 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
460 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
461 $options{'cc_surcharge'} = $cc_surcharge;
465 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
466 warn " cc_surcharge = $cc_surcharge\n";
469 warn " $_ => $options{$_}\n" foreach keys %options;
472 return $self->fake_bop(\%options) if $options{'fake'};
474 $self->_bop_defaults(\%options);
476 return "Missing payinfo"
477 unless $options{'payinfo'};
480 # set trans_is_recur based on invnum if there is one
483 my $trans_is_recur = 0;
484 if ( $options{'invnum'} ) {
486 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
487 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
493 $cust_bill->cust_bill_pkg;
496 if grep { $_->freq ne '0' } @part_pkg;
504 my $payment_gateway = $self->_payment_gateway( \%options );
505 my $namespace = $payment_gateway->gateway_namespace;
507 eval "use $namespace";
511 # check for banned credit card/ACH
514 my $ban = FS::banned_pay->ban_search(
515 'payby' => $bop_method2payby{$options{method}},
516 'payinfo' => $options{payinfo},
518 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
521 # check for term discount validity
524 my $discount_term = $options{discount_term};
525 if ( $discount_term ) {
526 my $bill = ($self->cust_bill)[-1]
527 or return "Can't apply a term discount to an unbilled customer";
528 my $plan = FS::discount_plan->new(
530 months => $discount_term
531 ) or return "No discount available for term '$discount_term'";
533 if ( $plan->discounted_total != $options{amount} ) {
534 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
542 my $bop_content = $self->_bop_content(\%options);
543 return $bop_content unless ref($bop_content);
545 my @invoicing_list = $self->invoicing_list_emailonly;
546 if ( $conf->exists('emailinvoiceautoalways')
547 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
548 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
549 push @invoicing_list, $self->all_emails;
552 my $email = ($conf->exists('business-onlinepayment-email-override'))
553 ? $conf->config('business-onlinepayment-email-override')
554 : $invoicing_list[0];
559 if ( $namespace eq 'Business::OnlinePayment' ) {
561 if ( $options{method} eq 'CC' ) {
563 $content{card_number} = $options{payinfo};
564 $paydate = $options{'paydate'};
565 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
566 $content{expiration} = "$2/$1";
568 $content{cvv2} = $options{'paycvv'}
569 if length($options{'paycvv'});
571 my $paystart_month = $options{'paystart_month'};
572 my $paystart_year = $options{'paystart_year'};
573 $content{card_start} = "$paystart_month/$paystart_year"
574 if $paystart_month && $paystart_year;
576 my $payissue = $options{'payissue'};
577 $content{issue_number} = $payissue if $payissue;
579 if ( $self->_bop_recurring_billing(
580 'payinfo' => $options{'payinfo'},
581 'trans_is_recur' => $trans_is_recur,
585 $content{recurring_billing} = 'YES';
586 $content{acct_code} = 'rebill'
587 if $conf->exists('credit_card-recurring_billing_acct_code');
590 } elsif ( $options{method} eq 'ECHECK' ){
592 ( $content{account_number}, $content{routing_code} ) =
593 split('@', $options{payinfo});
594 $content{bank_name} = $options{payname};
595 $content{bank_state} = $options{'paystate'};
596 $content{account_type}= uc($options{'paytype'}) || 'PERSONAL CHECKING';
598 $content{company} = $self->company if $self->company;
600 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
601 $content{account_name} = $self->company;
603 $content{account_name} = $self->getfield('first'). ' '.
604 $self->getfield('last');
607 $content{customer_org} = $self->company ? 'B' : 'I';
608 $content{state_id} = exists($options{'stateid'})
609 ? $options{'stateid'}
610 : $self->getfield('stateid');
611 $content{state_id_state} = exists($options{'stateid_state'})
612 ? $options{'stateid_state'}
613 : $self->getfield('stateid_state');
614 $content{customer_ssn} = exists($options{'ss'})
619 die "unknown method ". $options{method};
622 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
625 die "unknown namespace $namespace";
632 my $balance = exists( $options{'balance'} )
633 ? $options{'balance'}
636 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
637 $self->select_for_update; #mutex ... just until we get our pending record in
638 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
640 #the checks here are intended to catch concurrent payments
641 #double-form-submission prevention is taken care of in cust_pay_pending::check
644 return "The customer's balance has changed; $options{method} transaction aborted."
645 if $self->balance < $balance;
647 #also check and make sure there aren't *other* pending payments for this cust
649 my @pending = qsearch('cust_pay_pending', {
650 'custnum' => $self->custnum,
651 'status' => { op=>'!=', value=>'done' }
654 #for third-party payments only, remove pending payments if they're in the
655 #'thirdparty' (waiting for customer action) state.
656 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
657 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
658 my $error = $_->delete;
659 warn "error deleting unfinished third-party payment ".
660 $_->paypendingnum . ": $error\n"
663 @pending = grep { $_->status ne 'thirdparty' } @pending;
666 return "A payment is already being processed for this customer (".
667 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
668 "); $options{method} transaction aborted."
671 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
673 my $cust_pay_pending = new FS::cust_pay_pending {
674 'custnum' => $self->custnum,
675 'paid' => $options{amount},
677 'payby' => $bop_method2payby{$options{method}},
678 'payinfo' => $options{payinfo},
679 'paymask' => $options{paymask},
680 'paydate' => $paydate,
681 'recurring_billing' => $content{recurring_billing},
682 'pkgnum' => $options{'pkgnum'},
684 'gatewaynum' => $payment_gateway->gatewaynum || '',
685 'session_id' => $options{session_id} || '',
686 'jobnum' => $options{depend_jobnum} || '',
688 $cust_pay_pending->payunique( $options{payunique} )
689 if defined($options{payunique}) && length($options{payunique});
691 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
693 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
694 return $cpp_new_err if $cpp_new_err;
696 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
698 warn Dumper($cust_pay_pending) if $DEBUG > 2;
700 my( $action1, $action2 ) =
701 split( /\s*\,\s*/, $payment_gateway->gateway_action );
703 my $transaction = new $namespace( $payment_gateway->gateway_module,
704 _bop_options(\%options),
707 $transaction->content(
708 'type' => $options{method},
709 _bop_auth(\%options),
710 'action' => $action1,
711 'description' => $options{'description'},
712 'amount' => $options{amount},
713 #'invoice_number' => $options{'invnum'},
714 'customer_id' => $self->custnum,
716 'reference' => $cust_pay_pending->paypendingnum, #for now
717 'callback_url' => $payment_gateway->gateway_callback_url,
718 'cancel_url' => $payment_gateway->gateway_cancel_url,
723 $cust_pay_pending->status('pending');
724 my $cpp_pending_err = $cust_pay_pending->replace;
725 return $cpp_pending_err if $cpp_pending_err;
727 warn Dumper($transaction) if $DEBUG > 2;
729 unless ( $BOP_TESTING ) {
730 $transaction->test_transaction(1)
731 if $conf->exists('business-onlinepayment-test_transaction');
732 $transaction->submit();
734 if ( $BOP_TESTING_SUCCESS ) {
735 $transaction->is_success(1);
736 $transaction->authorization('fake auth');
738 $transaction->is_success(0);
739 $transaction->error_message('fake failure');
743 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
745 $cust_pay_pending->status('thirdparty');
746 my $cpp_err = $cust_pay_pending->replace;
747 return { error => $cpp_err } if $cpp_err;
748 return { reference => $cust_pay_pending->paypendingnum,
749 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
751 } elsif ( $transaction->is_success() && $action2 ) {
753 $cust_pay_pending->status('authorized');
754 my $cpp_authorized_err = $cust_pay_pending->replace;
755 return $cpp_authorized_err if $cpp_authorized_err;
757 my $auth = $transaction->authorization;
758 my $ordernum = $transaction->can('order_number')
759 ? $transaction->order_number
763 new Business::OnlinePayment( $payment_gateway->gateway_module,
764 _bop_options(\%options),
769 type => $options{method},
771 _bop_auth(\%options),
772 order_number => $ordernum,
773 amount => $options{amount},
774 authorization => $auth,
775 description => $options{'description'},
778 foreach my $field (qw( authorization_source_code returned_ACI
779 transaction_identifier validation_code
780 transaction_sequence_num local_transaction_date
781 local_transaction_time AVS_result_code )) {
782 $capture{$field} = $transaction->$field() if $transaction->can($field);
785 $capture->content( %capture );
787 $capture->test_transaction(1)
788 if $conf->exists('business-onlinepayment-test_transaction');
791 unless ( $capture->is_success ) {
792 my $e = "Authorization successful but capture failed, custnum #".
793 $self->custnum. ': '. $capture->result_code.
794 ": ". $capture->error_message;
802 # remove paycvv after initial transaction
805 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
806 if ( length($options{'paycvv'})
807 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
809 my $error = $self->remove_cvv_from_cust_payby($options{payinfo});
811 $log->critical('Error removing cvv for cust '.$self->custnum.': '.$error);
812 #not returning error, should at least attempt to handle results of an otherwise valid transaction
813 warn "WARNING: error removing cvv: $error\n";
821 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
822 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
823 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
824 # cpp will be replaced in _realtime_bop_result
825 $cust_pay_pending->payinfo($card_token);
826 if ($options{'cust_payby'} and my $error = $options{'cust_payby'}->replace) {
827 $log->critical('Error storing token for cust '.$self->custnum.', cust_payby '.$options{'cust_payby'}->custpaybynum.': '.$error);
828 #not returning error, should at least attempt to handle results of an otherwise valid transaction
829 #this leaves real card number in cust_payby, but can't do much else if cust_payby won't replace
837 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
849 if (ref($_[0]) eq 'HASH') {
852 my ( $method, $amount ) = ( shift, shift );
854 $options{method} = $method;
855 $options{amount} = $amount;
858 if ( $options{'fake_failure'} ) {
859 return "Error: No error; test failure requested with fake_failure";
862 my $cust_pay = new FS::cust_pay ( {
863 'custnum' => $self->custnum,
864 'invnum' => $options{'invnum'},
865 'paid' => $options{amount},
867 'payby' => $bop_method2payby{$options{method}},
868 'payinfo' => '4111111111111111',
869 'paydate' => '2012-05-01',
870 'processor' => 'FakeProcessor',
872 'order_number' => '32',
874 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
877 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
878 warn " $_ => $options{$_}\n" foreach keys %options;
881 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
884 $cust_pay->invnum(''); #try again with no specific invnum
885 my $error2 = $cust_pay->insert( $options{'manual'} ?
886 ( 'manual' => 1 ) : ()
889 # gah, even with transactions.
890 my $e = 'WARNING: Card/ACH debited but database not updated - '.
891 "error inserting (fake!) payment: $error2".
892 " (previously tried insert with invnum #$options{'invnum'}" .
899 if ( $options{'paynum_ref'} ) {
900 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
908 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
910 # Wraps up processing of a realtime credit card or ACH (electronic check)
913 sub _realtime_bop_result {
914 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
916 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
919 warn "$me _realtime_bop_result: pending transaction ".
920 $cust_pay_pending->paypendingnum. "\n";
921 warn " $_ => $options{$_}\n" foreach keys %options;
924 my $payment_gateway = $options{payment_gateway}
925 or return "no payment gateway in arguments to _realtime_bop_result";
927 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
928 my $cpp_captured_err = $cust_pay_pending->replace; #also saves post-transaction tokenization, if that happens
929 return $cpp_captured_err if $cpp_captured_err;
931 if ( $transaction->is_success() ) {
933 my $order_number = $transaction->order_number
934 if $transaction->can('order_number');
936 my $cust_pay = new FS::cust_pay ( {
937 'custnum' => $self->custnum,
938 'invnum' => $options{'invnum'},
939 'paid' => $cust_pay_pending->paid,
941 'payby' => $cust_pay_pending->payby,
942 'payinfo' => $options{'payinfo'},
943 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
944 'paydate' => $cust_pay_pending->paydate,
945 'pkgnum' => $cust_pay_pending->pkgnum,
946 'discount_term' => $options{'discount_term'},
947 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
948 'processor' => $payment_gateway->gateway_module,
949 'auth' => $transaction->authorization,
950 'order_number' => $order_number || '',
951 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
953 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
954 $cust_pay->payunique( $options{payunique} )
955 if defined($options{payunique}) && length($options{payunique});
957 my $oldAutoCommit = $FS::UID::AutoCommit;
958 local $FS::UID::AutoCommit = 0;
961 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
963 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
966 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
967 $cust_pay->invnum(''); #try again with no specific invnum
968 $cust_pay->paynum('');
969 my $error2 = $cust_pay->insert( $options{'manual'} ?
970 ( 'manual' => 1 ) : ()
973 # gah. but at least we have a record of the state we had to abort in
974 # from cust_pay_pending now.
975 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
976 my $e = "WARNING: $options{method} captured but payment not recorded -".
977 " error inserting payment (". $payment_gateway->gateway_module.
979 " (previously tried insert with invnum #$options{'invnum'}" .
980 ": $error ) - pending payment saved as paypendingnum ".
981 $cust_pay_pending->paypendingnum. "\n";
987 my $jobnum = $cust_pay_pending->jobnum;
989 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
991 unless ( $placeholder ) {
992 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
993 my $e = "WARNING: $options{method} captured but job $jobnum not ".
994 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
999 $error = $placeholder->delete;
1002 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
1003 my $e = "WARNING: $options{method} captured but could not delete ".
1004 "job $jobnum for paypendingnum ".
1005 $cust_pay_pending->paypendingnum. ": $error\n";
1010 $cust_pay_pending->set('jobnum','');
1014 if ( $options{'paynum_ref'} ) {
1015 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
1018 $cust_pay_pending->status('done');
1019 $cust_pay_pending->statustext('captured');
1020 $cust_pay_pending->paynum($cust_pay->paynum);
1021 my $cpp_done_err = $cust_pay_pending->replace;
1023 if ( $cpp_done_err ) {
1025 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
1026 my $e = "WARNING: $options{method} captured but payment not recorded - ".
1027 "error updating status for paypendingnum ".
1028 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1034 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1036 if ( $options{'apply'} ) {
1037 my $apply_error = $self->apply_payments_and_credits;
1038 if ( $apply_error ) {
1039 warn "WARNING: error applying payment: $apply_error\n";
1040 #but we still should return no error cause the payment otherwise went
1045 # have a CC surcharge portion --> one-time charge
1046 if ( $options{'cc_surcharge'} > 0 ) {
1047 # XXX: this whole block needs to be in a transaction?
1050 $invnum = $options{'invnum'} if $options{'invnum'};
1051 unless ( $invnum ) { # probably from a payment screen
1052 # do we have any open invoices? pick earliest
1053 # uses the fact that cust_main->cust_bill sorts by date ascending
1054 my @open = $self->open_cust_bill;
1055 $invnum = $open[0]->invnum if scalar(@open);
1058 unless ( $invnum ) { # still nothing? pick last closed invoice
1059 # again uses fact that cust_main->cust_bill sorts by date ascending
1060 my @closed = $self->cust_bill;
1061 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1064 unless ( $invnum ) {
1065 # XXX: unlikely case - pre-paying before any invoices generated
1066 # what it should do is create a new invoice and pick it
1067 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1072 my $charge_error = $self->charge({
1073 'amount' => $options{'cc_surcharge'},
1074 'pkg' => 'Credit Card Surcharge',
1076 'cust_pkg_ref' => \$cust_pkg,
1079 warn 'Unable to add CC surcharge cust_pkg';
1083 $cust_pkg->setup(time);
1084 my $cp_error = $cust_pkg->replace;
1086 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1090 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1091 unless ( $cust_bill ) {
1092 warn "race condition + invoice deletion just happened";
1097 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1099 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1103 return ''; #no error
1109 my $perror = $transaction->error_message;
1110 #$payment_gateway->gateway_module. " error: ".
1111 # removed for conciseness
1113 my $jobnum = $cust_pay_pending->jobnum;
1115 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1117 if ( $placeholder ) {
1118 my $error = $placeholder->depended_delete;
1119 $error ||= $placeholder->delete;
1120 $cust_pay_pending->set('jobnum','');
1121 warn "error removing provisioning jobs after declined paypendingnum ".
1122 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1124 my $e = "error finding job $jobnum for declined paypendingnum ".
1125 $cust_pay_pending->paypendingnum. "\n";
1131 unless ( $transaction->error_message ) {
1134 if ( $transaction->can('response_page') ) {
1136 'page' => ( $transaction->can('response_page')
1137 ? $transaction->response_page
1140 'code' => ( $transaction->can('response_code')
1141 ? $transaction->response_code
1144 'headers' => ( $transaction->can('response_headers')
1145 ? $transaction->response_headers
1151 "No additional debugging information available for ".
1152 $payment_gateway->gateway_module;
1155 $perror .= "No error_message returned from ".
1156 $payment_gateway->gateway_module. " -- ".
1157 ( ref($t_response) ? Dumper($t_response) : $t_response );
1161 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1162 && $conf->exists('emaildecline', $self->agentnum)
1163 && grep { $_ ne 'POST' } $self->invoicing_list
1164 && ! grep { $transaction->error_message =~ /$_/ }
1165 $conf->config('emaildecline-exclude', $self->agentnum)
1168 # Send a decline alert to the customer.
1169 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1172 # include the raw error message in the transaction state
1173 $cust_pay_pending->setfield('error', $transaction->error_message);
1174 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1175 $error = $msg_template->send( 'cust_main' => $self,
1176 'object' => $cust_pay_pending );
1180 $perror .= " (also received error sending decline notification: $error)"
1185 $cust_pay_pending->status('done');
1186 $cust_pay_pending->statustext($perror);
1187 #'declined:': no, that's failure_status
1188 if ( $transaction->can('failure_status') ) {
1189 $cust_pay_pending->failure_status( $transaction->failure_status );
1191 my $cpp_done_err = $cust_pay_pending->replace;
1192 if ( $cpp_done_err ) {
1193 my $e = "WARNING: $options{method} declined but pending payment not ".
1194 "resolved - error updating status for paypendingnum ".
1195 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1197 $perror = "$e ($perror)";
1205 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1207 Verifies successful third party processing of a realtime credit card or
1208 ACH (electronic check) transaction via a
1209 Business::OnlineThirdPartyPayment realtime gateway. See
1210 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1212 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1214 The additional options I<payname>, I<city>, I<state>,
1215 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1216 if set, will override the value from the customer record.
1218 I<description> is a free-text field passed to the gateway. It defaults to
1219 "Internet services".
1221 If an I<invnum> is specified, this payment (if successful) is applied to the
1222 specified invoice. If you don't specify an I<invnum> you might want to
1223 call the B<apply_payments> method.
1225 I<quiet> can be set true to surpress email decline notices.
1227 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1228 resulting paynum, if any.
1230 I<payunique> is a unique identifier for this payment.
1232 Returns a hashref containing elements bill_error (which will be undefined
1233 upon success) and session_id of any associated session.
1237 sub realtime_botpp_capture {
1238 my( $self, $cust_pay_pending, %options ) = @_;
1240 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1243 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1244 warn " $_ => $options{$_}\n" foreach keys %options;
1247 eval "use Business::OnlineThirdPartyPayment";
1251 # select the gateway
1254 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1256 my $payment_gateway;
1257 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1258 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1259 { gatewaynum => $gatewaynum }
1261 : $self->agent->payment_gateway( 'method' => $method,
1262 # 'invnum' => $cust_pay_pending->invnum,
1263 # 'payinfo' => $cust_pay_pending->payinfo,
1266 $options{payment_gateway} = $payment_gateway; # for the helper subs
1272 my @invoicing_list = $self->invoicing_list_emailonly;
1273 if ( $conf->exists('emailinvoiceautoalways')
1274 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1275 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1276 push @invoicing_list, $self->all_emails;
1279 my $email = ($conf->exists('business-onlinepayment-email-override'))
1280 ? $conf->config('business-onlinepayment-email-override')
1281 : $invoicing_list[0];
1285 $content{email_customer} =
1286 ( $conf->exists('business-onlinepayment-email_customer')
1287 || $conf->exists('business-onlinepayment-email-override') );
1290 # run transaction(s)
1294 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1295 _bop_options(\%options),
1298 $transaction->reference({ %options });
1300 $transaction->content(
1302 _bop_auth(\%options),
1303 'action' => 'Post Authorization',
1304 'description' => $options{'description'},
1305 'amount' => $cust_pay_pending->paid,
1306 #'invoice_number' => $options{'invnum'},
1307 'customer_id' => $self->custnum,
1308 'reference' => $cust_pay_pending->paypendingnum,
1310 'phone' => $self->daytime || $self->night,
1312 # plus whatever is required for bogus capture avoidance
1315 $transaction->submit();
1318 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1320 if ( $options{'apply'} ) {
1321 my $apply_error = $self->apply_payments_and_credits;
1322 if ( $apply_error ) {
1323 warn "WARNING: error applying payment: $apply_error\n";
1328 bill_error => $error,
1329 session_id => $cust_pay_pending->session_id,
1334 =item default_payment_gateway
1336 DEPRECATED -- use agent->payment_gateway
1340 sub default_payment_gateway {
1341 my( $self, $method ) = @_;
1343 die "Real-time processing not enabled\n"
1344 unless $conf->exists('business-onlinepayment');
1346 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1349 my $bop_config = 'business-onlinepayment';
1350 $bop_config .= '-ach'
1351 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1352 my ( $processor, $login, $password, $action, @bop_options ) =
1353 $conf->config($bop_config);
1354 $action ||= 'normal authorization';
1355 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1356 die "No real-time processor is enabled - ".
1357 "did you set the business-onlinepayment configuration value?\n"
1360 ( $processor, $login, $password, $action, @bop_options )
1363 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1365 Refunds a realtime credit card or ACH (electronic check) transaction
1366 via a Business::OnlinePayment realtime gateway. See
1367 L<http://420.am/business-onlinepayment> for supported gateways.
1369 Available methods are: I<CC> or I<ECHECK>
1371 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1373 Most gateways require a reference to an original payment transaction to refund,
1374 so you probably need to specify a I<paynum>.
1376 I<amount> defaults to the original amount of the payment if not specified.
1378 I<reasonnum> specified an existing refund reason for the refund
1380 I<paydate> specifies the expiration date for a credit card overriding the
1381 value from the customer record or the payment record. Specified as yyyy-mm-dd
1383 Implementation note: If I<amount> is unspecified or equal to the amount of the
1384 orignal payment, first an attempt is made to "void" the transaction via
1385 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1386 the normal attempt is made to "refund" ("credit") the transaction via the
1387 gateway is attempted. No attempt to "void" the transaction is made if the
1388 gateway has introspection data and doesn't support void.
1390 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1391 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1392 #if set, will override the value from the customer record.
1394 #If an I<invnum> is specified, this payment (if successful) is applied to the
1395 #specified invoice. If you don't specify an I<invnum> you might want to
1396 #call the B<apply_payments> method.
1400 #some false laziness w/realtime_bop, not enough to make it worth merging
1401 #but some useful small subs should be pulled out
1402 sub realtime_refund_bop {
1405 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1408 if (ref($_[0]) eq 'HASH') {
1409 %options = %{$_[0]};
1413 $options{method} = $method;
1417 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1418 warn " $_ => $options{$_}\n" foreach keys %options;
1421 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1426 # look up the original payment and optionally a gateway for that payment
1430 my $amount = $options{'amount'};
1432 my( $processor, $login, $password, @bop_options, $namespace ) ;
1433 my( $auth, $order_number ) = ( '', '', '' );
1434 my $gatewaynum = '';
1436 if ( $options{'paynum'} ) {
1438 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1439 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1440 or return "Unknown paynum $options{'paynum'}";
1441 $amount ||= $cust_pay->paid;
1443 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1444 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1446 if ( $cust_pay->get('processor') ) {
1447 ($gatewaynum, $processor, $auth, $order_number) =
1449 $cust_pay->gatewaynum,
1450 $cust_pay->processor,
1452 $cust_pay->order_number,
1455 # this payment wasn't upgraded, which probably means this won't work,
1457 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1458 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1459 $cust_pay->paybatch;
1460 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1463 if ( $gatewaynum ) { #gateway for the payment to be refunded
1465 my $payment_gateway =
1466 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1467 die "payment gateway $gatewaynum not found"
1468 unless $payment_gateway;
1470 $processor = $payment_gateway->gateway_module;
1471 $login = $payment_gateway->gateway_username;
1472 $password = $payment_gateway->gateway_password;
1473 $namespace = $payment_gateway->gateway_namespace;
1474 @bop_options = $payment_gateway->options;
1476 } else { #try the default gateway
1479 my $payment_gateway =
1480 $self->agent->payment_gateway('method' => $options{method});
1482 ( $conf_processor, $login, $password, $namespace ) =
1483 map { my $method = "gateway_$_"; $payment_gateway->$method }
1484 qw( module username password namespace );
1486 @bop_options = $payment_gateway->gatewaynum
1487 ? $payment_gateway->options
1488 : @{ $payment_gateway->get('options') };
1490 return "processor of payment $options{'paynum'} $processor does not".
1491 " match default processor $conf_processor"
1492 unless $processor eq $conf_processor;
1497 } else { # didn't specify a paynum, so look for agent gateway overrides
1498 # like a normal transaction
1500 my $payment_gateway =
1501 $self->agent->payment_gateway( 'method' => $options{method},
1502 #'payinfo' => $payinfo,
1504 my( $processor, $login, $password, $namespace ) =
1505 map { my $method = "gateway_$_"; $payment_gateway->$method }
1506 qw( module username password namespace );
1508 my @bop_options = $payment_gateway->gatewaynum
1509 ? $payment_gateway->options
1510 : @{ $payment_gateway->get('options') };
1513 return "neither amount nor paynum specified" unless $amount;
1515 eval "use $namespace";
1520 'type' => $options{method},
1522 'password' => $password,
1523 'order_number' => $order_number,
1524 'amount' => $amount,
1526 $content{authorization} = $auth
1527 if length($auth); #echeck/ACH transactions have an order # but no auth
1528 #(at least with authorize.net)
1530 my $currency = $conf->exists('business-onlinepayment-currency')
1531 && $conf->config('business-onlinepayment-currency');
1532 $content{currency} = $currency if $currency;
1534 my $disable_void_after;
1535 if ($conf->exists('disable_void_after')
1536 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1537 $disable_void_after = $1;
1540 #first try void if applicable
1541 my $void = new Business::OnlinePayment( $processor, @bop_options );
1544 if ($void->can('info')) {
1546 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1547 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1548 my %supported_actions = $void->info('supported_actions');
1550 if ( %supported_actions && $paytype
1551 && defined($supported_actions{$paytype})
1552 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1555 if ( $cust_pay && $cust_pay->paid == $amount
1557 ( not defined($disable_void_after) )
1558 || ( time < ($cust_pay->_date + $disable_void_after ) )
1562 warn " attempting void\n" if $DEBUG > 1;
1563 if ( $void->can('info') ) {
1564 if ( $cust_pay->payby eq 'CARD'
1565 && $void->info('CC_void_requires_card') )
1567 $content{'card_number'} = $cust_pay->payinfo;
1568 } elsif ( $cust_pay->payby eq 'CHEK'
1569 && $void->info('ECHECK_void_requires_account') )
1571 ( $content{'account_number'}, $content{'routing_code'} ) =
1572 split('@', $cust_pay->payinfo);
1573 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1576 $void->content( 'action' => 'void', %content );
1577 $void->test_transaction(1)
1578 if $conf->exists('business-onlinepayment-test_transaction');
1580 if ( $void->is_success ) {
1581 # specified as a refund reason, but now we want a payment void reason
1582 # extract just the reason text, let cust_pay::void handle new_or_existing
1583 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1585 $error = 'Reason could not be loaded' unless $reason;
1586 $error = $cust_pay->void($reason->reason) unless $error;
1588 # gah, even with transactions.
1589 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1590 "error voiding payment: $error";
1594 warn " void successful\n" if $DEBUG > 1;
1599 warn " void unsuccessful, trying refund\n"
1603 my $address = $self->address1;
1604 $address .= ", ". $self->address2 if $self->address2;
1606 my($payname, $payfirst, $paylast);
1607 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1608 $payname = $self->payname;
1609 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1610 or return "Illegal payname $payname";
1611 ($payfirst, $paylast) = ($1, $2);
1613 $payfirst = $self->getfield('first');
1614 $paylast = $self->getfield('last');
1615 $payname = "$payfirst $paylast";
1618 my @invoicing_list = $self->invoicing_list_emailonly;
1619 if ( $conf->exists('emailinvoiceautoalways')
1620 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1621 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1622 push @invoicing_list, $self->all_emails;
1625 my $email = ($conf->exists('business-onlinepayment-email-override'))
1626 ? $conf->config('business-onlinepayment-email-override')
1627 : $invoicing_list[0];
1629 my $payip = exists($options{'payip'})
1632 $content{customer_ip} = $payip
1636 if ( $options{method} eq 'CC' ) {
1639 $content{card_number} = $payinfo = $cust_pay->payinfo;
1640 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1641 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1642 ($content{expiration} = "$2/$1"); # where available
1644 $content{card_number} = $payinfo = $self->payinfo;
1645 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1646 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1647 $content{expiration} = "$2/$1";
1650 } elsif ( $options{method} eq 'ECHECK' ) {
1653 $payinfo = $cust_pay->payinfo;
1655 $payinfo = $self->payinfo;
1657 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1658 $content{bank_name} = $self->payname;
1659 $content{account_type} = 'CHECKING';
1660 $content{account_name} = $payname;
1661 $content{customer_org} = $self->company ? 'B' : 'I';
1662 $content{customer_ssn} = $self->ss;
1667 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1668 my %sub_content = $refund->content(
1669 'action' => 'credit',
1670 'customer_id' => $self->custnum,
1671 'last_name' => $paylast,
1672 'first_name' => $payfirst,
1674 'address' => $address,
1675 'city' => $self->city,
1676 'state' => $self->state,
1677 'zip' => $self->zip,
1678 'country' => $self->country,
1680 'phone' => $self->daytime || $self->night,
1683 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1685 $refund->test_transaction(1)
1686 if $conf->exists('business-onlinepayment-test_transaction');
1689 return "$processor error: ". $refund->error_message
1690 unless $refund->is_success();
1692 $order_number = $refund->order_number if $refund->can('order_number');
1694 # change this to just use $cust_pay->delete_cust_bill_pay?
1695 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1696 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1697 last unless @cust_bill_pay;
1698 my $cust_bill_pay = pop @cust_bill_pay;
1699 my $error = $cust_bill_pay->delete;
1703 my $cust_refund = new FS::cust_refund ( {
1704 'custnum' => $self->custnum,
1705 'paynum' => $options{'paynum'},
1706 'source_paynum' => $options{'paynum'},
1707 'refund' => $amount,
1709 'payby' => $bop_method2payby{$options{method}},
1710 'payinfo' => $payinfo,
1711 'reasonnum' => $options{'reasonnum'},
1712 'gatewaynum' => $gatewaynum, # may be null
1713 'processor' => $processor,
1714 'auth' => $refund->authorization,
1715 'order_number' => $order_number,
1717 my $error = $cust_refund->insert;
1719 $cust_refund->paynum(''); #try again with no specific paynum
1720 $cust_refund->source_paynum('');
1721 my $error2 = $cust_refund->insert;
1723 # gah, even with transactions.
1724 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1725 "error inserting refund ($processor): $error2".
1726 " (previously tried insert with paynum #$options{'paynum'}" .
1737 =item realtime_verify_bop [ OPTION => VALUE ... ]
1739 Runs an authorization-only transaction for $1 against this credit card (if
1740 successful, immediatly reverses the authorization).
1742 Returns the empty string if the authorization was sucessful, or an error
1745 Option I<cust_payby> should be passed, even if it's not yet been inserted.
1746 Object will be tokenized if possible, but that change will not be
1747 updated in database (must be inserted/replaced afterwards.)
1749 Currently only succeeds for Business::OnlinePayment CC transactions.
1753 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1754 #it worth merging but some useful small subs should be pulled out
1755 sub realtime_verify_bop {
1758 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1759 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1762 if (ref($_[0]) eq 'HASH') {
1763 %options = %{$_[0]};
1769 warn "$me realtime_verify_bop\n";
1770 warn " $_ => $options{$_}\n" foreach keys %options;
1773 # set fields from passed cust_payby
1774 return "No cust_payby" unless $options{'cust_payby'};
1775 _bop_cust_payby_options(\%options);
1777 # possibly run a separate transaction to tokenize card number,
1778 # so that we never store tokenized card info in cust_pay_pending
1779 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
1780 my $token_error = $self->realtime_tokenize(\%options);
1781 return $token_error if $token_error;
1782 #important that we not replace cust_payby here,
1783 #because cust_payby->replace uses realtime_verify_bop!
1784 return "Cannot tokenize card info"
1785 if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'});
1792 my $payment_gateway = $self->_payment_gateway( \%options );
1793 my $namespace = $payment_gateway->gateway_namespace;
1795 eval "use $namespace";
1799 # check for banned credit card/ACH
1802 my $ban = FS::banned_pay->ban_search(
1803 'payby' => $bop_method2payby{'CC'},
1804 'payinfo' => $options{payinfo},
1806 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1812 my $bop_content = $self->_bop_content(\%options);
1813 return $bop_content unless ref($bop_content);
1815 my @invoicing_list = $self->invoicing_list_emailonly;
1816 if ( $conf->exists('emailinvoiceautoalways')
1817 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1818 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1819 push @invoicing_list, $self->all_emails;
1822 my $email = ($conf->exists('business-onlinepayment-email-override'))
1823 ? $conf->config('business-onlinepayment-email-override')
1824 : $invoicing_list[0];
1829 if ( $namespace eq 'Business::OnlinePayment' ) {
1831 if ( $options{method} eq 'CC' ) {
1833 $content{card_number} = $options{payinfo};
1834 $paydate = $options{'paydate'};
1835 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1836 $content{expiration} = "$2/$1";
1838 $content{cvv2} = $options{'paycvv'}
1839 if length($options{'paycvv'});
1841 my $paystart_month = $options{'paystart_month'};
1842 my $paystart_year = $options{'paystart_year'};
1844 $content{card_start} = "$paystart_month/$paystart_year"
1845 if $paystart_month && $paystart_year;
1847 my $payissue = $options{'payissue'};
1848 $content{issue_number} = $payissue if $payissue;
1850 } elsif ( $options{method} eq 'ECHECK' ){
1851 #cannot verify, move along (though it shouldn't be called...)
1854 return "unknown method ". $options{method};
1856 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1857 #cannot verify, move along
1860 return "unknown namespace $namespace";
1864 # run transaction(s)
1868 my $transaction; #need this back so we can do _tokenize_card
1870 # don't mutex the customer here, because they might be uncommitted. and
1871 # this is only verification. it doesn't matter if they have other
1872 # unfinished verifications.
1874 my $cust_pay_pending = new FS::cust_pay_pending {
1875 'custnum_pending' => 1,
1878 'payby' => $bop_method2payby{'CC'},
1879 'payinfo' => $options{payinfo},
1880 'paymask' => $options{paymask},
1881 'paydate' => $paydate,
1882 'pkgnum' => $options{'pkgnum'},
1884 'gatewaynum' => $payment_gateway->gatewaynum || '',
1885 'session_id' => $options{session_id} || '',
1887 $cust_pay_pending->payunique( $options{payunique} )
1888 if defined($options{payunique}) && length($options{payunique});
1891 # open a separate handle for creating/updating the cust_pay_pending
1893 local $FS::UID::dbh = myconnect();
1894 local $FS::UID::AutoCommit = 1;
1896 # if this is an existing customer (and we can tell now because
1897 # this is a fresh transaction), it's safe to assign their custnum
1898 # to the cust_pay_pending record, and then the verification attempt
1899 # will remain linked to them even if it fails.
1900 if ( FS::cust_main->by_key($self->custnum) ) {
1901 $cust_pay_pending->set('custnum', $self->custnum);
1904 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1907 # if this fails, just return; everything else will still allow the
1908 # cust_pay_pending to have its custnum set later
1909 my $cpp_new_err = $cust_pay_pending->insert;
1910 return $cpp_new_err if $cpp_new_err;
1912 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1914 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1916 $transaction = new $namespace( $payment_gateway->gateway_module,
1917 _bop_options(\%options),
1920 $transaction->content(
1922 _bop_auth(\%options),
1923 'action' => 'Authorization Only',
1924 'description' => $options{'description'},
1926 'customer_id' => $self->custnum,
1928 'reference' => $cust_pay_pending->paypendingnum, #for now
1933 $cust_pay_pending->status('pending');
1934 my $cpp_pending_err = $cust_pay_pending->replace;
1935 return $cpp_pending_err if $cpp_pending_err;
1937 warn Dumper($transaction) if $DEBUG > 2;
1939 unless ( $BOP_TESTING ) {
1940 $transaction->test_transaction(1)
1941 if $conf->exists('business-onlinepayment-test_transaction');
1942 $transaction->submit();
1944 if ( $BOP_TESTING_SUCCESS ) {
1945 $transaction->is_success(1);
1946 $transaction->authorization('fake auth');
1948 $transaction->is_success(0);
1949 $transaction->error_message('fake failure');
1953 if ( $transaction->is_success() ) {
1955 $cust_pay_pending->status('authorized');
1956 my $cpp_authorized_err = $cust_pay_pending->replace;
1957 return $cpp_authorized_err if $cpp_authorized_err;
1959 my $auth = $transaction->authorization;
1960 my $ordernum = $transaction->can('order_number')
1961 ? $transaction->order_number
1964 my $reverse = new $namespace( $payment_gateway->gateway_module,
1965 _bop_options(\%options),
1968 $reverse->content( 'action' => 'Reverse Authorization',
1969 _bop_auth(\%options),
1973 'authorization' => $transaction->authorization,
1974 'order_number' => $ordernum,
1977 'result_code' => $transaction->result_code,
1978 'txn_date' => $transaction->txn_date,
1982 $reverse->test_transaction(1)
1983 if $conf->exists('business-onlinepayment-test_transaction');
1986 if ( $reverse->is_success ) {
1988 $cust_pay_pending->status('done');
1989 $cust_pay_pending->statustext('reversed');
1990 my $cpp_reversed_err = $cust_pay_pending->replace;
1991 return $cpp_reversed_err if $cpp_reversed_err;
1995 my $e = "Authorization successful but reversal failed, custnum #".
1996 $self->custnum. ': '. $reverse->result_code.
1997 ": ". $reverse->error_message;
2004 ### Address Verification ###
2006 # Single-letter codes vary by cardtype.
2008 # Erring on the side of accepting cards if avs is not available,
2009 # only rejecting if avs occurred and there's been an explicit mismatch
2011 # Charts below taken from vSecure documentation,
2012 # shows codes for Amex/Dscv/MC/Visa
2014 # ACCEPTABLE AVS RESPONSES:
2015 # Both Address and 5-digit postal code match Y A Y Y
2016 # Both address and 9-digit postal code match Y A X Y
2017 # United Kingdom – Address and postal code match _ _ _ F
2018 # International transaction – Address and postal code match _ _ _ D/M
2020 # ACCEPTABLE, BUT ISSUE A WARNING:
2021 # Ineligible transaction; or message contains a content error _ _ _ E
2022 # System unavailable; retry R U R R
2023 # Information unavailable U W U U
2024 # Issuer does not support AVS S U S S
2025 # AVS is not applicable _ _ _ S
2026 # Incompatible formats – Not verified _ _ _ C
2027 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2028 # International transaction – address not verified _ G _ G/I
2030 # UNACCEPTABLE AVS RESPONSES:
2031 # Only Address matches A Y A A
2032 # Only 5-digit postal code matches Z Z Z Z
2033 # Only 9-digit postal code matches Z Z W W
2034 # Neither address nor postal code matches N N N N
2036 if (my $avscode = uc($transaction->avs_code)) {
2038 # map codes to accept/warn/reject
2040 'American Express card' => {
2049 'Discover card' => {
2088 my $cardtype = cardtype($content{card_number});
2089 if ($avs->{$cardtype}) {
2090 my $avsact = $avs->{$cardtype}->{$avscode};
2092 if ($avsact eq 'r') {
2093 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2094 } elsif ($avsact eq 'w') {
2095 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2096 } elsif (!$avsact) {
2097 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2098 } # else $avsact eq 'a'
2100 $log->warning($warning);
2103 } # else $cardtype avs handling not implemented
2104 } # else !$transaction->avs_code
2106 } else { # is not success
2108 # status is 'done' not 'declined', as in _realtime_bop_result
2109 $cust_pay_pending->status('done');
2110 $error = $transaction->error_message || 'Unknown error';
2111 $cust_pay_pending->statustext($error);
2112 # could also record failure_status here,
2113 # but it's not supported by B::OP::vSecureProcessing...
2114 # need a B::OP module with (reverse) auth only to test it with
2115 my $cpp_declined_err = $cust_pay_pending->replace;
2116 return $cpp_declined_err if $cpp_declined_err;
2120 } # end of IMMEDIATE; we now have our $error and $transaction
2123 # Save the custnum (as part of the main transaction, so it can reference
2127 if (!$cust_pay_pending->custnum) {
2128 $cust_pay_pending->set('custnum', $self->custnum);
2129 my $set_custnum_err = $cust_pay_pending->replace;
2130 if ($set_custnum_err) {
2131 $log->error($set_custnum_err);
2132 $error ||= $set_custnum_err;
2133 # but if there was a real verification error also, return that one
2138 # remove paycvv here? need to find out if a reversed auth
2139 # counts as an initial transaction for paycvv retention requirements
2146 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
2147 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
2148 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
2149 $cust_pay_pending->payinfo($card_token);
2150 my $cpp_token_err = $cust_pay_pending->replace;
2151 #this leaves real card number in cust_pay_pending, but can't do much else if cpp won't replace
2152 return $cpp_token_err if $cpp_token_err;
2153 #important that we not replace cust_payby here,
2154 #because cust_payby->replace uses realtime_verify_bop!
2161 # $error contains the transaction error_message, if is_success was false.
2167 =item realtime_tokenize [ OPTION => VALUE ... ]
2169 If possible and necessary, runs a tokenize transaction.
2170 In order to be possible, a credit card cust_payby record
2171 must be passed and a Business::OnlinePayment gateway capable
2172 of Tokenize transactions must be configured for this user.
2173 Is only necessary if payinfo is not yet tokenized.
2175 Returns the empty string if the authorization was sucessful
2176 or was not possible/necessary (thus allowing this to be safely called with
2177 non-tokenizable records/gateways, without having to perform separate tests),
2178 or an error message otherwise.
2180 Option I<cust_payby> may be passed, even if it's not yet been inserted.
2181 Object will be tokenized if possible, but that change will not be
2182 updated in database (must be inserted/replaced afterwards.)
2184 Otherwise, options I<method>, I<payinfo> and other cust_payby fields
2185 may be passed. If options are passed as a hashref, I<payinfo>
2186 will be updated as appropriate in the passed hashref.
2188 Can be run as a class method if option I<payment_gateway> is passed,
2189 but default customer id/name/phone can't be set in that case. This
2190 is really only intended for tokenizing old records on upgrade.
2194 # careful--might be run as a class method
2195 sub realtime_tokenize {
2198 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2199 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_tokenize');
2202 my $outoptions; #for returning cust_payby/payinfo
2203 if (ref($_[0]) eq 'HASH') {
2204 %options = %{$_[0]};
2205 $outoptions = $_[0];
2208 $outoptions = \%options;
2211 # set fields from passed cust_payby
2212 _bop_cust_payby_options(\%options);
2213 return '' unless $options{method} eq 'CC';
2214 return '' if $self->tokenized($options{payinfo}); #already tokenized
2220 $options{'nofatal'} = 1;
2221 my $payment_gateway = $self->_payment_gateway( \%options );
2222 return '' unless $payment_gateway;
2223 my $namespace = $payment_gateway->gateway_namespace;
2224 return '' unless $namespace eq 'Business::OnlinePayment';
2226 eval "use $namespace";
2230 # check for tokenize ability
2233 my $transaction = new $namespace( $payment_gateway->gateway_module,
2234 _bop_options(\%options),
2237 return '' unless $transaction->can('info');
2239 my %supported_actions = $transaction->info('supported_actions');
2240 return '' unless $supported_actions{'CC'}
2241 && grep /^Tokenize$/, @{$supported_actions{'CC'}};
2244 # check for banned credit card/ACH
2247 my $ban = FS::banned_pay->ban_search(
2248 'payby' => $bop_method2payby{'CC'},
2249 'payinfo' => $options{payinfo},
2251 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
2257 ### Currently, cardfortress only keys in on card number and exp date.
2258 ### We pass everything we'd pass to a normal transaction,
2259 ### for ease of current and future development,
2260 ### but note, when tokenizing old records, we may only have access to payinfo/paydate
2262 my $bop_content = $self->_bop_content(\%options);
2263 return $bop_content unless ref($bop_content);
2268 $content{card_number} = $options{payinfo};
2269 $paydate = $options{'paydate'};
2270 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2271 $content{expiration} = "$2/$1";
2273 $content{cvv2} = $options{'paycvv'}
2274 if length($options{'paycvv'});
2276 my $paystart_month = $options{'paystart_month'};
2277 my $paystart_year = $options{'paystart_year'};
2279 $content{card_start} = "$paystart_month/$paystart_year"
2280 if $paystart_month && $paystart_year;
2282 my $payissue = $options{'payissue'};
2283 $content{issue_number} = $payissue if $payissue;
2285 $content{customer_id} = $self->custnum
2294 # no cust_pay_pending---this is not a financial transaction
2296 $transaction->content(
2298 _bop_auth(\%options),
2299 'action' => 'Tokenize',
2300 'description' => $options{'description'}
2305 # no $BOP_TESTING handling for this
2306 $transaction->test_transaction(1)
2307 if $conf->exists('business-onlinepayment-test_transaction');
2308 $transaction->submit();
2310 if ( $transaction->card_token() ) { # no is_success flag
2312 # realtime_tokenize should not clear paycvv at this time. it might be
2313 # needed for the first transaction, and a tokenize isn't actually a
2314 # transaction that hits the gateway. at some point in the future, card
2315 # fortress should take on the "store paycvv until first transaction"
2316 # functionality and we should fix this in freeside, but i that's a bigger
2317 # project for another time.
2319 #important that we not replace cust_payby here,
2320 #because cust_payby->replace uses realtime_tokenize!
2321 $self->_tokenize_card($transaction,$outoptions);
2325 $error = $transaction->error_message || 'Unknown error when tokenizing card';
2334 =item tokenized PAYINFO
2336 Convenience wrapper for L<FS::payinfo_Mixin/tokenized>
2338 PAYINFO is required.
2340 Can be run as class or object method, never loads from object.
2346 my $payinfo = shift;
2347 FS::cust_pay->tokenized($payinfo);
2352 NOT A METHOD. Acts on all customers. Placed here because it makes
2353 use of module-internal methods, and to keep everything that uses
2354 Billing::OnlinePayment all in one place.
2356 Tokenizes all tokenizable card numbers from payinfo in cust_payby and
2357 CARD transactions in cust_pay_pending, cust_pay, cust_pay_void and cust_refund.
2359 If all configured gateways have the ability to tokenize, then detection of
2360 an untokenizable record will cause a fatal error.
2365 # no input, acts on all customers
2367 eval "use FS::Cursor";
2368 return "Error initializing FS::Cursor: ".$@ if $@;
2372 # get list of all gateways in table (not counting default gateway)
2373 my $cache = {}; #cache for module info
2374 my $sth = $dbh->prepare('SELECT DISTINCT gatewaynum FROM payment_gateway')
2375 or die $dbh->errstr;
2376 $sth->execute or die $sth->errstr;
2378 while (my $row = $sth->fetchrow_hashref) {
2379 push(@gatewaynums,$row->{'gatewaynum'});
2383 # look for a gateway that can't tokenize
2384 my $disallow_untokenized = 1;
2385 foreach my $gatewaynum ('',@gatewaynums) {
2386 my $gateway = FS::agent->payment_gateway( load_gatewaynum => $gatewaynum, nofatal => 1 );
2387 if (!$gateway) { # already died if $gatewaynum
2388 # no default gateway, no promise to tokenize
2389 # can just load other gateways as-needeed below
2390 $disallow_untokenized = 0;
2393 my $info = _token_check_gateway_info($cache,$gateway);
2394 return $info unless ref($info); # means it's an error message
2395 unless ($info->{'can_tokenize'}) {
2396 # a configured gateway can't tokenize, that's all we need to know right now
2397 # can just load other gateways as-needeed below
2398 $disallow_untokenized = 0;
2403 my $oldAutoCommit = $FS::UID::AutoCommit;
2404 local $FS::UID::AutoCommit = 0;
2406 ### Tokenize cust_payby
2408 my $cust_search = FS::Cursor->new({ table => 'cust_main' },$dbh);
2409 while (my $cust_main = $cust_search->fetch) {
2410 foreach my $cust_payby ($cust_main->cust_payby('CARD','DCRD')) {
2411 next if $cust_payby->tokenized;
2412 # load gateway first, just so we can cache it
2413 my $payment_gateway = $cust_main->_payment_gateway({
2414 'nofatal' => 1, # handle error smoothly below
2416 unless ($payment_gateway) {
2417 # no reason to have untokenized card numbers saved if no gateway,
2418 # but only fatal if we expected everyone to tokenize card numbers
2419 next unless $disallow_untokenized;
2420 $cust_search->DESTROY;
2421 $dbh->rollback if $oldAutoCommit;
2422 return "No gateway found for custnum ".$cust_main->custnum;
2424 my $info = _token_check_gateway_info($cache,$payment_gateway);
2425 # no fail here--a configured gateway can't tokenize, so be it
2426 next unless ref($info) && $info->{'can_tokenize'};
2428 'payment_gateway' => $payment_gateway,
2429 'cust_payby' => $cust_payby,
2431 my $error = $cust_main->realtime_tokenize(\%tokenopts);
2432 if ($cust_payby->tokenized) { # implies no error
2433 $error = $cust_payby->replace;
2435 $error ||= 'Unknown error';
2438 $cust_search->DESTROY;
2439 $dbh->rollback if $oldAutoCommit;
2440 return "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error;
2445 ### Tokenize/mask transaction tables
2447 # allow tokenization of closed cust_pay/cust_refund records
2448 local $FS::payinfo_Mixin::allow_closed_replace = 1;
2451 # $cust_pay_pending->replace, $cust_pay->replace, $cust_pay_void->replace, $cust_refund->replace all run here
2452 foreach my $table ( qw(cust_pay_pending cust_pay cust_pay_void cust_refund) ) {
2453 my $search = FS::Cursor->new({
2455 hashref => { 'payby' => 'CARD' },
2457 while (my $record = $search->fetch) {
2458 next if $record->tokenized;
2459 next if !$record->payinfo; #shouldn't happen, but at least it's not a card number
2460 next if $record->payinfo =~ /N\/A/; # ??? Not sure why we do this, but it's not a card number
2462 # don't use customer agent gateway here, use the gatewaynum specified by the record
2463 my $gatewaynum = $record->gatewaynum || '';
2464 my $gateway = FS::agent->payment_gateway( load_gatewaynum => $gatewaynum );
2465 unless ($gateway) { # already died if $gatewaynum
2466 # only fatal if we expected everyone to tokenize
2467 next unless $disallow_untokenized;
2469 $dbh->rollback if $oldAutoCommit;
2470 return "No gateway found for $table ".$record->get($record->primary_key);
2472 my $info = _token_check_gateway_info($cache,$gateway);
2473 unless (ref($info)) {
2474 # only throws error if Business::OnlinePayment won't load,
2475 # which is just cause to abort this whole process
2477 $dbh->rollback if $oldAutoCommit;
2478 return $info; # error message
2481 # a configured gateway can't tokenize, move along
2482 next unless $info->{'can_tokenize'};
2484 my $cust_main = $record->cust_main;
2485 unless ($cust_main || (
2486 # might happen for cust_pay_pending from failed verify records,
2487 # in which case we attempt tokenization without cust_main
2488 # everything else should absolutely have a cust_main
2489 $table eq 'cust_pay_pending'
2490 && $record->{'custnum_pending'}
2491 && !$disallow_untokenized
2494 $dbh->rollback if $oldAutoCommit;
2495 return "Could not load cust_main for $table ".$record->get($record->primary_key);
2497 # no clear record of name/address/etc used for transaction,
2498 # but will load name/phone/id from customer if run as an object method,
2499 # so we try that if we can
2501 'payment_gateway' => $gateway,
2503 'payinfo' => $record->payinfo,
2504 'paydate' => $record->paydate,
2506 my $error = $cust_main
2507 ? $cust_main->realtime_tokenize(\%tokenopts)
2508 : FS::cust_main::Billing_Realtime->realtime_tokenize(\%tokenopts);
2509 if (FS::cust_main::Billing_Realtime->tokenized($tokenopts{'payinfo'})) { # implies no error
2510 $record->payinfo($tokenopts{'payinfo'});
2511 $error = $record->replace;
2513 $error ||= 'Unknown error';
2517 $dbh->rollback if $oldAutoCommit;
2518 return "Error tokenizing $table ".$record->get($record->primary_key).": ".$error;
2523 $dbh->commit if $oldAutoCommit;
2529 sub _token_check_gateway_info {
2530 my ($cache,$payment_gateway) = @_;
2532 return $cache->{$payment_gateway->gateway_module}
2533 if $cache->{$payment_gateway->gateway_module};
2536 $cache->{$payment_gateway->gateway_module} = $info;
2538 my $namespace = $payment_gateway->gateway_namespace;
2539 return $info unless $namespace eq 'Business::OnlinePayment';
2540 $info->{'is_bop'} = 1;
2542 # only need to load this once,
2543 # don't want to load if nothing is_bop
2544 unless ($cache->{'Business::OnlinePayment'}) {
2545 eval "use $namespace";
2546 return "Error initializing Business:OnlinePayment: ".$@ if $@;
2547 $cache->{'Business::OnlinePayment'} = 1;
2550 my $transaction = new $namespace( $payment_gateway->gateway_module,
2551 _bop_options({ 'payment_gateway' => $payment_gateway }),
2554 return $info unless $transaction->can('info');
2555 $info->{'can_info'} = 1;
2557 my %supported_actions = $transaction->info('supported_actions');
2558 $info->{'can_tokenize'} = 1
2559 if $supported_actions{'CC'}
2560 && grep /^Tokenize$/, @{$supported_actions{'CC'}};
2562 # not using this any more, but for future reference...
2563 $info->{'void_requires_card'} = 1
2564 if $transaction->info('CC_void_requires_card');
2566 $cache->{$payment_gateway->gateway_module} = $info;
2577 L<FS::cust_main>, L<FS::cust_main::Billing>