diff --git a/src/__tests__/usevalidationsbr.spec.ts b/src/__tests__/usevalidationsbr.spec.ts index c16d551..d3057d4 100644 --- a/src/__tests__/usevalidationsbr.spec.ts +++ b/src/__tests__/usevalidationsbr.spec.ts @@ -13,14 +13,18 @@ describe('UseValidationBr Cep', () => { describe('UseValidationBr CNPJ', () => { it('should be able return true to valid CNPJ', () => { + expect.assertions(2); + expect(useValidationsBR('cnpj', '66.919.381/0001-15')).toBe(true); + expect(useValidationsBR('cnpj', '12.ABC.345/01DE-35')).toBe(true); }); it('should be able return false to invalid CNPJ', () => { - expect.assertions(2); + expect.assertions(3); expect(useValidationsBR('cnpj', '12.732.455/0001-25')).toBe(false); expect(useValidationsBR('cnpj', '66.919.381/0001-10')).toBe(false); + expect(useValidationsBR('cnpj', '12.ABC.345/01DE-34')).toBe(false); }); it('should be able return false to pass a empty string', () => { @@ -28,7 +32,10 @@ describe('UseValidationBr CNPJ', () => { }); it('should be able return false to pass a string over 14 characters', () => { + expect.assertions(2); + expect(useValidationsBR('cnpj', '66.919.381/00001-14')).toBe(false); + expect(useValidationsBR('cnpj', '12.ABC.345/001DE-35')).toBe(false); }); it('should be able return false to pass a only repeated numbers', () => { diff --git a/src/__tests__/validateCNPJ.spec.ts b/src/__tests__/validateCNPJ.spec.ts index a6ba706..1097ceb 100644 --- a/src/__tests__/validateCNPJ.spec.ts +++ b/src/__tests__/validateCNPJ.spec.ts @@ -4,6 +4,8 @@ import { validateCNPJ } from '../validations/validateCNPJ'; describe('validateCNPJ', () => { it('should return true for valid CNPJs', () => { expect(validateCNPJ('66.919.381/0001-15')).toBe(true); + expect(validateCNPJ('12.ABC.345/01DE-35')).toBe(true); + expect(validateCNPJ('12ABC34501DE35')).toBe(true); }); it('should return false for invalid CNPJs', () => { @@ -12,15 +14,22 @@ describe('validateCNPJ', () => { expect(validateCNPJ('12.732.455/0001-25')).toBe(false); expect(validateCNPJ('66.919.381/0001-10')).toBe(false); expect(validateCNPJ('123')).toBe(false); + expect(validateCNPJ('12.ABC.345/01DE-34')).toBe(false); + expect(validateCNPJ('VQ.A8K.XRA/0001-49')).toBe(false); + expect(validateCNPJ('12abc34501de35')).toBe(false); + expect(validateCNPJ('49YYDYRJ000168')).toBe(false); + expect(validateCNPJ('12ABC34501DEAB')).toBe(false); }); it('should return false for empty or null strings', () => { expect(validateCNPJ('')).toBe(false); }); - it('should return false for CNPJs with all digits the same', () => { + it('should return false for CNPJs with all characters the same', () => { expect(validateCNPJ('11.111.111/1111-11')).toBe(false); expect(validateCNPJ('22222222222222')).toBe(false); + expect(validateCNPJ('BB.BBB.BBB/BBBB-BB')).toBe(false); + expect(validateCNPJ('AAAAAAAAAAAAAA')).toBe(false); }); it('should return false for CNPJs with incorrect length', () => { @@ -28,5 +37,9 @@ describe('validateCNPJ', () => { expect(validateCNPJ('66.919.381/0001-155')).toBe(false); expect(validateCNPJ('6691938100011')).toBe(false); expect(validateCNPJ('669193810001155')).toBe(false); + expect(validateCNPJ('12.ABC.345/01DE-3')).toBe(false); + expect(validateCNPJ('12.ABC.345/01DE-355')).toBe(false); + expect(validateCNPJ('12ABC34501DE3')).toBe(false); + expect(validateCNPJ('12ABC34501DE355')).toBe(false); }); }); diff --git a/src/validations/validateCNPJ.ts b/src/validations/validateCNPJ.ts index ac23ee8..a16a160 100644 --- a/src/validations/validateCNPJ.ts +++ b/src/validations/validateCNPJ.ts @@ -1,40 +1,55 @@ import { isRepeated } from './utils'; /** - * The function `validateCNPJ` validates a Brazilian CNPJ number. + * The function `charToNumber` converts a CNPJ character into the number used by the + * check digit calculation. Following the Receita Federal spec, the ASCII value of the + * character is offset by 48, so digits map to themselves ('0' -> 0) and uppercase + * letters to their code ('A' -> 17). + * @param {string} char - A single character from the CNPJ base. + * @returns The numeric value used in the weighted checksum. + */ +function charToNumber(char: string): number { + return char.charCodeAt(0) - 48; +} + +/** + * The function `generateChecksum` calculates a checksum based on a given base and a weight + * array. Each character in the `base` is converted with `charToNumber` and multiplied by the + * corresponding weight at the same index. + * @param {string} base - The value for which the checksum is calculated. + * @param {number[]} weight - The weights applied to each character of the base. + * @returns The calculated MOD 11 check digit. + */ +function generateChecksum(base: string, weight: number[]): number { + const sum = base + .split('') + .reduce((acc, char, i) => acc + charToNumber(char) * weight[i], 0); + const remainder = sum % 11; + + return remainder < 2 ? 0 : 11 - remainder; +} + +const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; +const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + +/** + * The function `validateCNPJ` validates a Brazilian CNPJ number, supporting both the + * traditional numeric format and the alphanumeric format (12 uppercase alphanumeric + * positions followed by 2 numeric check digits). * @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'. + * Nacional da Pessoa Jurídica) number to be validated. It can be formatted with or without + * punctuation, like '00.000.000/0000-00', '00000000000000' or '12.ABC.345/01DE-35'. * @returns The function `validateCNPJ` returns a boolean value. It returns `true` if the CNPJ is * valid, and `false` otherwise. */ export function validateCNPJ(value: string): boolean { - const cnpj = String(value).replace(/[^\d]+/g, ''); - - if (cnpj === '' || cnpj.length !== 14 || isRepeated(cnpj)) { - return false; - } - - const calculateDigit = (numbers: string, weights: number[]): number => { - let sum = 0; - for (let i = 0; i < numbers.length; i++) { - sum += parseInt(numbers.charAt(i), 10) * weights[i]; - } - const remainder = sum % 11; - return remainder < 2 ? 0 : 11 - remainder; - }; - - 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); - - if (dv1 !== parseInt(cnpj.charAt(12), 10)) { - return false; - } - - 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); - - return dv2 === parseInt(cnpj.charAt(13), 10); + const cnpj = String(value).replace(/[./-]/g, ''); + + if (!/^[A-Z\d]{12}\d{2}$/.test(cnpj) || isRepeated(cnpj)) return false; + + const base = cnpj.substring(0, 12); + const dv1 = generateChecksum(base, weights1); + const dv2 = generateChecksum(base + dv1, weights2); + + return `${dv1}${dv2}` === cnpj.substring(12); }