RT#71513: Card tokenization in v4+
[freeside.git] / FS / FS / cust_main / Billing_Realtime.pm
index 8cac982..3e4a438 100644 (file)
@@ -5,7 +5,7 @@ use vars qw( $conf $DEBUG $me );
 use vars qw( $realtime_bop_decline_quiet ); #ugh
 use Carp;
 use Data::Dumper;
-use Business::CreditCard 0.28;
+use Business::CreditCard 0.35;
 use FS::UID qw( dbh );
 use FS::Record qw( qsearch qsearchs );
 use FS::payby;
@@ -355,6 +355,35 @@ sub _bop_content {
   \%content;
 }
 
+sub _tokenize_card {
+  my ($self,$transaction,$payinfo,$log) = @_;
+
+  if ( $transaction->can('card_token') 
+       and $transaction->card_token 
+       and $payinfo !~ /^99\d{14}$/ #not already tokenized
+  ) {
+
+    my @cust_payby = $self->cust_payby('CARD','DCRD');
+    @cust_payby = grep { $payinfo == $_->payinfo } @cust_payby;
+    if (@cust_payby > 1) {
+      $log->error('Multiple matching card numbers for cust '.$self->custnum.', could not tokenize card');
+    } elsif (@cust_payby) {
+      my $cust_payby = $cust_payby[0];
+      $cust_payby->payinfo($transaction->card_token);
+      my $error = $cust_payby->replace;
+      if ( $error ) {
+        $log->error('Error storing token for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum.': '.$error);
+      } else {
+        $log->debug('Tokenized card for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum);
+      }
+    } else {
+      $log->debug('No matching card numbers for cust '.$self->custnum.', could not tokenize card');
+    }
+
+  }
+
+}
+
 my %bop_method2payby = (
   'CC'     => 'CARD',
   'ECHECK' => 'CHEK',
@@ -369,6 +398,8 @@ sub realtime_bop {
     unless $FS::UID::AutoCommit;
 
   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
+  my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_bop');
  
   my %options = ();
   if (ref($_[0]) eq 'HASH') {
@@ -510,11 +541,8 @@ sub realtime_bop {
       $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
       $content{expiration} = "$2/$1";
 
-      my $paycvv = exists($options{'paycvv'})
-                     ? $options{'paycvv'}
-                     : $self->paycvv;
-      $content{cvv2} = $paycvv
-        if length($paycvv);
+      $content{cvv2} = $options{'paycvv'}
+        if length($options{'paycvv'});
 
       my $paystart_month = exists($options{'paystart_month'})
                              ? $options{'paystart_month'}
@@ -764,10 +792,10 @@ sub realtime_bop {
   ###
 
   # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
-  if ( length($self->paycvv)
+  if ( length($options{'paycvv'})
        && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
   ) {
-    my $error = $self->remove_cvv;
+    my $error = $self->remove_cvv_from_cust_payby($options{payinfo});
     if ( $error ) {
       warn "WARNING: error removing cvv: $error\n";
     }
@@ -777,18 +805,7 @@ sub realtime_bop {
   # Tokenize
   ###
 
-
-  if ( $transaction->can('card_token') && $transaction->card_token ) {
-
-    if ( $options{'payinfo'} eq $self->payinfo ) {
-      $self->payinfo($transaction->card_token);
-      my $error = $self->replace;
-      if ( $error ) {
-        warn "WARNING: error storing token: $error, but proceeding anyway\n";
-      }
-    }
-
-  }
+  $self->_tokenize_card($transaction,$options{'payinfo'},$log);
 
   ###
   # result handling
@@ -1790,11 +1807,8 @@ sub realtime_verify_bop {
       $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
       $content{expiration} = "$2/$1";
 
-      my $paycvv = exists($options{'paycvv'})
-                     ? $options{'paycvv'}
-                     : $self->paycvv;
-      $content{cvv2} = $paycvv
-        if length($paycvv);
+      $content{cvv2} = $options{'paycvv'}
+        if length($options{'paycvv'});
 
       my $paystart_month = exists($options{'paystart_month'})
                              ? $options{'paystart_month'}
@@ -1918,6 +1932,8 @@ sub realtime_verify_bop {
     }
   }
 
+  my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
+
   if ( $transaction->is_success() ) {
 
     $cust_pay_pending->status('authorized');
@@ -1954,6 +1970,7 @@ sub realtime_verify_bop {
     if ( $reverse->is_success ) {
 
       $cust_pay_pending->status('done');
+      $cust_pay_pending->statustext('reversed');
       my $cpp_authorized_err = $cust_pay_pending->replace;
       return $cpp_authorized_err if $cpp_authorized_err;
 
@@ -1962,6 +1979,7 @@ sub realtime_verify_bop {
       my $e = "Authorization successful but reversal failed, custnum #".
               $self->custnum. ': '.  $reverse->result_code.
               ": ". $reverse->error_message;
+      $log->warning($e);
       warn $e;
       return $e;
 
@@ -2000,6 +2018,7 @@ sub realtime_verify_bop {
     # Neither address nor postal code matches N N N N
 
     if (my $avscode = uc($transaction->avs_code)) {
+
       # map codes to accept/warn/reject
       my $avs = {
         'American Express card' => {
@@ -2053,13 +2072,18 @@ sub realtime_verify_bop {
       my $cardtype = cardtype($content{card_number});
       if ($avs->{$cardtype}) {
         my $avsact = $avs->{$cardtype}->{$avscode};
+        my $warning = '';
         if ($avsact eq 'r') {
           return "AVS code verification failed, cardtype $cardtype, code $avscode";
         } elsif ($avsact eq 'w') {
-          warn "AVS code verification did not occur, cardtype $cardtype, code $avscode";
+          $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
         } elsif (!$avsact) {
-          warn "AVS code verification did not occur, unknown avscode, cardtype $cardtype, code $avscode";
+          $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
         } # else $avsact eq 'a'
+        if ($warning) {
+          $log->warning($warning);
+          warn $warning;
+        }
       } # else $cardtype avs handling not implemented
     } # else !$transaction->avs_code
 
@@ -2080,17 +2104,7 @@ sub realtime_verify_bop {
   # Tokenize
   ###
 
-  if ( $transaction->can('card_token') && $transaction->card_token ) {
-
-    if ( $options{'payinfo'} eq $self->payinfo ) {
-      $self->payinfo($transaction->card_token);
-      my $error = $self->replace;
-      if ( $error ) {
-        warn "WARNING: error storing token: $error, but proceeding anyway\n";
-      }
-    }
-
-  }
+  $self->_tokenize_card($transaction,$options{'payinfo'},$log);
 
   ###
   # result handling