UInIQ
3/15/2018 - 3:42 PM

Determines if a credit card number is valid and returns the name of the issuing network.

Determines if a credit card number is valid and returns the name of the issuing network.

// cardInfo()
//
// Determines if a credit card number is valid and returns the name of the issuing network.  When
// passed a purported card number as a candidate if the card number is valid, the cardInfo()
// function returns a boolean flag ('isValid') indicating if the card is a valid number and a 
// string ('network') containing the name of the issuing network.  Card validity is determined
// based upon the card number length and Luhn check digit value.  Valid card lengths are based
// upon the card lengths valid for a given issuing network.  The Luhn check digit is calculated
// using the Luhn algorithm.  The function contains a static table ('IINData') that defines the
// supported issuing network.  The table is not a complete list--it is only meant to contain
// the networks supported by the system using this function.  But, it can easily be expanded
// too support additional networks.  cardInfo() expects that the card number be passed as a 
// string and will log a console error and return 'isValid' of false if this is not the case. The
// function does not return the card number itself in the object for safety, but does return the
// last four digits and a masked version as a conveinence.
//

function cardInfo(cardNum) {

  // Issuer identification number (IIN) look-up table
  // c.f. https://en.wikipedia.org/wiki/Payment_card_number
  // Expand this table to support additional issuing networks.  'startIIN' and 'endIIN' define
  // the range of IINs supported by an issuing network.  'name' defines the network's common
  // name.  And, 'lengths' is an array containing the supported card number lengths valid
  // for the IIN range.
  const IINData = [
    {startIIN:"222100", endIIN:"272099", name:"MasterCard", lengths:[16]},
    {startIIN:"340000", endIIN:"349999", name:"American Express", lengths:[15]},    
    {startIIN:"370000", endIIN:"379999", name:"American Express", lengths:[15]}, 
    {startIIN:"400000", endIIN:"499999", name:"Visa", lengths:[13, 16, 19]},
    {startIIN:"510000", endIIN:"559999", name:"MasterCard", lengths:[16]},    
  ];
  
  var cardInfo = {
    fourDigits: cardNum.slice(-4),
    masked: Array(cardNum.length - 4).join('*') + cardNum.slice(-4),
    isValid: false
  }
  var IIN, IINIndex, lengthOk, luhnOk, sum, parity;
  
  // Set card primary account number or exit if card number is not a string
  if (typeof cardNum !== "string") {
    console.error("'cardInfo()' passed an unexpected type (" + (typeof cardNum) + ") instead of a string");
    cardInfo.errMessage = "Internal error: 'cardInfo()' passed an unexpected type";
    return cardInfo;
  }

  // Get IIN or exit if PAN is less then absolute minimum length (6)
  if (cardNum.length < 6) {
    cardInfo.errMessage = "Card number is not long enough";
    return cardInfo;
  }
  IIN = cardNum.substring(0, 5);
    
  // Search IIN table and exit if an entry is not found
  for (IINIndex = 0; IINIndex < IINData.length; IINIndex++) {
    if (IINData[IINIndex].startIIN <= IIN && IIN <= IINData[IINIndex].endIIN) {
      break;
    }
  }
  if (IINIndex === IINData.length) {
    cardInfo.errMessage = "Issuing network is not supported"
    return cardInfo;
  }
  cardInfo.network = IINData[IINIndex].name;
  
  // Check length requirements and exit if length is too short for issuing network
  lengthOk = false;
  for (var i = 0; i < IINData[IINIndex].lengths.length && !lengthOk; i++) {
    lengthOk = (IINData[IINIndex].lengths[i] === cardNum.length);
  }
  if (!lengthOk) {
    cardInfo.errMessage = IINData[IINIndex].name + " card numbers must be " + IINData[IINIndex].lengths + " digits in length";
    return cardInfo;
  }

  // Check Luhn 
  // c.f. https://en.wikipedia.org/wiki/Luhn_algorithm
  sum = 0;
  parity = cardNum.length % 2;
  for (var i = 0; i < cardNum.length - 1; i++) {
    let doubleDigit = (((i % 2) === parity) ? 2 : 1) * cardNum[i];
    let sumOfDigits = (Math.floor(doubleDigit / 10) + doubleDigit % 10);
    sum += sumOfDigits;
  }
  luhnOk = (String(9 * sum).slice(-1) === cardNum.slice(-1));
  if (!luhnOk) {
    cardInfo.errMessage = "Card number is invalid (incorrect Luhn value)";
    return;
  }
    
  // Card is valid if both the length is correct and the Luhn check is valid
  cardInfo.isValid = lengthOk && luhnOk;
  
  // Return card
  return cardInfo;
}

function test(cardNum, expectedResult) {
  card = cardInfo(cardNum);
  result = (card.cardNum + " " + card.isValid + " " + card.IIN);
  return (result === expectedResult);
}

function runTests() {
  
  tests = [
    {card:undefined, result:"undefined false undefined"},
    {card:"378282246310005", result:"378282246310005 true 37828"}
  ];
  
  for (var i = 0; i < tests.length; i++) {
    testNum = (1000+i).toString().substring(1);
    if (test(tests[i].card, tests[i].result)) {
      console.log(testNum + ': "' + tests[i].card + '": Pass');
    } else {
      console.log(testNum + ': "' + tests[i].card + '": Fail (' + tests[i].result + ')');
    }
  }
}