Last active
April 26, 2018 15:31
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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