summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Prykop <jonathan@freeside.biz>2016-11-08 02:48:09 -0600
committerJonathan Prykop <jonathan@freeside.biz>2016-11-29 05:04:39 -0600
commit9026d337602fd1f9c463de2a46db4f27c0d429f9 (patch)
tree63c05443300bd06a1d9dbb78f6eecd589c3439b3
parent1185daff43389fe53ad43e84962329a63d31523e (diff)
71513: Card tokenization [tokenize for unsaved cards, bug fix to saving from cust_main]
-rw-r--r--FS/FS/cust_main/Billing_Realtime.pm57
-rw-r--r--FS/FS/payinfo_Mixin.pm2
2 files changed, 46 insertions, 13 deletions
diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm
index 407b9ca..e7226fe 100644
--- a/FS/FS/cust_main/Billing_Realtime.pm
+++ b/FS/FS/cust_main/Billing_Realtime.pm
@@ -375,13 +375,14 @@ sub _bop_content {
\%content;
}
+# updates payinfo and cust_payby options with token from transaction
sub _tokenize_card {
my ($self,$transaction,$options) = @_;
if ( $transaction->can('card_token')
and $transaction->card_token
and !$self->tokenized($options->{'payinfo'})
) {
- $options->{'payinfo'} = $transaction->card_token; #for creating cust_pay
+ $options->{'payinfo'} = $transaction->card_token;
$options->{'cust_payby'}->payinfo($transaction->card_token) if $options->{'cust_payby'};
return $transaction->card_token;
}
@@ -418,6 +419,19 @@ sub realtime_bop {
# set fields from passed cust_payby
$self->_bop_cust_payby_options(\%options);
+ # possibly run a separate transaction to tokenize card number,
+ # so that we never store tokenized card info in cust_pay_pending
+ if (!$self->tokenized($options{'payinfo'})) {
+ my $token_error = $self->realtime_tokenize(\%options);
+ return $token_error if $token_error;
+ # in theory, all cust_payby will be tokenized during original save,
+ # so we shouldn't get here with opt cust_payby...but just in case...
+ if ($options{'cust_payby'}) {
+ $token_error = $options{'cust_payby'}->replace;
+ return $token_error if $token_error;
+ }
+ }
+
###
# optional credit card surcharge
###
@@ -801,6 +815,8 @@ sub realtime_bop {
# Tokenize
###
+ # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
+ # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
# cpp will be replaced in _realtime_bop_result
$cust_pay_pending->payinfo($card_token);
@@ -906,7 +922,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; #also saves tokenization
+ my $cpp_captured_err = $cust_pay_pending->replace; #also saves post-transaction tokenization, if that happens
return $cpp_captured_err if $cpp_captured_err;
if ( $transaction->is_success() ) {
@@ -1755,6 +1771,15 @@ sub realtime_verify_bop {
return "No cust_payby" unless $options{'cust_payby'};
$self->_bop_cust_payby_options(\%options);
+ # possibly run a separate transaction to tokenize card number,
+ # so that we never store tokenized card info in cust_pay_pending
+ if (!$self->tokenized($options{'payinfo'})) {
+ my $token_error = $self->realtime_tokenize(\%options);
+ return $token_error if $token_error;
+ #important that we not replace cust_payby here,
+ #because cust_payby->replace uses realtime_verify_bop!
+ }
+
###
# select a gateway
###
@@ -2113,13 +2138,15 @@ sub realtime_verify_bop {
# Tokenize
###
- #important that we not replace cust_payby here,
- #because cust_payby->replace uses realtime_verify_bop!
+ # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
+ # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
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
+ #this leaves real card number in cust_pay_pending, but can't do much else if cpp won't replace
return $cpp_token_err if $cpp_token_err;
+ #important that we not replace cust_payby here,
+ #because cust_payby->replace uses realtime_verify_bop!
}
###
@@ -2134,20 +2161,25 @@ sub realtime_verify_bop {
=item realtime_tokenize [ OPTION => VALUE ... ]
-If possible, runs a tokenize transaction.
+If possible and necessary, runs a tokenize transaction.
In order to be possible, a credit card cust_payby record
must be passed and a Business::OnlinePayment gateway capable
of Tokenize transactions must be configured for this user.
+Is only necessary if payinfo is not yet tokenized.
Returns the empty string if the authorization was sucessful
-or was not possible (thus allowing this to be safely called with
+or was not possible/necessary (thus allowing this to be safely called with
non-tokenizable records/gateways, without having to perform separate tests),
or an error message otherwise.
-Option I<cust_payby> should be passed, even if it's not yet been inserted.
+Option I<cust_payby> may be passed, even if it's not yet been inserted.
Object will be tokenized if possible, but that change will not be
updated in database (must be inserted/replaced afterwards.)
+Otherwise, options I<method>, I<payinfo> and other cust_payby fields
+may be passed. If options are passed as a hashref, I<payinfo>
+will be updated as appropriate in the passed hashref.
+
=cut
sub realtime_tokenize {
@@ -2157,14 +2189,16 @@ sub realtime_tokenize {
my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_tokenize');
my %options = ();
+ my $outoptions; #for returning cust_payby/payinfo
if (ref($_[0]) eq 'HASH') {
%options = %{$_[0]};
+ $outoptions = $_[0];
} else {
%options = @_;
+ $outoptions = \%options;
}
# set fields from passed cust_payby
- return "No cust_payby" unless $options{'cust_payby'};
$self->_bop_cust_payby_options(\%options);
return '' unless $options{method} eq 'CC';
return '' if $self->tokenized($options{payinfo}); #already tokenized
@@ -2186,7 +2220,6 @@ sub realtime_tokenize {
# check for tokenize ability
###
- # just create transaction now, so it loads gateway_module
my $transaction = new $namespace( $payment_gateway->gateway_module,
$self->_bop_options(\%options),
);
@@ -2265,11 +2298,11 @@ sub realtime_tokenize {
#important that we not replace cust_payby here,
#because cust_payby->replace uses realtime_tokenize!
- $self->_tokenize_card($transaction,\%options);
+ $self->_tokenize_card($transaction,$outoptions);
} else {
- $error = $transaction->error_message || 'Unknown error';
+ $error = $transaction->error_message || 'Unknown error when tokenizing card';
}
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 6982834..dfcce2f 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->paymask || $self->tokenized; #make sure old mask is set
+ $self->paymask($self->mask_payinfo) unless $self->getfield('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 {