Fix PAN generation: random prefix within range, Mastercard series option
Two bugs in generateBrandPan: 1. For networks with multiple prefix rules, always picked the same rule (Mastercard always 2-series, AmEx always 37, Discover always 6011) 2. Always used the start of the range as the prefix, so Mastercard generated 51xxxxx or 2221xxxxxx every time instead of any value in 51-55 or 2221-2720 Fix both: pick a random prefix rule and a random prefix within start..end. Add a "Mastercard series" arg to PAN Generate so callers can explicitly request 5-series (51-55), 2-series (2221-2720), or leave it random. The curated sample path is unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
eba7e37185
commit
0da4c99a0c
@ -6,6 +6,7 @@
|
|||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
const PAN_BRANDS = ["Visa", "Mastercard", "American Express", "Discover"];
|
const PAN_BRANDS = ["Visa", "Mastercard", "American Express", "Discover"];
|
||||||
|
const MASTERCARD_SERIES = ["Any", "5-series (51-55)", "2-series (2221-2720)"];
|
||||||
|
|
||||||
// ── Card classification tables ────────────────────────────────────────────────
|
// ── Card classification tables ────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -264,26 +265,33 @@ function fillerDigits(length) {
|
|||||||
*
|
*
|
||||||
* @param {string} brand
|
* @param {string} brand
|
||||||
* @param {number} requestedLength
|
* @param {number} requestedLength
|
||||||
|
* @param {string} mastercardSeries - "Any", "5-series (51-55)", or "2-series (2221-2720)"
|
||||||
* @returns {{pan: string, prefixDescription: string}}
|
* @returns {{pan: string, prefixDescription: string}}
|
||||||
*/
|
*/
|
||||||
function generateBrandPan(brand, requestedLength) {
|
function generateBrandPan(brand, requestedLength, mastercardSeries = "Any") {
|
||||||
const config = PAN_BRAND_RULES[brand];
|
const config = PAN_BRAND_RULES[brand];
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new OperationError("Unsupported payment network.");
|
throw new OperationError("Unsupported payment network.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const length = config.lengths.includes(requestedLength) ? requestedLength : config.lengths[0];
|
const length = config.lengths.includes(requestedLength) ? requestedLength : config.lengths[0];
|
||||||
let selectedRule = config.prefixes[0];
|
const eligibleRules = config.prefixes.filter(r => r.lengths.includes(length));
|
||||||
|
|
||||||
if (brand === "Mastercard" && length === 16) {
|
let selectedRule;
|
||||||
selectedRule = config.prefixes[1];
|
if (brand === "Mastercard") {
|
||||||
} else if (brand === "American Express") {
|
if (mastercardSeries === "5-series (51-55)") {
|
||||||
selectedRule = config.prefixes[1];
|
selectedRule = config.prefixes[0];
|
||||||
} else if (brand === "Discover") {
|
} else if (mastercardSeries === "2-series (2221-2720)") {
|
||||||
selectedRule = config.prefixes[0];
|
selectedRule = config.prefixes[1];
|
||||||
|
} else {
|
||||||
|
selectedRule = eligibleRules[Math.floor(Math.random() * eligibleRules.length)];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedRule = eligibleRules[Math.floor(Math.random() * eligibleRules.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix = String(selectedRule.start);
|
const prefixValue = selectedRule.start + Math.floor(Math.random() * (selectedRule.end - selectedRule.start + 1));
|
||||||
|
const prefix = String(prefixValue);
|
||||||
const bodyLength = length - 1;
|
const bodyLength = length - 1;
|
||||||
const body = `${prefix}${fillerDigits(bodyLength - prefix.length)}`.substring(0, bodyLength);
|
const body = `${prefix}${fillerDigits(bodyLength - prefix.length)}`.substring(0, bodyLength);
|
||||||
|
|
||||||
@ -299,9 +307,10 @@ function generateBrandPan(brand, requestedLength) {
|
|||||||
* @param {string} brand
|
* @param {string} brand
|
||||||
* @param {string} mode
|
* @param {string} mode
|
||||||
* @param {number} length
|
* @param {number} length
|
||||||
|
* @param {string} mastercardSeries
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
function generateTestPan(brand, mode, length) {
|
function generateTestPan(brand, mode, length, mastercardSeries = "Any") {
|
||||||
const config = PAN_BRAND_RULES[brand];
|
const config = PAN_BRAND_RULES[brand];
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new OperationError("Unsupported payment network.");
|
throw new OperationError("Unsupported payment network.");
|
||||||
@ -318,7 +327,7 @@ function generateTestPan(brand, mode, length) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const generated = generateBrandPan(brand, Number(length) || config.lengths[0]);
|
const generated = generateBrandPan(brand, Number(length) || config.lengths[0], mastercardSeries);
|
||||||
const parsed = parsePan(generated.pan);
|
const parsed = parsePan(generated.pan);
|
||||||
return {
|
return {
|
||||||
brand,
|
brand,
|
||||||
@ -332,6 +341,7 @@ function generateTestPan(brand, mode, length) {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
PAN_BRANDS,
|
PAN_BRANDS,
|
||||||
|
MASTERCARD_SERIES,
|
||||||
generateTestPan,
|
generateTestPan,
|
||||||
isLuhnValid,
|
isLuhnValid,
|
||||||
parsePan,
|
parsePan,
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import { PAN_BRANDS, generateTestPan } from "../lib/Pan.mjs";
|
import { PAN_BRANDS, MASTERCARD_SERIES, generateTestPan } from "../lib/Pan.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate test PAN operation.
|
* Generate test PAN operation.
|
||||||
@ -24,7 +24,7 @@ class GenerateTestPAN extends Operation {
|
|||||||
{
|
{
|
||||||
name: "Visa curated sample",
|
name: "Visa curated sample",
|
||||||
input: "",
|
input: "",
|
||||||
args: ["Visa", "Curated sample", 16, true]
|
args: ["Visa", "Curated sample", 16, "Any", true]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
this.infoURL = "https://en.wikipedia.org/wiki/Payment_card_number";
|
this.infoURL = "https://en.wikipedia.org/wiki/Payment_card_number";
|
||||||
@ -51,6 +51,12 @@ class GenerateTestPAN extends Operation {
|
|||||||
max: 19,
|
max: 19,
|
||||||
comment: "Used only in generated mode. Networks that do not support the requested length fall back to their first supported length."
|
comment: "Used only in generated mode. Networks that do not support the requested length fall back to their first supported length."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Mastercard series",
|
||||||
|
type: "option",
|
||||||
|
value: MASTERCARD_SERIES,
|
||||||
|
comment: "Applies only when Network is Mastercard in generated mode. '5-series' restricts to the 51–55 range. '2-series' restricts to 2221–2720. 'Any' picks randomly between both ranges."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Output as JSON",
|
name: "Output as JSON",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
@ -66,8 +72,8 @@ class GenerateTestPAN extends Operation {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const [brand, mode, length, outputJson] = args;
|
const [brand, mode, length, mastercardSeries, outputJson] = args;
|
||||||
const result = generateTestPan(brand, mode, length);
|
const result = generateTestPan(brand, mode, length, mastercardSeries);
|
||||||
return outputJson ? JSON.stringify(result, null, 4) : result.pan;
|
return outputJson ? JSON.stringify(result, null, 4) : result.pan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -416,7 +416,7 @@ TestRegister.addTests([
|
|||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "PAN Generate",
|
op: "PAN Generate",
|
||||||
args: ["Visa", "Curated sample", 16, true]
|
args: ["Visa", "Curated sample", 16, "Any", true]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -427,7 +427,7 @@ TestRegister.addTests([
|
|||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "PAN Generate",
|
op: "PAN Generate",
|
||||||
args: ["American Express", "Curated sample", 15, false]
|
args: ["American Express", "Curated sample", 15, "Any", false]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user