Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ptdecker/875e5ee19c60d53e39f4ccf42e26d49e to your computer and use it in GitHub Desktop.
Save ptdecker/875e5ee19c60d53e39f4ccf42e26d49e to your computer and use it in GitHub Desktop.
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. Also, anytime 'isValid' is false an
// error reason is returned in 'errMessage'
//
function cardInfo(cardNum) {
// Issuer identification number (IIN) look-up table
// c.f. https://en.wikipedia.org/wiki/Payment_card_number
// c.f. http://www.iinbase.com/
// 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]},
{startIIN:"601100", endIIN:"601199", name:"Discover", lengths:[16]},
{startIIN:"622126", endIIN:"622925", name:"Discover", lengths:[16]},
{startIIN:"644000", endIIN:"649999", name:"Discover", lengths:[16]},
{startIIN:"650000", endIIN:"659999", name:"Discover", lengths:[16]},
];
var cardInfo = {
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,6);
// Save masked and last-four-digits version of number
cardInfo.fourDigits = cardNum.slice(-4);
cardInfo.masked = Array(cardNum.length - 3).join('*') + cardNum.slice(-4);
// 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
for (var i = 0; i < IINData[IINIndex].lengths.length && !lengthOk; i++) {
lengthOk = (IINData[IINIndex].lengths[i] === cardNum.length);
}
if (lengthOk === false) { // Avoiding not-truthy (!lengthOk) for paranoid safety
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;
} // cardInfo()
function testValidity(cardNum, expectedResult) {
card = cardInfo(cardNum);
return (card.isValid === expectedResult);
} // testValidity()
function runTests() {
// Sources:
// [1] http://testcreditcardnumbers.com/ (note "311" and "61" are not a valid IINs, see http://www.iinbase.com/)
// [2] Internal test cards
tests = [
{card:"378282246310005", isValid:true}, // American Express [1]
{card:"371449635398431", isValid:true}, // American Express [1]
{card:"378734493671000", isValid:true}, // Amercian Express Corporate [1]
{card:"371144371144376", isValid:true}, // American Express [2]
{card:"341134113411347", isValid:true}, // American Express [2]
{card:"30569309025904", isValid:false}, // Diners Club [1]
{card:"38520000023237", isValid:false}, // Diners Club [1]
{card:"6011111111111117", isValid:true}, // Discover [1]
{card:"6011000990139424", isValid:true}, // Discover [1]
{card:"6011016011016011", isValid:true}, // Discover [2]
{card:"6559906559906557", isValid:true}, // Discover [2]
{card:"3530111333300000", isValid:false}, // JCB [1]
{card:"3566002020360505", isValid:false}, // JCB [1]
{card:"5111111111111118", isValid:true}, // MasterCard [1]
{card:"5555555555554444", isValid:true}, // MasterCard [1]
{card:"5105105105105100", isValid:true}, // MasterCard [1]
{card:"5111005111051128", isValid:true}, // MasterCard [2]
{card:"5112345112345114", isValid:true}, // MasterCard [2]
{card:"5115915115915118", isValid:true}, // MasterCard II [2]
{card:"5116601234567894", isValid:true}, // MasterCard III [2]
{card:"5610591081018250", isValid:false}, // Austrian Bankcard [1]
{card:"4111111111111111", isValid:true}, // Visa [1]
{card:"4012888888881881", isValid:true}, // Visa [1]
{card:"4222222222222", isValid:true}, // Visa [1]
{card:"4000300611112224", isValid:true}, // Visa [2]
{card:"4110144110144115", isValid:true}, // Visa Commercial Card [2]
{card:"4114360123456785", isValid:true}, // Visa Commercial Card II [2]
{card:"4061724061724061", isValid:true}, // Visa Commercial Card III [2]
{card:"76009244561", isValid:false}, // Dankort (PBS) [1]
{card:"5019717010103742", isValid:false}, // Dankort (PBS) [1]
{card:"6331101999990016", isValid:false}, // Switch/Solo (Paymenttech) [1]
];
var globalPass = true;
console.log("Validity checks:")
for (var i = 0; i < tests.length; i++) {
testNum = (1000+i).toString().substring(1);
if (testValidity(tests[i].card, tests[i].isValid)) {
console.log("\t" + testNum + ': "' + tests[i].card + '": Pass');
} else {
console.log("\t" + testNum + ': "' + tests[i].card + '": Fail');
globalPass = false;
}
}
if (globalPass) {
console.log("All test cases passed");
} else {
console.error("One or more tests failed");
}
} // runTests()
runTests();
console.log(cardInfo("371449635398431"));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment