Skip to content
Merged
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
112 changes: 112 additions & 0 deletions lib/card_bins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const CARD_BINS = {
'400443': '40036',
'400820': '40131',
'402766': '40127',
'403707': '40002',
'404360': '40036',
'405306': '40002',
'409851': '40012',
'415231': '40012',
'416916': '40137',
'418914': '40072',
'418928': '40072',
'419821': '40127',
'421003': '40030',
'421316': '40021',
'426808': '40132',
'430967': '40021',
'433454': '40072',
'434798': '40058',
'455504': '40012',
'455509': '40012',
'455510': '40012',
'455511': '40012',
'457249': '40130',
'457476': '40136',
'460766': '40128',
'462974': '40036',
'465828': '40036',
'469693': '40133',
'473701': '40036',
'474174': '40058',
'474176': '40021',
'476588': '40072',
'476684': '40002',
'476687': '40002',
'477210': '40012',
'477211': '40012',
'477213': '40012',
'477214': '40012',
'477291': '40012',
'477292': '40012',
'481279': '37019',
'481515': '40012',
'481516': '40012',
'483030': '40021',
'483104': '40127',
'483112': '40127',
'491089': '40021',
'491282': '40021',
'491344': '37019',
'491566': '40072',
'491580': '40072',
'511114': '40072',
'512280': '40030',
'517439': '40012',
'517712': '40002',
'517721': '40002',
'518004': '40002',
'518853': '40002',
'518899': '40002',
'520021': '40002',
'520116': '40002',
'520416': '40002',
'520694': '40002',
'520698': '40002',
'522130': '40002',
'524711': '40002',
'525424': '40002',
'525678': '40002',
'526192': '37166',
'526354': '40127',
'526424': '40072',
'526476': '40042',
'526498': '40036',
'527333': '40036',
'528843': '40002',
'528851': '40002',
'529091': '40002',
'530056': '40002',
'534381': '40127',
'535875': '40012',
'535943': '40062',
'539975': '40036',
'539978': '40060',
'542015': '40012',
'542073': '40012',
'543924': '40138',
'545631': '40002',
'548234': '40002',
'548236': '40002',
'549138': '40002',
'549639': '40002',
'549949': '40002',
'551238': '40127',
'551507': '37166',
'553467': '40127',
'553800': '40113',
'554629': '40012',
'557604': '40127',
'557907': '40014',
'557908': '40014',
'557909': '40014',
'557910': '40014',
'557920': '40044',
'557922': '40044',
'558426': '40002',
'585678': '40002',
'854829': '40002',
'882290': '40002',
};

module.exports = { CARD_BINS };
5 changes: 3 additions & 2 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export const BANKS: Record<string, string>;
export const BANK_NAMES: Record<string, string>;
export const CARD_BINS: Record<string, string>;
export function validateClabe(clabe: string): boolean;
export function computeControlDigit(clabe: string): string;
export function getBankName(clabe: string | null | undefined): string | null;
export function getBankNameOrThrow(clabe: string): string;
export function getBankName(account: string | null | undefined): string | null;
export function getBankNameOrThrow(account: string): string;
export function generateNewClabes(
numberOfClabes: number,
prefix: string,
Expand Down
67 changes: 48 additions & 19 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { BANKS, BANK_NAMES } = require('./banks.js');
const { CARD_BINS } = require('./card_bins.js');

const CLABE_LENGTH = 18;
const CLABE_WEIGHTS = [3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7];
Expand Down Expand Up @@ -35,33 +36,60 @@ function validateClabe(clabe) {
clabe.substring(CLABE_LENGTH - 1) === computeControlDigit(clabe);
}

function getBankName(clabe) {
function getBankName(account) {
/*
Devuelve el nombre del banco basado en los primeros 3 dígitos.
Devuelve null si la entrada es nula/vacía o si el código no está registrado.
Para mantener el comportamiento previo (lanzar excepción), usar getBankNameOrThrow.
Devuelve el nombre del banco basado en el tipo de cuenta:
- 18 dígitos: CLABE → busca por código ABM (primeros 3 dígitos)
- 15-16 dígitos: Tarjeta → busca por BIN (primeros 6 dígitos)
Devuelve null si la entrada es inválida o si el código no está registrado.
Para mantener el comportamiento estricto (lanzar excepción), usar getBankNameOrThrow.
*/
if (typeof clabe !== 'string' || clabe.length < 3) return null;
const code = clabe.substring(0, 3);
const bankName = BANK_NAMES[BANKS[code]];
return bankName === undefined ? null : bankName;
if (typeof account !== 'string' || account.length < 3) return null;

if (account.length === CLABE_LENGTH) {
const code = account.substring(0, 3);
const bankName = BANK_NAMES[BANKS[code]];
return bankName === undefined ? null : bankName;
}
Comment on lines +49 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate full CLABE before ABM lookup in both APIs.

On Line 49 and Line 74, the 18-char path only checks length and prefix, so invalid CLABEs (including wrong control digit) can still resolve to a bank name. That breaks the documented invalid-input contract.

🐛 Proposed fix
 function getBankName(account) {
@@
   if (account.length === CLABE_LENGTH) {
+    if (!validateClabe(account)) return null;
     const code = account.substring(0, 3);
     const bankName = BANK_NAMES[BANKS[code]];
     return bankName === undefined ? null : bankName;
   }

   if (account.length >= 15 && account.length <= 16) {
+    if (!isANumber(account)) return null;
     const bin = account.substring(0, 6);
     const bankName = BANK_NAMES[CARD_BINS[bin]];
     return bankName === undefined ? null : bankName;
   }
@@
 function getBankNameOrThrow(account) {
@@
   if (account.length === CLABE_LENGTH) {
+    if (!validateClabe(account)) {
+      throw new Error('Cuenta inválida: CLABE no válida');
+    }
     const code = account.substring(0, 3);
     const bankName = BANK_NAMES[BANKS[code]];
@@
   if (account.length >= 15 && account.length <= 16) {
+    if (!isANumber(account)) {
+      throw new Error('Cuenta inválida: tarjeta debe contener solo dígitos');
+    }
     const bin = account.substring(0, 6);
     const bankName = BANK_NAMES[CARD_BINS[bin]];

Also applies to: 74-81

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/index.js` around lines 49 - 53, The current checks only verify CLABE
length and prefix when resolving bank names; update both lookup sites (the block
using account.substring(0, 3) and the similar branch around lines 74-81) to
validate the full 18-digit CLABE using the official CLABE control-digit
algorithm before performing the ABM/bank-name lookup. Implement or call a helper
like validateClabe(account) that computes and verifies the control digit (using
the standard weights and modulus 10 rule) and only proceed to derive code =
account.substring(0,3) and BANK_NAMES[BANKS[code]] if validateClabe returns
true; otherwise return null (or the existing invalid-input response) to preserve
the contract.


if (account.length >= 15 && account.length <= 16) {
const bin = account.substring(0, 6);
const bankName = BANK_NAMES[CARD_BINS[bin]];
return bankName === undefined ? null : bankName;
}

return null;
}

function getBankNameOrThrow(clabe) {
function getBankNameOrThrow(account) {
/*
Versión estricta de getBankName. Lanza Error si la CLABE es inválida o si el
código no está registrado. Equivalente al comportamiento de getBankName antes
de la versión 1.3.0.
Versión estricta de getBankName. Lanza Error si la cuenta es inválida o si el
código no está registrado.
Soporta CLABEs (18 dígitos) y tarjetas (15-16 dígitos).
*/
if (typeof clabe !== 'string' || clabe.length < 3) {
throw new Error('CLABE inválida: debe ser un string de al menos 3 caracteres');
if (typeof account !== 'string' || account.length < 3) {
throw new Error('Cuenta inválida: debe ser un string de al menos 3 caracteres');
}
const code = clabe.substring(0, 3);
const bankName = BANK_NAMES[BANKS[code]];
if (bankName === undefined) {
throw new Error('Ningún banco tiene este código ' + code);

if (account.length === CLABE_LENGTH) {
const code = account.substring(0, 3);
const bankName = BANK_NAMES[BANKS[code]];
if (bankName === undefined) {
throw new Error('Ningún banco tiene este código ' + code);
}
return bankName;
}

if (account.length >= 15 && account.length <= 16) {
const bin = account.substring(0, 6);
const bankName = BANK_NAMES[CARD_BINS[bin]];
if (bankName === undefined) {
throw new Error('Ningún banco tiene este BIN ' + bin);
}
return bankName;
}
return bankName;

throw new Error('Cuenta inválida: debe ser CLABE (18 dígitos) o tarjeta (15-16 dígitos)');
}

function generateNewClabes(numberOfClabes, prefix) {
Expand Down Expand Up @@ -148,6 +176,7 @@ function removeBank(bankCodeBanxico) {
module.exports = {
BANKS,
BANK_NAMES,
CARD_BINS,
validateClabe,
computeControlDigit,
getBankName,
Expand Down
2 changes: 0 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clabe-js",
"version": "1.3.0",
"version": "1.4.0",
"description": "Validate, generate and resolve bank names for Mexican CLABE numbers.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
36 changes: 33 additions & 3 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const { expect } = require('chai');
const VALID_CLABE = '002000000000000008';
const INVALID_CLABE_CONTROL_DIGIT = '002000000000000007';
const INVALID_CLABE_BANK_CODE = '000000000000000000'; // Control digit es valido
const VALID_CARD_BBVA = '4152310000000000'; // BIN 415231 -> 40012 -> BBVA Mexico
const VALID_CARD_BANAMEX = '5177120000000000'; // BIN 517712 -> 40002 -> Banamex


describe('computeControlDigit', function () {
Expand Down Expand Up @@ -67,19 +69,47 @@ describe('getBankName', function () {
});
});

describe('getBankName con tarjetas', function () {
it('resuelve BBVA Mexico para tarjeta con BIN 415231', function () {
expect(getBankName(VALID_CARD_BBVA)).to.equal('BBVA Mexico');
});
it('resuelve Banamex para tarjeta con BIN 517712', function () {
expect(getBankName(VALID_CARD_BANAMEX)).to.equal('Banamex');
});
it('retorna null para tarjeta con BIN desconocido', function () {
expect(getBankName('0000000000000000')).to.equal(null);
});
it('soporta tarjetas de 15 dígitos (Amex)', function () {
expect(getBankName('415231000000000')).to.equal('BBVA Mexico');
});
it('retorna null para longitudes no soportadas (10 dígitos)', function () {
expect(getBankName('4152310000')).to.equal(null);
});
});


describe('getBankNameOrThrow', function () {
it('returns bank name if code exists', function () {
it('returns bank name if code exists (CLABE)', function () {
expect(getBankNameOrThrow(VALID_CLABE)).to.equal('Banamex');
});
it('returns bank name for card number', function () {
expect(getBankNameOrThrow(VALID_CARD_BBVA)).to.equal('BBVA Mexico');
});
it('throws when bank code is unknown', function () {
expect(() => getBankNameOrThrow(INVALID_CLABE_BANK_CODE)).to.throw(Error);
});
it('throws when CLABE is null', function () {
it('throws when card BIN is unknown', function () {
expect(() => getBankNameOrThrow('0000000000000000')).to.throw(Error);
});
it('throws when input is null', function () {
expect(() => getBankNameOrThrow(null)).to.throw(Error);
});
it('throws when CLABE is too short', function () {
it('throws when input is too short', function () {
expect(() => getBankNameOrThrow('00')).to.throw(Error);
});
it('throws when length is not CLABE nor card', function () {
expect(() => getBankNameOrThrow('4152310000')).to.throw(Error);
});
});

describe('addBank / removeBank', function () {
Expand Down
Loading