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
158 changes: 156 additions & 2 deletions src/utilities/cnpj/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { format, LENGTH, isValid, generate, RESERVED_NUMBERS } from '.';
import {
format,
LENGTH,
isValid,
generate,
generateAlphanumeric,
isAlphanumericCnpj,
isNumericCnpj,
cleanCnpj,
charToCnpjValue,
isValidFormat,
isValidNumericFormat,
RESERVED_NUMBERS,
} from '.';

describe('format', () => {
test('should format cnpj with mask', () => {
Expand Down Expand Up @@ -76,7 +89,19 @@ describe('format', () => {
});

test('should remove all non numeric characters', () => {
expect(format('46.?ABC843.485/0001-86abc')).toBe('46.843.485/0001-86');
expect(format('46.?ABC843.485/0001-86abc')).toBe('46.ABC.843/4850-00');
});

// Novos testes para CNPJ alfanumérico
test('should format alphanumeric cnpj with mask', () => {
expect(format('AB1C2D3E4F5G6')).toBe('AB.1C2.D3E/4F5G-6');
expect(format('12ABC34501DE35')).toBe('12.ABC.345/01DE-35');
expect(format('ABCDEFGHIJKL35')).toBe('AB.CDE.FGH/IJKL-35');
});

test('should format alphanumeric cnpj with special characters', () => {
expect(format('AB.?1C2.D3E/4F5G-35abc')).toBe('AB.1C2.D3E/4F5G-35');
expect(format('12.ABC.345/01DE-35')).toBe('12.ABC.345/01DE-35');
});
});

Expand All @@ -93,6 +118,113 @@ describe('generate', () => {
});
});

describe('generateAlphanumeric', () => {
test(`should have the right length without mask (${LENGTH})`, () => {
expect(generateAlphanumeric().length).toBe(LENGTH);
});

test('should return valid alphanumeric CNPJ', () => {
// iterate over 100 to insure that random generated alphanumeric CNPJ is valid
for (let i = 0; i < 100; i++) {
const cnpj = generateAlphanumeric();
expect(isValid(cnpj)).toBe(true);
expect(isAlphanumericCnpj(cnpj)).toBe(true);
}
});

test('should contain alphanumeric characters', () => {
const cnpj = generateAlphanumeric();
expect(/[A-Z]/.test(cnpj)).toBe(true);
expect(/[0-9]/.test(cnpj)).toBe(true);
});
});

describe('charToCnpjValue', () => {
test('should convert characters to numeric values (ASCII - 48)', () => {
expect(charToCnpjValue('A')).toBe(17); // 65 - 48
expect(charToCnpjValue('B')).toBe(18); // 66 - 48
expect(charToCnpjValue('C')).toBe(19); // 67 - 48
expect(charToCnpjValue('0')).toBe(0); // 48 - 48
expect(charToCnpjValue('1')).toBe(1); // 49 - 48
expect(charToCnpjValue('9')).toBe(9); // 57 - 48
expect(charToCnpjValue('Z')).toBe(42); // 90 - 48
});
});

describe('cleanCnpj', () => {
test('should remove special characters and convert to uppercase', () => {
expect(cleanCnpj('12.ABC.345/01DE-35')).toBe('12ABC34501DE35');
expect(cleanCnpj('12.345.678/0001-95')).toBe('12345678000195');
expect(cleanCnpj('ab.cde.fgh/ijkl-35')).toBe('ABCDEFGHIJKL35');
expect(cleanCnpj('12.?ABC.345/01DE-35abc')).toBe('12ABC34501DE35ABC');
});
});

describe('isNumericCnpj', () => {
test('should return true for numeric CNPJs', () => {
expect(isNumericCnpj('12345678000195')).toBe(true);
expect(isNumericCnpj('12.345.678/0001-95')).toBe(true);
expect(isNumericCnpj('00000000000000')).toBe(true);
});

test('should return false for alphanumeric CNPJs', () => {
expect(isNumericCnpj('12ABC34501DE35')).toBe(false);
expect(isNumericCnpj('AB.1C2.D3E/4F5G-35')).toBe(false);
expect(isNumericCnpj('ABCDEFGHIJKL35')).toBe(false);
});
});

describe('isAlphanumericCnpj', () => {
test('should return true for alphanumeric CNPJs', () => {
expect(isAlphanumericCnpj('12ABC34501DE35')).toBe(true);
expect(isAlphanumericCnpj('AB.1C2.D3E/4F5G-35')).toBe(true);
expect(isAlphanumericCnpj('ABCDEFGHIJKL35')).toBe(true);
});

test('should return false for numeric CNPJs', () => {
expect(isAlphanumericCnpj('12345678000195')).toBe(false);
expect(isAlphanumericCnpj('12.345.678/0001-95')).toBe(false);
expect(isAlphanumericCnpj('00000000000000')).toBe(false);
});

test('should return false for invalid lengths', () => {
expect(isAlphanumericCnpj('ABC')).toBe(false);
expect(isAlphanumericCnpj('ABCDEFGHIJKLMNOP')).toBe(false);
});
});

describe('isValidFormat', () => {
test('should return true for valid alphanumeric formats', () => {
expect(isValidFormat('12.ABC.345/01DE-35')).toBe(true);
expect(isValidFormat('AB.1C2.D3E/4F5G-35')).toBe(true);
expect(isValidFormat('12ABC34501DE35')).toBe(true);
expect(isValidFormat('AB1C2D3E4F5G35')).toBe(true);
});

test('should return true for valid numeric formats', () => {
expect(isValidFormat('12.345.678/0001-95')).toBe(true);
expect(isValidFormat('12345678000195')).toBe(true);
});

test('should return false for invalid formats', () => {
expect(isValidFormat('12.ABC.345/01DE-99')).toBe(true); // Actually valid format, just invalid DV
expect(isValidFormat('AB.1C2.D3E/4F5G-3')).toBe(false); // Too short
expect(isValidFormat('AB.1C2.D3E/4F5G-356')).toBe(false); // Too long
});
});

describe('isValidNumericFormat', () => {
test('should return true for valid numeric formats', () => {
expect(isValidNumericFormat('12.345.678/0001-95')).toBe(true);
expect(isValidNumericFormat('12345678000195')).toBe(true);
});

test('should return false for alphanumeric formats', () => {
expect(isValidNumericFormat('12.ABC.345/01DE-35')).toBe(false);
expect(isValidNumericFormat('AB.1C2.D3E/4F5G-35')).toBe(false);
});
});

describe('isValid', () => {
describe('should return false', () => {
test('when it is on the RESERVED_NUMBERS', () => {
Expand Down Expand Up @@ -139,6 +271,13 @@ describe('isValid', () => {
test('when is a CNPJ invalid', () => {
expect(isValid('11257245286531')).toBe(false);
});

// Novos testes para CNPJ alfanumérico inválido
test('when is an invalid alphanumeric CNPJ', () => {
expect(isValid('12.ABC.345/01DE-99')).toBe(false); // Invalid DV
expect(isValid('AB.1C2.D3E/4F5G-3')).toBe(false); // Too short
expect(isValid('AB.1C2.D3E/4F5G-356')).toBe(false); // Too long
});
});

describe('should return true', () => {
Expand All @@ -149,5 +288,20 @@ describe('isValid', () => {
test('when is a CNPJ valid with mask', () => {
expect(isValid('60.391.947/0001-00')).toBe(true);
});

// Novos testes para CNPJ alfanumérico válido
test('when is a valid alphanumeric CNPJ', () => {
// Estes testes precisam de CNPJs alfanuméricos válidos gerados pela função
const alphanumericCnpj = generateAlphanumeric();
expect(isValid(alphanumericCnpj)).toBe(true);
expect(isAlphanumericCnpj(alphanumericCnpj)).toBe(true);
});

test('when is a valid alphanumeric CNPJ with mask', () => {
const alphanumericCnpj = generateAlphanumeric();
const formattedCnpj = format(alphanumericCnpj);
expect(isValid(formattedCnpj)).toBe(true);
expect(isAlphanumericCnpj(formattedCnpj)).toBe(true);
});
});
});
100 changes: 93 additions & 7 deletions src/utilities/cnpj/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isLastChar, onlyNumbers, generateChecksum, generateRandomNumber } from '../../helpers';
import { isLastChar, generateChecksum, generateRandomNumber } from '../../helpers';

export const LENGTH = 14;

Expand Down Expand Up @@ -27,12 +27,35 @@ export const FIRST_CHECK_DIGIT_WEIGHTS = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];

export const SECOND_CHECK_DIGIT_WEIGHTS = [6, ...FIRST_CHECK_DIGIT_WEIGHTS];

export const VALID_CNPJ_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';

export const CNPJ_FORMAT_REGEX = /^[0-9A-Z]{2}\.?[0-9A-Z]{3}\.?[0-9A-Z]{3}\/?[0-9A-Z]{4}-?[0-9]{2}$/;

export const NUMERIC_CNPJ_REGEX = /^\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2}$/;

export interface FormatCnpjOptions {
pad?: boolean;
}

export function charToCnpjValue(char: string): number {
return char.charCodeAt(0) - 48;
}

export function cleanCnpj(cnpj: string): string {
return cnpj.replace(/[^0-9A-Za-z]/g, '').toUpperCase();
}

export function isNumericCnpj(cnpj: string): boolean {
return /^\d+$/.test(cleanCnpj(cnpj));
}

export function isAlphanumericCnpj(cnpj: string): boolean {
const cleaned = cleanCnpj(cnpj);
return cleaned.length === LENGTH && /[A-Z]/.test(cleaned);
}

export function format(cnpj: string | number, options: FormatCnpjOptions = {}): string {
let digits = onlyNumbers(cnpj);
let digits = cleanCnpj(cnpj.toString());

if (options.pad) {
digits = digits.padStart(LENGTH, '0');
Expand All @@ -54,6 +77,22 @@ export function format(cnpj: string | number, options: FormatCnpjOptions = {}):
}, '');
}

export function generateRandomCnpjChar(): string {
return VALID_CNPJ_CHARS[Math.floor(Math.random() * VALID_CNPJ_CHARS.length)];
}

export function generateAlphanumericCnpjBase(): string {
let base = '';
for (let i = 0; i < 12; i++) {
base += generateRandomCnpjChar();
}
return base;
}

export function generateAlphanumericChecksum(cnpj: string, weights: number[]): number {
return cnpj.split('').reduce((sum, char, idx) => sum + charToCnpjValue(char) * weights[idx], 0);
}

export function generate(): string {
const baseCNPJ = generateRandomNumber(LENGTH - 2);

Expand All @@ -66,12 +105,51 @@ export function generate(): string {
return `${baseCNPJ}${firstCheckDigit}${secondCheckDigit}`;
}

export function generateAlphanumeric(): string {
const baseCNPJ = generateAlphanumericCnpjBase();

const firstCheckDigitMod = generateAlphanumericChecksum(baseCNPJ, FIRST_CHECK_DIGIT_WEIGHTS) % 11;
const firstCheckDigit = (firstCheckDigitMod < 2 ? 0 : 11 - firstCheckDigitMod).toString();

const secondCheckDigitMod = generateAlphanumericChecksum(baseCNPJ + firstCheckDigit, SECOND_CHECK_DIGIT_WEIGHTS) % 11;
const secondCheckDigit = (secondCheckDigitMod < 2 ? 0 : 11 - secondCheckDigitMod).toString();

return `${baseCNPJ}${firstCheckDigit}${secondCheckDigit}`;
}

export function isValidFormat(cnpj: string): boolean {
return /^\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2}$/.test(cnpj);
return CNPJ_FORMAT_REGEX.test(cnpj);
}

export function isValidNumericFormat(cnpj: string): boolean {
return NUMERIC_CNPJ_REGEX.test(cnpj);
}

export function isReservedNumber(cpf: string): boolean {
return RESERVED_NUMBERS.indexOf(cpf) >= 0;
export function isReservedNumber(cnpj: string): boolean {
const cleaned = cleanCnpj(cnpj);
return RESERVED_NUMBERS.indexOf(cleaned) >= 0;
}

export function isValidAlphanumericChecksum(cnpj: string): boolean {
const cleaned = cleanCnpj(cnpj);
const weights = [...FIRST_CHECK_DIGIT_WEIGHTS];

return CHECK_DIGITS_INDEXES.every((i) => {
if (i === CHECK_DIGITS_INDEXES[CHECK_DIGITS_INDEXES.length - 1]) {
weights.unshift(6);
}

const mod =
generateAlphanumericChecksum(
cleaned
.slice(0, i)
.split('')
.reduce((acc, digit) => acc + digit, ''),
weights
) % 11;

return cleaned[i] === String(mod < 2 ? 0 : 11 - mod);
});
}

// TODO: move to checksum helper
Expand Down Expand Up @@ -99,7 +177,15 @@ export function isValidChecksum(cnpj: string): boolean {
export function isValid(cnpj: string): boolean {
if (!cnpj || typeof cnpj !== 'string') return false;

const numbers = onlyNumbers(cnpj);
const cleaned = cleanCnpj(cnpj);

if (isNumericCnpj(cleaned)) {
return isValidNumericFormat(cnpj) && !isReservedNumber(cleaned) && isValidChecksum(cleaned);
}

if (isAlphanumericCnpj(cleaned)) {
return isValidFormat(cnpj) && isValidAlphanumericChecksum(cleaned);
}

return isValidFormat(cnpj) && !isReservedNumber(numbers) && isValidChecksum(numbers);
return false;
}