Skip to content
Merged
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: 5 additions & 1 deletion src/__tests__/usevalidationsbr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
51 changes: 47 additions & 4 deletions src/__tests__/validateCNPJ.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,65 @@ import { describe, expect, it } from 'vitest';
import { validateCNPJ } from '../validations/validateCNPJ';

describe('validateCNPJ', () => {
it('should return true for valid CNPJs', () => {
it('should return true for valid numeric CNPJs', () => {
expect(validateCNPJ('66.919.381/0001-15')).toBe(true);
expect(validateCNPJ('04.740.714/0001-97')).toBe(true);
expect(validateCNPJ('44.108.058/0001-29')).toBe(true);
expect(validateCNPJ('00000000000191')).toBe(true);
});

it('should return false for invalid CNPJs', () => {
it('should return true for valid alphanumeric CNPJs', () => {
// Official examples from the Receita Federal reference implementation.
expect(validateCNPJ('12.ABC.345/01DE-35')).toBe(true);
expect(validateCNPJ('12ABC34501DE35')).toBe(true);
expect(validateCNPJ('90.021.382/0001-22')).toBe(true);
expect(validateCNPJ('90.024.778/0001-23')).toBe(true);
expect(validateCNPJ('90.025.108/0001-21')).toBe(true);
expect(validateCNPJ('90.025.255/0001-00')).toBe(true);
expect(validateCNPJ('90.024.420/0001-09')).toBe(true);
expect(validateCNPJ('90.024.781/0001-47')).toBe(true);
expect(validateCNPJ('90.024.780/0001-00')).toBe(true);
expect(validateCNPJ('90.024.779/0001-78')).toBe(true);
expect(validateCNPJ('ABCDEFGHIJKL80')).toBe(true);
});

it('should return false for invalid check digits', () => {
expect(validateCNPJ('48.514.588/0001-70')).toBe(false);
expect(validateCNPJ('48514588000170')).toBe(false);
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('00000000000192')).toBe(false);
expect(validateCNPJ('ABCDEFGHIJKL81')).toBe(false);
});

it('should return false for lowercase letters in the base', () => {
expect(validateCNPJ('12.ABc.345/01DE-35')).toBe(false);
expect(validateCNPJ('12abc34501de35')).toBe(false);
});

it('should return false for non-numeric check digits', () => {
expect(validateCNPJ('0000000000019L')).toBe(false);
expect(validateCNPJ('000000000001P1')).toBe(false);
});

it('should return false for disallowed characters', () => {
expect(validateCNPJ("'!@#$%&*-_=+^~")).toBe(false);
expect(validateCNPJ('$0123456789ABC')).toBe(false);
expect(validateCNPJ('0123456?789ABC')).toBe(false);
expect(validateCNPJ('0123456789ABC#')).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 the zeroed CNPJ', () => {
expect(validateCNPJ('00000000000000')).toBe(false);
expect(validateCNPJ('00.000.000/0000-00')).toBe(false);
});

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);
});
Expand All @@ -28,5 +70,6 @@ describe('validateCNPJ', () => {
expect(validateCNPJ('66.919.381/0001-155')).toBe(false);
expect(validateCNPJ('6691938100011')).toBe(false);
expect(validateCNPJ('669193810001155')).toBe(false);
expect(validateCNPJ('123')).toBe(false);
});
});
76 changes: 49 additions & 27 deletions src/validations/validateCNPJ.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,62 @@
import { isRepeated } from './utils';

// First 12 positions accept the alphanumeric base (uppercase letters or digits);
// the 2 check digits are always numeric. See the Receita Federal spec:
// https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/documentos-tecnicos/cnpj
const cnpjRegex = /^[A-Z\d]{12}\d{2}$/;
const maskRegex = /[./-]/g;
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.
* @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 `charToNumber` converts a CNPJ base character into the value used by the
* check-digit calculation. Following the Receita Federal spec, the character's ASCII code
* is offset by 48, so digits map to themselves ('0' -> 0) and uppercase letters to their
* code ('A' -> 17, 'B' -> 18, ... 'Z' -> 42).
* @param {string} char - A single character from the CNPJ base.
* @returns The numeric value used in the weighted checksum.
*/
export function validateCNPJ(value: string): boolean {
const cnpj = String(value).replace(/[^\d]+/g, '');
function charToNumber(char: string): number {
return char.charCodeAt(0) - 48;
}

if (cnpj === '' || cnpj.length !== 14 || isRepeated(cnpj)) {
return false;
}
/**
* The function `generateChecksum` calculates a MOD 11 check digit for a given base and a
* weight array. Each character of the `base` is converted with `charToNumber` and multiplied
* by the corresponding weight at the same index.
* @param {string} base - The value for which the check digit is calculated.
* @param {number[]} weights - The weights applied to each character of the base.
* @returns The calculated check digit (0-9).
*/
function generateChecksum(base: string, weights: number[]): number {
const sum = base
.split('')
.reduce((acc, char, i) => acc + charToNumber(char) * weights[i], 0);
const remainder = sum % 11;

const calculateDigit = (numbers: string, weights: number[]): number => {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += Number.parseInt(numbers.charAt(i), 10) * weights[i];
}
const remainder = sum % 11;
return remainder < 2 ? 0 : 11 - remainder;
};
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);
/**
* 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) introduced by Receita Federal.
* @param {string} value - The `value` parameter is a string that represents the CNPJ
* (Cadastro Nacional da Pessoa Jurídica) 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(maskRegex, '');

if (dv1 !== Number.parseInt(cnpj.charAt(12), 10)) {
if (!cnpjRegex.test(cnpj) || isRepeated(cnpj)) {
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);
const base = cnpj.substring(0, 12);
const dv1 = generateChecksum(base, weights1);
const dv2 = generateChecksum(base + dv1, weights2);

return dv2 === Number.parseInt(cnpj.charAt(13), 10);
return `${dv1}${dv2}` === cnpj.substring(12);
}