Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions src/lib/get-request-value.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,171 @@
/**
* VIN (Vehicle Identification Number) Constants and Utilities
*
* VIN Structure (ISO 3779):
* - Positions 1-3: World Manufacturer Identifier (WMI)
* - Positions 4-8: Vehicle Descriptor Section (VDS)
* - Position 9: Check digit (calculated using ISO 3779 algorithm)
* - Position 10: Model year
* - Position 11: Plant code
* - Positions 12-17: Sequential number (vehicle serial)
*
* Valid characters: A-H, J-N, P, R-Z (excluding I, O, Q), 0-9
*/

const type_flag = '_type',
// VIN Constants
VIN_ALLOWED_CHARS = 'ABCDEFGHJKLMNPRSTUVWXYZ0123456789',
VIN_SERIAL_CHARS = '0123456789',
VIN_FORMAT_REGEX = /^[A-HJ-NPR-Z0-9]{17}$/,
VIN_CHECK_DIGIT_POSITION = 8,
VIN_SERIAL_START_POSITION = 11,
VIN_LENGTH = 17,

/**
* Weights used for VIN check digit calculation per ISO 3779
* Position 9 (index 8) has weight 0 as it's the check digit itself
*/
VIN_WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2],

/**
* Character transliteration values for VIN check digit calculation
* Letters are mapped to numeric values, numbers remain unchanged
*/
VIN_TRANSLITERATION = {
A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8,
J: 1, K: 2, L: 3, M: 4, N: 5, P: 7, R: 9,
S: 2, T: 3, U: 4, V: 5, W: 6, X: 7, Y: 8, Z: 9,
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
'5': 5, '6': 6, '7': 7, '8': 8, '9': 9
},

/**
* Returns a random character from the provided character set
* @param {string} chars - String of characters to choose from
* @returns {string} A single random character
*/
getRandomChar = function (chars) {
return chars[Math.floor(Math.random() * chars.length)];
},

/**
* Gets the numeric value of a VIN character for check digit calculation
* @param {string} char - Single character from a VIN
* @returns {number} Numeric value (0-9) or 0 for invalid characters
*/
getVinCharValue = function (char) {
const mapped = VIN_TRANSLITERATION[char];
if (typeof mapped !== 'number') {
console.warn(`Invalid VIN character: '${char}'. Using 0 as fallback.`);
return 0;
}
return mapped;
},

/**
* Calculates the VIN check digit (position 9) per ISO 3779
* @param {Array<string>} vinChars - Array of 17 VIN characters
* @returns {string} Check digit ('0'-'9' or 'X' for 10)
*/
calculateVinCheckDigit = function (vinChars) {
let sum = 0;
for (let i = 0; i < vinChars.length; i++) {
sum += getVinCharValue(vinChars[i]) * VIN_WEIGHTS[i];
}
const remainder = sum % 11;
return remainder === 10 ? 'X' : `${remainder}`;
},

/**
* Generates a random valid VIN with correct check digit
* @returns {string} A 17-character VIN string
*/
generateRandomVin = function () {
const vinChars = new Array(VIN_LENGTH);
for (let i = 0; i < vinChars.length; i++) {
if (i === VIN_CHECK_DIGIT_POSITION) {
// Placeholder; will be calculated below
vinChars[i] = '0';
} else {
vinChars[i] = getRandomChar(VIN_ALLOWED_CHARS);
}
}
vinChars[VIN_CHECK_DIGIT_POSITION] = calculateVinCheckDigit(vinChars);
return vinChars.join('');
},

/**
* Mutates the serial number portion (positions 12-17) of a VIN
* while preserving the WMI and VDS, then recalculates check digit
* @param {string} vin - Base VIN to mutate
* @returns {string} New VIN with mutated serial and valid check digit
*/
mutateVinSerial = function (vin) {
const vinChars = vin.toUpperCase().split('');
// Mutate serial portion (positions 12-17, indices 11-16)
for (let i = VIN_SERIAL_START_POSITION; i < vinChars.length; i++) {
vinChars[i] = getRandomChar(VIN_SERIAL_CHARS);
}
// Recalculate check digit with mutated serial
vinChars[VIN_CHECK_DIGIT_POSITION] = calculateVinCheckDigit(vinChars);
return vinChars.join('');
},

/**
* Picks a random element from an array
* @param {Array} pool - Array to pick from
* @returns {*} Random element from the array
*/
pickFromPool = function (pool) {
return pool[Math.floor(Math.random() * pool.length)];
},

/**
* Normalizes and validates a pool of VINs
* Filters out invalid VINs, trims whitespace, converts to uppercase
* @param {Array} pool - Array of potential VIN strings
* @returns {Array<string>} Array of valid, normalized VINs
*/
normalizeVinPool = function (pool) {
if (!Array.isArray(pool)) {
return [];
}
return pool
.map(vin => (typeof vin === 'string' ? vin.trim().toUpperCase() : ''))
.filter(vin => VIN_FORMAT_REGEX.test(vin));
},

/**
* Validates a VIN string according to ISO 3779 standard
* Checks length, character validity, and check digit
* @param {string} vin - VIN string to validate
* @returns {boolean} True if VIN is valid
*/
validateVin = function (vin) {
if (!vin || vin.length !== VIN_LENGTH) {
return false;
}
const upper = vin.toUpperCase();

// Validate format (allowed characters)
if (!VIN_FORMAT_REGEX.test(upper)) {
return false;
}

// Validate check digit
let sum = 0;
for (let i = 0; i < upper.length; i++) {
const value = VIN_TRANSLITERATION[upper[i]];
if (typeof value !== 'number') {
return false;
}
sum += value * VIN_WEIGHTS[i];
}
const remainder = sum % 11;
const expectedCheckDigit = remainder === 10 ? 'X' : `${remainder}`;
return upper[VIN_CHECK_DIGIT_POSITION] === expectedCheckDigit;
},

generators = {
literal: function (request) {
return request.value;
Expand All @@ -10,6 +177,30 @@ const type_flag = '_type',
value += request.template;
}
return value.substring(0, request.size);
},
/**
* VIN Generator
* @param {Object} request - Configuration object
* @param {string} request.mode - 'valid' for random VIN, 'real' for pool-based
* @param {Array<string>} [request.pool] - Pool of real VINs (for 'real' mode)
* @param {boolean} [request.mutateSerial] - Whether to mutate serial portion (for 'real' mode)
* @returns {string} Generated VIN
*/
vin: function (request) {
const mode = request.mode || 'valid';
if (mode === 'real') {
const pool = normalizeVinPool(request.pool);
if (!pool.length) {
// No valid VINs in pool, fall back to random generation
return generateRandomVin();
}
const picked = pickFromPool(pool);
if (request.mutateSerial) {
return mutateVinSerial(picked);
}
return picked;
}
return generateRandomVin();
}
};

Expand All @@ -23,3 +214,17 @@ export function getRequestValue(request) {
}
return generator(request);
}

/**
* Export VIN validation function for testing
* @param {string} vin - VIN to validate
* @returns {boolean} True if valid
*/
export function isValidVin(vin) {
return validateVin(vin);
}

/**
* Export VIN constants for testing
*/
export { VIN_WEIGHTS, VIN_TRANSLITERATION };
110 changes: 110 additions & 0 deletions template/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,116 @@
"1E-16", "-1", "0.0001",
"1,234,567", "1.234.567,89"
],
"VINs": {
"Random valid VIN": { "_type": "vin", "mode": "valid" },
"Random real VIN": {
"_type": "vin",
"mode": "real",
"mutateSerial": true,
"pool": [
"19XFB2F55CE065344",
"19XFB2F57FE015985",
"1C3CDFAHXDD319370",
"1C4BJWDGXEL161008",
"1D7HU182X7S223329",
"1FADP3F25DL231400",
"1FADP5AU7DL509038",
"1FAFP444X3F407826",
"1FDKF37G6VEB24646",
"1FDKF37GXVEB21815",
"1FDXE45S7YHB32330",
"1FMCU0J92DUC12004",
"1FTFW1CF9DFA31880",
"1FTFW1EV1AFA88803",
"1FUJCRCK56PV71604",
"1G11C5SL5FF137854",
"1G1AK55FX67672309",
"1G1YF2D77E5122846",
"1G3HN52K0S4830196",
"1G6DA5E5XC0104969",
"1G6DG5EY2B0102432",
"1G8ZK5275WZ228518",
"1GAZG1FGXE1111677",
"1GCDT148168266667",
"1GCVKSEC5EZ229744",
"1GKCS13W0Y2207683",
"1GKDT13SX52126717",
"1GKKRRKD7FJ108514",
"1GNEC13T93R313404",
"1GNFH15TX31190570",
"1GNLRGED4AS111641",
"1HGCG555XWA157274",
"1HGCG56632A166187",
"1HGCP2F36CA226503",
"1HGCP2F37CA050576",
"1HGCS2B83AA000965",
"1HGEM21525L051619",
"1HTSDAAN1YH233592",
"1J4GK48K67W506892",
"1J4GW58N22C111641",
"1J8GA59198L640523",
"1J8GS48K37C559050",
"1N4AL11D36N436040",
"1N4AL21E89N556158",
"1N4AL3AP0FN304875",
"1N4AL3AP7EC103419",
"1N4BL11D45C244172",
"1NXBR32E74Z245624",
"1NXBU4EE4AZ171431",
"1YVHZ8BH2B5M22068",
"2C3KA43R58H175878",
"2C3KA53G46H405086",
"2C4RC1CG0FR610217",
"2C4RC1CGXDR801737",
"2C4RDGCG9ER265252",
"2FMDK4JCXABA86201",
"2G1FB1E30E9322561",
"2G1WF5E37D1146066",
"2G2FV32G822122005",
"2GCEK19T041221983",
"2GKFLTEK7D6228236",
"2HGFG3B87FH507235",
"2T1KR32E03C109092",
"2T3BFREV0FW278448",
"2T3DFREV6EW187837",
"2V4RW3DG2BR669772",
"3C63DRGL7CG217087",
"3C6TRVAG2EE116026",
"3D4PG4FB6AT130074",
"3FA6P0H77FR211122",
"3FA6P0RU4FR147044",
"3FADP4BJ5EM191726",
"3FADP4EJ8BM165015",
"3GCUKREC3EG380201",
"3GNCA53V99S568422",
"3GTP1VE05CG154205",
"3MEHM07Z67R660001",
"3N1CN7AP2EL817258",
"4T1BE30K03U743425",
"4T1BF1FK9FU893482",
"4T1BF3EK2BU670479",
"4T1BF3EK9AU009045",
"4TANL42NXWZ102130",
"55SWF6GB9FU033851",
"5FNRL18623B024271",
"5FNRL38627B458935",
"5FRYD4H8XEB023334",
"5J6RE3H34BL025729",
"5J6RE3H70BL009370",
"5J6RM4H70DL080841",
"5J6YH2H72BL005124",
"5NPDH4AE1EH501856",
"5TDDK3EHXDS250974",
"5XYZG3AB2CG134978",
"5YFBURHE0EP106902",
"5YFBURHE9EP030774",
"JF2SHABC5BH716784",
"JHMCB7652PC029395",
"JHMCG56702C021652",
"JM1BM1K77F1239111"
]
}
},
"Amounts": ["5000", "$5,000", "$5 000", "$5,000.00"],
"Currencies": {
"No decimals": "JPY",
Expand Down
Loading