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
6 changes: 6 additions & 0 deletions src/mx/rfc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Choose a reason for hiding this comment

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

medium

This combined assertion result.isValid && result.compact can lead to confusing test failure messages. If result.isValid is false, the expression evaluates to false, and the test fails with expected false to equal 'SOTO800101110', which doesn't clearly indicate the root cause. Asserting on an object containing the properties you want to check provides much clearer output on failure, making tests easier to debug.

Suggested change
expect(result.isValid && result.compact).toEqual('SOTO800101110');
expect({ isValid: result.isValid, compact: result.compact }).toEqual({ isValid: true, compact: 'SOTO800101110' });

});

it('format:GODE561231GR8', () => {
const result = format('GODE561231GR8');

Expand Down
65 changes: 33 additions & 32 deletions src/mx/rfc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ const nameBlacklist = new Set([
'RUIN',
]);

// Official alphabet per SAT (Anexo 20). Includes '&' and 'Ñ'.
Copy link
Owner

Choose a reason for hiding this comment

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

After thinking about this, unless there is a really good reason there should be two distinct validators for efc and rfcLegacy or similar names.

Copy link
Author

@abarone-btf abarone-btf Dec 10, 2025

Choose a reason for hiding this comment

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

Thanks for the reply! @koblas . But making all the consumers knows which one to call?, isnt that more invasive? (because they are "just" rfc). Like:

1 - Consumer would need to apply a logic to know if the rfc is "legacy" or not, then call the correct function

or

2 - Consumer would call rfc, and if it fails then call rfcLegacy to check again (not as ugly imo, but more logic to the consumer)

Open to the conversation, whatever you preferred I can make the change to fit that 🙌

const checkAlphabet = '0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ Ñ';
// const checkAlphabetDict: Record<string, number> = 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',
Expand Down Expand Up @@ -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);
Comment on lines +156 to +176

Choose a reason for hiding this comment

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

medium

The calculateChecksum function is a great improvement for reducing code duplication. To make it a pure function, which improves readability and prepares it for potential extraction into a standalone testable utility, consider passing paddedInput as an argument instead of capturing it from the outer scope.

      const calculateChecksum = (input: string, alphabet: string) => {
        const sum = weightedSum(input, {
          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(paddedInput, checkAlphabet);

      if (check !== valOfficial) {
        // If it fails, try with Legacy alphabet (Base 36)
        // This handles older RFCs generated without '&' or 'Ñ' support
        const valLegacy = calculateChecksum(paddedInput, checkAlphabetLegacy);


if (check !== valLegacy) {
return { isValid: false, error: new exceptions.InvalidChecksum() };
}
}
}

Expand All @@ -190,4 +191,4 @@ const impl: Validator = {
};

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