From: Jonathan Prykop Date: Sat, 5 Nov 2016 06:20:35 +0000 (-0500) Subject: 71513: Card tokenization [bug fixes to previous checkpoint] X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=bde747e981abe6c517ce25118a53b13f53d63da7 71513: Card tokenization [bug fixes to previous checkpoint] --- diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 7718f7aab..e7a8030ae 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -376,44 +376,16 @@ sub _bop_content { } sub _tokenize_card { - my ($self,$transaction,$options,$log,%opt) = @_; - # options is for entire process, so we can update payinfo - # opt is just for this call, only key is replace - - my $cust_payby = $options->{'cust_payby'}; - if ( $cust_payby - and $transaction->can('card_token') + my ($self,$transaction,$options) = @_; + if ( $transaction->can('card_token') and $transaction->card_token - and !$cust_payby->tokenized #not already tokenized + and !$self->tokenized($options->{'payinfo'}) ) { - - $options->{'payinfo'} = $transaction->card_token; - $cust_payby->payinfo($transaction->card_token); - - my $error; - $error = $cust_payby->replace if $opt{'replace'}; - if ( $error ) { - $log->error('Error storing token for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum.': '.$error); - return $error; - } else { - $log->debug('Tokenized card for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum); - return ''; - } - + $options->{'payinfo'} = $transaction->card_token; #for creating cust_pay + $options->{'cust_payby'}->payinfo($transaction->card_token) if $options->{'cust_payby'}; + return $transaction->card_token; } - -} - -# only store payinfo in cust_pay/cust_pay_pending -# if it's a tokenized card or if processor requires card for void -sub _cust_pay_opts { - my ($self,$payby,$payinfo,$transaction) = @_; - ( (($payby eq 'CARD') && $self->tokenized($payinfo)) - || (($payby eq 'CARD') && $transaction->info('CC_void_requires_card')) - || (($payby eq 'CHEK') && $transaction->info('ECHECK_void_requires_account')) - ) - ? ('payinfo' => $payinfo) - : (); + return ''; } my %bop_method2payby = ( @@ -681,15 +653,12 @@ sub realtime_bop { #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out - my $transaction = new $namespace( $payment_gateway->gateway_module, - $self->_bop_options(\%options), - ); - my $cust_pay_pending = new FS::cust_pay_pending { 'custnum' => $self->custnum, 'paid' => $options{amount}, '_date' => '', 'payby' => $bop_method2payby{$options{method}}, + 'payinfo' => $options{payinfo}, 'paymask' => $options{paymask}, 'paydate' => $paydate, 'recurring_billing' => $content{recurring_billing}, @@ -698,7 +667,6 @@ sub realtime_bop { 'gatewaynum' => $payment_gateway->gatewaynum || '', 'session_id' => $options{session_id} || '', 'jobnum' => $options{depend_jobnum} || '', - $self->_cust_pay_opts($options{payinfo},$transaction), }; $cust_pay_pending->payunique( $options{payunique} ) if defined($options{payunique}) && length($options{payunique}); @@ -715,6 +683,10 @@ sub realtime_bop { my( $action1, $action2 ) = split( /\s*\,\s*/, $payment_gateway->gateway_action ); + my $transaction = new $namespace( $payment_gateway->gateway_module, + $self->_bop_options(\%options), + ); + $transaction->content( 'type' => $options{method}, $self->_bop_auth(\%options), @@ -819,6 +791,8 @@ sub realtime_bop { ) { my $error = $self->remove_cvv_from_cust_payby($options{payinfo}); if ( $error ) { + $log->critical('Error removing cvv for cust '.$self->custnum.': '.$error); + #not returning error, should at least attempt to handle results of an otherwise valid transaction warn "WARNING: error removing cvv: $error\n"; } } @@ -827,8 +801,15 @@ sub realtime_bop { # Tokenize ### - my $error = $self->_tokenize_card($transaction,\%options,$log,'replace' => 1); - return $error if $error; + if (my $card_token = $self->_tokenize_card($transaction,\%options)) { + # cpp will be replaced in _realtime_bop_result + $cust_pay_pending->payinfo($card_token); + if ($options{'cust_payby'} and my $error = $options{'cust_payby'}->replace) { + $log->critical('Error storing token for cust '.$self->custnum.', cust_payby '.$options{'cust_payby'}->custpaybynum.': '.$error); + #not returning error, should at least attempt to handle results of an otherwise valid transaction + #this leaves real card number in cust_payby, but can't do much else if cust_payby won't replace + } + } ### # result handling @@ -925,7 +906,7 @@ sub _realtime_bop_result { or return "no payment gateway in arguments to _realtime_bop_result"; $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined'); - my $cpp_captured_err = $cust_pay_pending->replace; + my $cpp_captured_err = $cust_pay_pending->replace; #also saves tokenization return $cpp_captured_err if $cpp_captured_err; if ( $transaction->is_success() ) { @@ -939,6 +920,7 @@ sub _realtime_bop_result { 'paid' => $cust_pay_pending->paid, '_date' => '', 'payby' => $cust_pay_pending->payby, + 'payinfo' => $options{'payinfo'}, 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask, 'paydate' => $cust_pay_pending->paydate, 'pkgnum' => $cust_pay_pending->pkgnum, @@ -948,7 +930,6 @@ sub _realtime_bop_result { 'auth' => $transaction->authorization, 'order_number' => $order_number || '', 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '', - $self->_cust_pay_opts($options{payinfo},$transaction), } ); #doesn't hurt to know, even though the dup check is in cust_pay_pending now $cust_pay->payunique( $options{payunique} ) @@ -1854,9 +1835,7 @@ sub realtime_verify_bop { ### my $error; - my $transaction = new $namespace( $payment_gateway->gateway_module, - $self->_bop_options(\%options), - ); #need this back so we can do _tokenize_card + my $transaction; #need this back so we can do _tokenize_card # don't mutex the customer here, because they might be uncommitted. and # this is only verification. it doesn't matter if they have other @@ -1867,13 +1846,13 @@ sub realtime_verify_bop { 'paid' => '1.00', '_date' => '', 'payby' => $bop_method2payby{'CC'}, + 'payinfo' => $options{payinfo}, 'paymask' => $options{paymask}, 'paydate' => $paydate, 'pkgnum' => $options{'pkgnum'}, 'status' => 'new', 'gatewaynum' => $payment_gateway->gatewaynum || '', 'session_id' => $options{session_id} || '', - $self->_cust_pay_opts($options{payinfo},$transaction), }; $cust_pay_pending->payunique( $options{payunique} ) if defined($options{payunique}) && length($options{payunique}); @@ -1904,6 +1883,10 @@ sub realtime_verify_bop { if $DEBUG > 1; warn Dumper($cust_pay_pending) if $DEBUG > 2; + $transaction = new $namespace( $payment_gateway->gateway_module, + $self->_bop_options(\%options), + ); + $transaction->content( 'type' => 'CC', $self->_bop_auth(\%options), @@ -2122,12 +2105,22 @@ sub realtime_verify_bop { } ### + # remove paycvv here? need to find out if a reversed auth + # counts as an initial transaction for paycvv retention requirements + ### + + ### # Tokenize ### - #important that we not pass replace option here, + #important that we not replace cust_payby here, #because cust_payby->replace uses realtime_verify_bop! - $self->_tokenize_card($transaction,\%options,$log); + if (my $card_token = $self->_tokenize_card($transaction,\%options)) { + $cust_pay_pending->payinfo($card_token); + my $cpp_token_err = $cust_pay_pending->replace; + #this leaves real card number in cust_payby, but can't do much else if cust_payby won't replace + return $cpp_token_err if $cpp_token_err; + } ### # result handling @@ -2264,9 +2257,16 @@ sub realtime_tokenize { if ( $transaction->card_token() ) { # no is_success flag - #important that we not pass replace option here, + # realtime_tokenize should not clear paycvv at this time. it might be + # needed for the first transaction, and a tokenize isn't actually a + # transaction that hits the gateway. at some point in the future, card + # fortress should take on the "store paycvv until first transaction" + # functionality and we should fix this in freeside, but i that's a bigger + # project for another time. + + #important that we not replace cust_payby here, #because cust_payby->replace uses realtime_tokenize! - $self->_tokenize_card($transaction,\%options,$log); + $self->_tokenize_card($transaction,\%options); } else { @@ -2278,10 +2278,19 @@ sub realtime_tokenize { } + +=item tokenized PAYINFO + +Convenience wrapper for L + +PAYINFO is required + +=cut + sub tokenized { my $this = shift; my $payinfo = shift; - $payinfo =~ /^99\d{14}$/; + FS::cust_pay->tokenized($payinfo); } =back diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index a0a2cbcc9..69828349e 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -67,7 +67,7 @@ sub payinfo { my($self,$payinfo) = @_; if ( defined($payinfo) ) { - $self->paymask($self->mask_payinfo) unless $self->tokenized; #make sure old mask is set + $self->paymask($self->mask_payinfo) unless $self->paymask || $self->tokenized; #make sure old mask is set $self->setfield('payinfo', $payinfo); $self->paymask($self->mask_payinfo) unless $self->tokenized($payinfo); #remask unless tokenizing } else { @@ -454,12 +454,17 @@ sub process_set_cardtype { } } +=item tokenized [ PAYINFO ] + +Returns true if object payinfo is tokenized + +Optionally, an arbitrary payby and payinfo can be passed. + +=cut + sub tokenized { my $self = shift; my $payinfo = scalar(@_) ? shift : $self->payinfo; - ## or just $self->cust_main->tokenized($payinfo) ?? - ## everything that currently uses this mixin is linked to cust_main, - ## but just in case, false laziness w/ FS::cust_main::Billing_Realtime $payinfo =~ /^99\d{14}$/; } diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi index 84687f019..1532605d4 100644 --- a/httemplate/misc/process/payment.cgi +++ b/httemplate/misc/process/payment.cgi @@ -193,6 +193,11 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) { errorpage("error saving info, payment not processed: $error") if $error; + + } elsif ( $payby eq 'CARD' ) { # not saving + + $paymask = FS::payinfo_Mixin->mask_payinfo('CARD',$payinfo); # for untokenized but tokenizable payinfo + } }