From 89ecc204df64191d74ddb2e803e16b01f70b749d Mon Sep 17 00:00:00 2001 From: abarone-btf Date: Tue, 2 Dec 2025 22:01:15 -0300 Subject: [PATCH] fix: Update RFC validation to include legacy alphabet --- src/mx/rfc.spec.ts | 6 +++++ src/mx/rfc.ts | 65 +++++++++++++++++++++++----------------------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/mx/rfc.spec.ts b/src/mx/rfc.spec.ts index b2c7a5f2..dff5133b 100644 --- a/src/mx/rfc.spec.ts +++ b/src/mx/rfc.spec.ts @@ -61,6 +61,12 @@ describe('mx/rfc', () => { expect(result.isValid).toEqual(false); }); + it('validate:SOTO800101110', () => { + const result = validate('SOTO800101110'); + + expect(result.isValid && result.compact).toEqual('SOTO800101110'); + }); + it('format:GODE561231GR8', () => { const result = format('GODE561231GR8'); diff --git a/src/mx/rfc.ts b/src/mx/rfc.ts index 94057417..2961ef5d 100644 --- a/src/mx/rfc.ts +++ b/src/mx/rfc.ts @@ -80,10 +80,12 @@ const nameBlacklist = new Set([ 'RUIN', ]); +// Official alphabet per SAT (Anexo 20). Includes '&' and 'Ñ'. const checkAlphabet = '0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ Ñ'; -// const checkAlphabetDict: Record = checkAlphabet -// .split('') -// .reduce((acc, c, idx) => ({ ...acc, [c]: idx }), {}); + +// Legacy alphabet (Base 36) used in older systems. +// It excludes '&' and 'Ñ'. Space is added to support padding for companies. +const checkAlphabetLegacy = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ '; const impl: Validator = { name: 'Mexican Tax Number', @@ -149,34 +151,33 @@ const impl: Validator = { } const [front, check] = strings.splitAt(value, -1); - - const sum = weightedSum(front.padStart(12, ' '), { - modulus: 11, - alphabet: checkAlphabet, - weights: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], - reverse: true, - }); - - // const sum = value - // .substr(0, value.length - 1) - // .padStart(12, ' ') - // .split('') - // .reduce( - // (acc, c, idx) => acc + (checkAlphabetDict[c] ?? 0) * (13 - idx), - // 0, - // ); - const mod = 11 - (sum % 11); - let val; - if (mod === 11) { - val = '0'; - } else if (mod === 10) { - val = 'A'; - } else { - val = String(mod); - } - - if (check !== val) { - return { isValid: false, error: new exceptions.InvalidChecksum() }; + const paddedInput = front.padStart(12, ' '); + + const calculateChecksum = (alphabet: string) => { + const sum = weightedSum(paddedInput, { + modulus: 11, + alphabet: alphabet, + weights: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + reverse: true, + }); + + const mod = 11 - (sum % 11); + if (mod === 11) return '0'; + if (mod === 10) return 'A'; + return String(mod); + }; + + // Try with official SAT alphabet first + const valOfficial = calculateChecksum(checkAlphabet); + + if (check !== valOfficial) { + // If it fails, try with Legacy alphabet (Base 36) + // This handles older RFCs generated without '&' or 'Ñ' support + const valLegacy = calculateChecksum(checkAlphabetLegacy); + + if (check !== valLegacy) { + return { isValid: false, error: new exceptions.InvalidChecksum() }; + } } } @@ -190,4 +191,4 @@ const impl: Validator = { }; export const { name, localName, abbreviation, validate, format, compact } = - impl; + impl; \ No newline at end of file