diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index b375f313..35ca530e 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -41,6 +41,14 @@ Valida se o CNPJ é válido. import { isValidCNPJ } from '@brazilian-utils/brazilian-utils'; isValidCNPJ('15515147234255'); // false + +/* + * valida um CNPJ versão 2, alfanumérico. + * Conforme a Nota Técnica COCAD/SUARA/RFB nº 49/2024, + * o CNPJ passará a combinar números e letras. + * A versão 2 segue essa norma técnica. +*/ +isValidCNPJ('1D4N1A9K2ZQQ30', { version: '2' }); // false ``` ## formatCNPJ @@ -52,9 +60,17 @@ import { formatCNPJ } from '@brazilian-utils/brazilian-utils'; formatCNPJ('245222000174'); // 24.522.200/0174 formatCNPJ('245222000174', { pad: true }); // 00.245.222/0001-74 + +/* + * Format CNPJ versão 2, alfanumérico. + * Conforme a Nota Técnica COCAD/SUARA/RFB nº 49/2024, + * o CNPJ passará a combinar números e letras. + * A versão 2 dessa função segue essa norma técnica. +*/ +formatCNPJ('PD4N1A9K1ZQQ39', { version: '2' }); // PD.4N1.A9K.1ZQQ/39 ``` -## generateCPF +## generateCNPJ Gera um CNPJ válido aleatório. @@ -62,6 +78,14 @@ Gera um CNPJ válido aleatório. import { generateCNPJ } from '@brazilian-utils/brazilian-utils' generateCNPJ(); + +/* + * Gera um CNPJ válido versão 2, alfanumérico. + * Conforme a Nota Técnica COCAD/SUARA/RFB nº 49/2024, + * o CNPJ passará a combinar números e letras. + * A versão 2 segue essa norma técnica. +*/ +generateCNPJ({ version: '2' }); ``` ## isValidCEP diff --git a/docs/utilities.md b/docs/utilities.md index 09a40031..e981432f 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -41,6 +41,14 @@ Check if CNPJ is valid. import { isValidCNPJ } from '@brazilian-utils/brazilian-utils'; isValidCNPJ('15515147234255'); // false + +/* + * validate CNPJ version 2, alphanumeric. + * According to Technical Note COCAD/SUARA/RFB No. 49/2024, + * the CNPJ will combine numbers and letters. + * Version 2 follows this technical. +*/ +isValidCNPJ('1D4N1A9K2ZQQ30', { version: '2' }); // false ``` ## formatCNPJ @@ -52,6 +60,14 @@ import { formatCNPJ } from '@brazilian-utils/brazilian-utils'; formatCNPJ('245222000174'); // 24.522.200/0174 formatCNPJ('245222000174', { pad: true }); // 00.245.222/0001-74 + +/* + * Format CNPJ version 2, alphanumeric. + * According to Technical Note COCAD/SUARA/RFB No. 49/2024, + * the CNPJ will combine numbers and letters. + * Version 2 follows this technical. +*/ +formatCNPJ('PD4N1A9K1ZQQ39', { version: '2' }); // PD.4N1.A9K.1ZQQ/39 ``` ## isValidCEP @@ -72,6 +88,14 @@ Generate a valid random CNPJ. import { generateCNPJ } from '@brazilian-utils/brazilian-utils' generateCNPJ(); + +/* + * Generates a valid random CNPJ version 2, alphanumeric. + * According to Technical Note COCAD/SUARA/RFB No. 49/2024, + * the CNPJ will combine numbers and letters. + * Version 2 follows this technical standard. +*/ +generateCNPJ({ version: '2' }); ``` ## isValidBoleto diff --git a/src/utilities/cnpj/index.test.ts b/src/utilities/cnpj/index.test.ts index d63d2d40..402dfde6 100644 --- a/src/utilities/cnpj/index.test.ts +++ b/src/utilities/cnpj/index.test.ts @@ -1,4 +1,8 @@ -import { format, LENGTH, isValid, generate, RESERVED_NUMBERS } from '.'; +import { format, LENGTH, isValid, generate, RESERVED_NUMBERS, CnpjVersions } from '.'; + +function randomCnpjOptionVersion2StringOrNumber(): '2' | 2 { + return Math.random() < 0.5 ? '2' : 2; +} describe('format', () => { test('should format cnpj with mask', () => { @@ -19,6 +23,24 @@ describe('format', () => { expect(format('46843485000186')).toBe('46.843.485/0001-86'); }); + test('should format cnpj alphanumeric with mask for version 2', () => { + expect(format('', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe(''); + expect(format('Q', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q'); + expect(format('Q0', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0'); + expect(format('Q0S', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.S'); + expect(format('Q0SL', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SL'); + expect(format('Q0SLF', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF'); + expect(format('Q0SLFM', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF.M'); + expect(format('Q0SLFMB', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF.MB'); + expect(format('Q0SLFMBD', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF.MBD'); + expect(format('Q0SLFMBD7', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF.MBD/7'); + expect(format('Q0SLFMBD7V', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF.MBD/7V'); + expect(format('Q0SLFMBD7VX', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF.MBD/7VX'); + expect(format('Q0SLFMBD7VX4', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF.MBD/7VX4'); + expect(format('Q0SLFMBD7VX43', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF.MBD/7VX4-3'); + expect(format('q0SLFMBD7VX439', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe('Q0.SLF.MBD/7VX4-39'); + }); + test('should format number cnpj with mask', () => { expect(format(4)).toBe('4'); expect(format(46)).toBe('46'); @@ -78,6 +100,12 @@ describe('format', () => { test('should remove all non numeric characters', () => { expect(format('46.?ABC843.485/0001-86abc')).toBe('46.843.485/0001-86'); }); + + test('should remove non-alphanumeric characters for version 2', () => { + expect(format('46.?ABC843.485/0001-86abc', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe( + '46.ABC.843/4850-00' + ); + }); }); describe('generate', () => { @@ -91,6 +119,14 @@ describe('generate', () => { expect(isValid(generate())).toBe(true); } }); + + test('should return valid CNPJ version 2', () => { + // iterate over 100 to insure that random generated CPNJ is valid + for (let i = 0; i < 100; i++) { + const options: { version?: CnpjVersions } = { version: randomCnpjOptionVersion2StringOrNumber() }; + expect(isValid(generate(options), options)).toBe(true); + } + }); }); describe('isValid', () => { diff --git a/src/utilities/cnpj/index.ts b/src/utilities/cnpj/index.ts index 6fead908..69e02eda 100644 --- a/src/utilities/cnpj/index.ts +++ b/src/utilities/cnpj/index.ts @@ -1,4 +1,4 @@ -import { isLastChar, onlyNumbers, generateChecksum, generateRandomNumber } from '../../helpers'; +import { isLastChar, onlyNumbers, generateRandomNumber } from '../../helpers'; export const LENGTH = 14; @@ -27,12 +27,16 @@ 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]; +const VALID_CHARS = '0123456789ABCDFGHIJKLMNPQRSVWXYZ'; + +export type CnpjVersions = '1' | '2' | 1 | 2; export interface FormatCnpjOptions { pad?: boolean; + version?: CnpjVersions; } export function format(cnpj: string | number, options: FormatCnpjOptions = {}): string { - let digits = onlyNumbers(cnpj); + let digits = options.version == 2 ? onlyValidCNPJAlphanumeric(String(cnpj).toUpperCase()) : onlyNumbers(cnpj); if (options.pad) { digits = digits.padStart(LENGTH, '0'); @@ -54,8 +58,36 @@ export function format(cnpj: string | number, options: FormatCnpjOptions = {}): }, ''); } -export function generate(): string { - const baseCNPJ = generateRandomNumber(LENGTH - 2); +function onlyValidCNPJAlphanumeric(input: string): string { + return input + .split('') + .filter((char) => VALID_CHARS.includes(char)) + .join(''); +} + +function generateChecksum(base: string, weight: number[]): number { + const digits = onlyValidCNPJAlphanumeric(base); + + return digits.split('').reduce((acc, char, i) => { + const value = char.charCodeAt(0) - 48; + return acc + value * weight[i]; + }, 0); +} + +function generateCNPJAlphanumericChars(length: number): string { + const charset = VALID_CHARS; + return Array(length) + .fill('') + .map(() => charset[Math.floor(Math.random() * charset.length)]) + .join(''); +} + +export interface GenerateCnpjOptions { + version?: CnpjVersions; +} + +export function generate(options: GenerateCnpjOptions = {}): string { + const baseCNPJ = options.version == 2 ? generateCNPJAlphanumericChars(LENGTH - 2) : generateRandomNumber(LENGTH - 2); const firstCheckDigitMod = generateChecksum(baseCNPJ, FIRST_CHECK_DIGIT_WEIGHTS) % 11; const firstCheckDigit = (firstCheckDigitMod < 2 ? 0 : 11 - firstCheckDigitMod).toString(); @@ -66,8 +98,10 @@ export function generate(): string { return `${baseCNPJ}${firstCheckDigit}${secondCheckDigit}`; } -export function isValidFormat(cnpj: string): boolean { - return /^\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2}$/.test(cnpj); +function isValidFormat(cnpj: string): boolean { + return /^[0-9ABCDFGHIJKLMNPQRSVWXYZ]{2}\.?[0-9ABCDFGHIJKLMNPQRSVWXYZ]{3}\.?[0-9ABCDFGHIJKLMNPQRSVWXYZ]{3}\/?[0-9ABCDFGHIJKLMNPQRSVWXYZ]{4}-?\d{2}$/.test( + cnpj + ); } export function isReservedNumber(cpf: string): boolean { @@ -96,10 +130,14 @@ export function isValidChecksum(cnpj: string): boolean { }); } -export function isValid(cnpj: string): boolean { +export interface isValidCnpjOptions { + version?: CnpjVersions; +} + +export function isValid(cnpj: string, options: isValidCnpjOptions = {}): boolean { if (!cnpj || typeof cnpj !== 'string') return false; - const numbers = onlyNumbers(cnpj); + const validValue = options.version == 2 ? onlyValidCNPJAlphanumeric(cnpj) : onlyNumbers(cnpj); - return isValidFormat(cnpj) && !isReservedNumber(numbers) && isValidChecksum(numbers); + return isValidFormat(cnpj) && !isReservedNumber(validValue) && isValidChecksum(validValue); }