From 8c27e58499975469a613a8701fe4b52315fccb5e Mon Sep 17 00:00:00 2001 From: Menyin Chang Date: Thu, 16 Jul 2026 09:55:16 -0300 Subject: [PATCH] fix(mx/rfc): do not enforce check digit, matching python-stdnum default The RFC check digit algorithm is not reliable for real, SAT-issued numbers: python-stdnum estimates ~1.5% of RFCs in circulation have check digits that do not match the published algorithm, so its validate() skips check digit verification unless the caller opts in via validate_check_digits=True. The TypeScript port enforced the check digit unconditionally, so real RFCs such as PAF070213GI1 (expected digit 'A' per the algorithm) and the generic foreign-resident RFC XEXX010101000 were rejected with InvalidChecksum, contradicting both the reference implementation and this module's own JSDoc ('It does not check for control letter'). Remove the check digit enforcement (and the [1-9A-V][1-9A-Z][0-9A] serial pattern check, which python-stdnum also only applies under the opt-in flag), update the VACE-460910-SX6 expectation to match the python-stdnum docstring, and re-enable the previously commented-out XEXX010101000 test. --- src/mx/rfc.spec.ts | 22 +++++++++++++++++----- src/mx/rfc.ts | 47 ++-------------------------------------------- 2 files changed, 19 insertions(+), 50 deletions(-) diff --git a/src/mx/rfc.spec.ts b/src/mx/rfc.spec.ts index dff5133b..f96b1a1d 100644 --- a/src/mx/rfc.spec.ts +++ b/src/mx/rfc.spec.ts @@ -37,11 +37,13 @@ describe('mx/rfc', () => { expect(result.isValid && result.compact).toEqual('SALC7304253S0'); }); - // it('validate:XEXX010101000', () => { - // const result = validate('XEXX010101000'); + it('validate:XEXX010101000', () => { + // Generic RFC for foreign residents; its serial/check digits do not + // follow the check digit algorithm but it is a real, valid RFC + const result = validate('XEXX010101000'); - // expect(result.isValid && result.compact).toEqual('XEXX010101000'); - // }); + expect(result.isValid && result.compact).toEqual('XEXX010101000'); + }); it('validate:RET130705MD5', () => { const result = validate('RET130705MD5'); @@ -56,9 +58,19 @@ describe('mx/rfc', () => { }); it('validate:VACE-460910-SX6', () => { + // python-stdnum docstring example: fails the check digit algorithm but + // is accepted because check digit validation is disabled by default const result = validate('VACE-460910-SX6'); - expect(result.isValid).toEqual(false); + expect(result.isValid && result.compact).toEqual('VACE460910SX6'); + }); + + it('validate:PAF-070213-GI1', () => { + // SAT-issued company RFC whose check digit does not match the published + // algorithm (expected 'A'); must validate like python-stdnum does + const result = validate('PAF-070213-GI1'); + + expect(result.isValid && result.compact).toEqual('PAF070213GI1'); }); it('validate:SOTO800101110', () => { diff --git a/src/mx/rfc.ts b/src/mx/rfc.ts index 2961ef5d..b4adee93 100644 --- a/src/mx/rfc.ts +++ b/src/mx/rfc.ts @@ -29,7 +29,7 @@ */ import * as exceptions from '../exceptions'; -import { isValidDateCompactYYMMDD, strings, weightedSum } from '../util'; +import { isValidDateCompactYYMMDD, strings } from '../util'; import { Validator, ValidateReturn } from '../types'; function clean(input: string): ReturnType { @@ -80,13 +80,6 @@ const nameBlacklist = new Set([ 'RUIN', ]); -// Official alphabet per SAT (Anexo 20). Includes '&' and 'Ñ'. -const checkAlphabet = '0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ Ñ'; - -// 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', localName: 'Registro Federal de Contribuyentes', @@ -145,42 +138,6 @@ const impl: Validator = { return { isValid: false, error: new exceptions.InvalidLength() }; } - if (value.length >= 12) { - if (!/[1-9A-V][1-9A-Z][0-9A]$/.test(value)) { - return { isValid: false, error: new exceptions.InvalidComponent() }; - } - - const [front, check] = strings.splitAt(value, -1); - 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() }; - } - } - } - return { isValid: true, compact: value, @@ -191,4 +148,4 @@ const impl: Validator = { }; export const { name, localName, abbreviation, validate, format, compact } = - impl; \ No newline at end of file + impl;