71513: Card tokenization [bug fixes to previous checkpoint]
authorJonathan Prykop <jonathan@freeside.biz>
Sat, 5 Nov 2016 06:20:35 +0000 (01:20 -0500)
committerJonathan Prykop <jonathan@freeside.biz>
Sat, 5 Nov 2016 06:20:35 +0000 (01:20 -0500)
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/payinfo_Mixin.pm
httemplate/misc/process/payment.cgi

index 7718f7a..e7a8030 100644 (file)
@@ -376,44 +376,16 @@ sub _bop_content {
 }
 
 sub _tokenize_card {
 }
 
 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 $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 = (
 }
 
 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
 
 
   #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}},
   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},
     '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} || '',
     '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});
   };
   $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( $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),          
   $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 ) {
   ) {
     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";
     }
   }
       warn "WARNING: error removing cvv: $error\n";
     }
   }
@@ -827,8 +801,15 @@ sub realtime_bop {
   # Tokenize
   ###
 
   # 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
 
   ###
   # 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');
     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() ) {
   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,
        '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,
        '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' : '',
        '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} )
     } );
     #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 $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
 
   # 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'},
     '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} || '',
     '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});
   };
   $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;
 
       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),          
     $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
   ###
 
   # 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!
   #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
 
   ###
   # result handling
@@ -2264,9 +2257,16 @@ sub realtime_tokenize {
 
   if ( $transaction->card_token() ) { # no is_success flag
 
 
   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!
     #because cust_payby->replace uses realtime_tokenize!
-    $self->_tokenize_card($transaction,\%options,$log);
+    $self->_tokenize_card($transaction,\%options);
 
   } else {
 
 
   } else {
 
@@ -2278,10 +2278,19 @@ sub realtime_tokenize {
 
 }
 
 
 }
 
+
+=item tokenized PAYINFO
+
+Convenience wrapper for L<FS::payinfo_Mixin/tokenized>
+
+PAYINFO is required
+
+=cut
+
 sub tokenized {
   my $this = shift;
   my $payinfo = shift;
 sub tokenized {
   my $this = shift;
   my $payinfo = shift;
-  $payinfo =~ /^99\d{14}$/;
+  FS::cust_pay->tokenized($payinfo);
 }
 
 =back
 }
 
 =back
index a0a2cbc..6982834 100644 (file)
@@ -67,7 +67,7 @@ sub payinfo {
   my($self,$payinfo) = @_;
 
   if ( defined($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 {
     $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;
 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}$/;
 }
 
   $payinfo =~ /^99\d{14}$/;
 }
 
index 84687f0..1532605 100644 (file)
@@ -193,6 +193,11 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
 
     errorpage("error saving info, payment not processed: $error")
       if $error;       
 
     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
+
   }
 
 }
   }
 
 }