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
22 changes: 17 additions & 5 deletions src/mx/rfc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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', () => {
Expand Down
47 changes: 2 additions & 45 deletions src/mx/rfc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof strings.cleanUnicode> {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand All @@ -191,4 +148,4 @@ const impl: Validator = {
};

export const { name, localName, abbreviation, validate, format, compact } =
impl;
impl;