From f0e68af8423e317511919706d2ae07b67506de8c Mon Sep 17 00:00:00 2001 From: Wender Rodrigo de Oliveira Alves Date: Tue, 24 Mar 2026 09:40:58 -0300 Subject: [PATCH] feat: add support for alphanumeric CNPJ --- src/validations/utils.ts | 3 +- src/validations/validateCNPJ.ts | 65 ++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/validations/utils.ts b/src/validations/utils.ts index e6e89fd..abb9d80 100644 --- a/src/validations/utils.ts +++ b/src/validations/utils.ts @@ -5,8 +5,7 @@ * @returns a boolean value. */ export const isRepeated = (ref: string) => { - const ret = ref.replace(new RegExp(ref[0], 'g'), '').trim().length === 0; - return ret; + return /^([A-Z0-9])\1+$/.test(ref); }; /** diff --git a/src/validations/validateCNPJ.ts b/src/validations/validateCNPJ.ts index ac23ee8..b0031bd 100644 --- a/src/validations/validateCNPJ.ts +++ b/src/validations/validateCNPJ.ts @@ -1,40 +1,71 @@ import { isRepeated } from './utils'; /** - * The function `validateCNPJ` validates a Brazilian CNPJ number. - * @param {string} value - The `value` parameter is a string that represents the CNPJ (Cadastro - * Nacional da Pessoa Jurídica) number to be validated. It can be formatted with or without punctuation, - * like '00.000.000/0000-00' or '00000000000000'. - * @returns The function `validateCNPJ` returns a boolean value. It returns `true` if the CNPJ is - * valid, and `false` otherwise. + * The function `validateCNPJ` validates a Brazilian CNPJ number, supporting both + * the traditional numeric format and the new alphanumeric format introduced by Receita Federal. + * + * @param {string} value - The `value` parameter is a string representing the CNPJ number to be validated. + * It can contain formatting characters (such as '.', '/', '-') and may include letters (A-Z) in the first + * 12 positions, as allowed in the new alphanumeric CNPJ format. + * + * Examples of valid inputs: + * - '00.000.000/0000-00' (numeric format) + * - '00000000000000' (numeric without formatting) + * - 'A1.BC2.34D/56EF-90' (alphanumeric format) + * - 'A1BC234D56EF90' (alphanumeric without formatting) + * + * @returns {boolean} Returns `true` if the CNPJ is valid according to the official + * validation algorithm (modulo 11), or `false` otherwise. + * + * @description + * The validation process includes: + * - Normalizing the input by removing all non-alphanumeric characters and converting to uppercase + * - Verifying that the length is exactly 14 characters + * - Rejecting sequences with all identical characters + * - Converting alphanumeric characters to numeric values (A=10, B=11, ..., Z=35) + * - Calculating the two verification digits using the modulo 11 algorithm + * + * This function is fully compatible with both legacy numeric CNPJs and the new alphanumeric standard. */ export function validateCNPJ(value: string): boolean { - const cnpj = String(value).replace(/[^\d]+/g, ''); + const clean = (value || "") + .toUpperCase() + .replace(/[^A-Z0-9]/g, ""); - if (cnpj === '' || cnpj.length !== 14 || isRepeated(cnpj)) { + if (clean.length !== 14 || isRepeated(clean)) { return false; } - const calculateDigit = (numbers: string, weights: number[]): number => { + // Converte caractere para número (0-9 ou A=10 ... Z=35) + const charToNumber = (char: string): number => { + if (/[0-9]/.test(char)) return parseInt(char, 10); + return char.charCodeAt(0) - 55; // A=10, B=11... + }; + + const calculateDigit = (base: string, weights: number[]): number => { let sum = 0; - for (let i = 0; i < numbers.length; i++) { - sum += parseInt(numbers.charAt(i), 10) * weights[i]; + + for (let i = 0; i < base.length; i++) { + sum += charToNumber(base[i]) * weights[i]; } + const remainder = sum % 11; return remainder < 2 ? 0 : 11 - remainder; }; + const base12 = clean.substring(0, 12); + const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; - const first12 = cnpj.substring(0, 12); - const dv1 = calculateDigit(first12, weights1); + const dv1 = calculateDigit(base12, weights1); - if (dv1 !== parseInt(cnpj.charAt(12), 10)) { + if (dv1 !== charToNumber(clean[12])) { return false; } + const base13 = clean.substring(0, 13); + const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; - const first13 = cnpj.substring(0, 13); - const dv2 = calculateDigit(first13, weights2); + const dv2 = calculateDigit(base13, weights2); - return dv2 === parseInt(cnpj.charAt(13), 10); + return dv2 === charToNumber(clean[13]); }