From f004fac4b218e016a8f612551960db605fd92793 Mon Sep 17 00:00:00 2001 From: Amauri Dias Date: Sun, 30 Jun 2024 22:42:49 -0300 Subject: [PATCH 1/5] feat: new cnpj alphanumeric --- src/utilities/cnpj/index.test.ts | 21 +++++++++++- src/utilities/cnpj/index.ts | 55 +++++++++++++++++++++++++------- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/utilities/cnpj/index.test.ts b/src/utilities/cnpj/index.test.ts index d63d2d40..59c64966 100644 --- a/src/utilities/cnpj/index.test.ts +++ b/src/utilities/cnpj/index.test.ts @@ -19,6 +19,25 @@ describe('format', () => { expect(format('46843485000186')).toBe('46.843.485/0001-86'); }); + + test('should format cnpj alphanumeric with mask', () => { + expect(format('')).toBe(''); + expect(format('Q')).toBe('Q'); + expect(format('Q0')).toBe('Q0'); + expect(format('Q0S')).toBe('Q0.S'); + expect(format('Q0SL')).toBe('Q0.SL'); + expect(format('Q0SLF')).toBe('Q0.SLF'); + expect(format('Q0SLFM')).toBe('Q0.SLF.M'); + expect(format('Q0SLFMB')).toBe('Q0.SLF.MB'); + expect(format('Q0SLFMBD')).toBe('Q0.SLF.MBD'); + expect(format('Q0SLFMBD7')).toBe('Q0.SLF.MBD/7'); + expect(format('Q0SLFMBD7V')).toBe('Q0.SLF.MBD/7V'); + expect(format('Q0SLFMBD7VX')).toBe('Q0.SLF.MBD/7VX'); + expect(format('Q0SLFMBD7VX4')).toBe('Q0.SLF.MBD/7VX4'); + expect(format('Q0SLFMBD7VX43')).toBe('Q0.SLF.MBD/7VX4-3'); + expect(format('q0SLFMBD7VX439')).toBe('Q0.SLF.MBD/7VX4-39'); + }); + test('should format number cnpj with mask', () => { expect(format(4)).toBe('4'); expect(format(46)).toBe('46'); @@ -76,7 +95,7 @@ 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'); }); }); diff --git a/src/utilities/cnpj/index.ts b/src/utilities/cnpj/index.ts index 6fead908..533b8ed4 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 } from '../../helpers'; export const LENGTH = 14; @@ -27,12 +27,15 @@ 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_CNPJ = '0123456789ABCDFGHIJKLMNPQRSVWXYZ' + export interface FormatCnpjOptions { pad?: boolean; } export function format(cnpj: string | number, options: FormatCnpjOptions = {}): string { - let digits = onlyNumbers(cnpj); + const valueCNPJUppercased = String(cnpj).toUpperCase(); + let digits = onlyValidCNPJAlphanumeric(valueCNPJUppercased); if (options.pad) { digits = digits.padStart(LENGTH, '0'); @@ -54,20 +57,49 @@ export function format(cnpj: string | number, options: FormatCnpjOptions = {}): }, ''); } -export function generate(): string { - const baseCNPJ = generateRandomNumber(LENGTH - 2); +function charToAsciiValue(char: string): number { + return char.charCodeAt(0) - 48; +} + +function onlyValidCNPJAlphanumeric(input: string): string { + return input + .split('') + .filter(char => VALID_CHARS_CNPJ.includes(char)) + .join(''); +} + +function generateChecksumCNPJ(base: string, weight: number[]): number { + const digits = onlyValidCNPJAlphanumeric(base); - const firstCheckDigitMod = generateChecksum(baseCNPJ, FIRST_CHECK_DIGIT_WEIGHTS) % 11; + return digits.split('').reduce((acc, char, i) => { + const value = charToAsciiValue(char); + return acc + value * weight[i]; + }, 0); +} + +function generateCNPJAlphanumericChars(length: number): string { + const charset = VALID_CHARS_CNPJ; + return Array(length) + .fill('') + .map(() => charset[Math.floor(Math.random() * charset.length)]) + .join(''); +} + + +export function generate(): string { + const baseCNPJ = generateCNPJAlphanumericChars(LENGTH - 2); + + const firstCheckDigitMod = generateChecksumCNPJ(baseCNPJ, FIRST_CHECK_DIGIT_WEIGHTS) % 11; const firstCheckDigit = (firstCheckDigitMod < 2 ? 0 : 11 - firstCheckDigitMod).toString(); - const secondCheckDigitMod = generateChecksum(baseCNPJ + firstCheckDigit, SECOND_CHECK_DIGIT_WEIGHTS) % 11; + const secondCheckDigitMod = generateChecksumCNPJ(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); +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 { @@ -84,7 +116,7 @@ export function isValidChecksum(cnpj: string): boolean { } const mod = - generateChecksum( + generateChecksumCNPJ( cnpj .slice(0, i) .split('') @@ -99,7 +131,8 @@ export function isValidChecksum(cnpj: string): boolean { export function isValid(cnpj: string): boolean { if (!cnpj || typeof cnpj !== 'string') return false; - const numbers = onlyNumbers(cnpj); + const validValue = onlyValidCNPJAlphanumeric(cnpj); + - return isValidFormat(cnpj) && !isReservedNumber(numbers) && isValidChecksum(numbers); + return isValidFormat(cnpj) && !isReservedNumber(validValue) && isValidChecksum(validValue); } From 43ce4f4e7147b077acef9ec112a19ba3f3929107 Mon Sep 17 00:00:00 2001 From: Amauri Dias Date: Sun, 30 Jun 2024 23:03:31 -0300 Subject: [PATCH 2/5] chore: run lint prettier --- src/utilities/cnpj/index.test.ts | 1 - src/utilities/cnpj/index.ts | 12 ++++++------ src/utilities/email/index.ts | 3 ++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/utilities/cnpj/index.test.ts b/src/utilities/cnpj/index.test.ts index 59c64966..29aff727 100644 --- a/src/utilities/cnpj/index.test.ts +++ b/src/utilities/cnpj/index.test.ts @@ -19,7 +19,6 @@ describe('format', () => { expect(format('46843485000186')).toBe('46.843.485/0001-86'); }); - test('should format cnpj alphanumeric with mask', () => { expect(format('')).toBe(''); expect(format('Q')).toBe('Q'); diff --git a/src/utilities/cnpj/index.ts b/src/utilities/cnpj/index.ts index 533b8ed4..61de7dbb 100644 --- a/src/utilities/cnpj/index.ts +++ b/src/utilities/cnpj/index.ts @@ -27,7 +27,7 @@ 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_CNPJ = '0123456789ABCDFGHIJKLMNPQRSVWXYZ' +const VALID_CHARS_CNPJ = '0123456789ABCDFGHIJKLMNPQRSVWXYZ'; export interface FormatCnpjOptions { pad?: boolean; @@ -64,7 +64,7 @@ function charToAsciiValue(char: string): number { function onlyValidCNPJAlphanumeric(input: string): string { return input .split('') - .filter(char => VALID_CHARS_CNPJ.includes(char)) + .filter((char) => VALID_CHARS_CNPJ.includes(char)) .join(''); } @@ -85,10 +85,9 @@ function generateCNPJAlphanumericChars(length: number): string { .join(''); } - export function generate(): string { const baseCNPJ = generateCNPJAlphanumericChars(LENGTH - 2); - + const firstCheckDigitMod = generateChecksumCNPJ(baseCNPJ, FIRST_CHECK_DIGIT_WEIGHTS) % 11; const firstCheckDigit = (firstCheckDigitMod < 2 ? 0 : 11 - firstCheckDigitMod).toString(); @@ -99,7 +98,9 @@ export function generate(): string { } function isValidFormat(cnpj: string): boolean { - return /^[0-9ABCDFGHIJKLMNPQRSVWXYZ]{2}\.?[0-9ABCDFGHIJKLMNPQRSVWXYZ]{3}\.?[0-9ABCDFGHIJKLMNPQRSVWXYZ]{3}\/?[0-9ABCDFGHIJKLMNPQRSVWXYZ]{4}-?\d{2}$/.test(cnpj); + return /^[0-9ABCDFGHIJKLMNPQRSVWXYZ]{2}\.?[0-9ABCDFGHIJKLMNPQRSVWXYZ]{3}\.?[0-9ABCDFGHIJKLMNPQRSVWXYZ]{3}\/?[0-9ABCDFGHIJKLMNPQRSVWXYZ]{4}-?\d{2}$/.test( + cnpj + ); } export function isReservedNumber(cpf: string): boolean { @@ -133,6 +134,5 @@ export function isValid(cnpj: string): boolean { const validValue = onlyValidCNPJAlphanumeric(cnpj); - return isValidFormat(cnpj) && !isReservedNumber(validValue) && isValidChecksum(validValue); } diff --git a/src/utilities/email/index.ts b/src/utilities/email/index.ts index a22c6113..3f646ecc 100644 --- a/src/utilities/email/index.ts +++ b/src/utilities/email/index.ts @@ -2,7 +2,8 @@ const MAX_RECIPIENT_LENGTH = 64; const MAX_DOMAIN_LENGTH = 253; const MAX_EMAIL_LENGTH = MAX_RECIPIENT_LENGTH + 1 + MAX_DOMAIN_LENGTH; -const validEmailRegex = /^([!#$%&'*+\-/=?^_`{|}~]{0,1}([a-zA-Z0-9][!#$%&'*+\-/=?^_`{|}~.]{0,1})+)@(([a-zA-Z0-9][-.]{0,1})+)([.]{1}[a-zA-Z0-9]+)$/; +const validEmailRegex = + /^([!#$%&'*+\-/=?^_`{|}~]{0,1}([a-zA-Z0-9][!#$%&'*+\-/=?^_`{|}~.]{0,1})+)@(([a-zA-Z0-9][-.]{0,1})+)([.]{1}[a-zA-Z0-9]+)$/; const stringIsBiggerThan = (len: number, ...strs: string[]): boolean => strs.reduce((length, s) => length + s.length, 0) > len; From c7b1c1659a7918efbf926be5f5659a1bdfd51255 Mon Sep 17 00:00:00 2001 From: Amauri Dias Date: Thu, 25 Jul 2024 22:31:24 -0300 Subject: [PATCH 3/5] feat: add options email utilities version 2 --- src/utilities/cnpj/index.test.ts | 52 +++++++++++++++++++++----------- src/utilities/cnpj/index.ts | 46 ++++++++++++++++------------ src/utilities/email/index.ts | 3 +- 3 files changed, 62 insertions(+), 39 deletions(-) diff --git a/src/utilities/cnpj/index.test.ts b/src/utilities/cnpj/index.test.ts index 29aff727..c12c41e3 100644 --- a/src/utilities/cnpj/index.test.ts +++ b/src/utilities/cnpj/index.test.ts @@ -1,4 +1,4 @@ -import { format, LENGTH, isValid, generate, RESERVED_NUMBERS } from '.'; +import { format, LENGTH, isValid, generate, RESERVED_NUMBERS, FormatCnpjOptions, CnpjVersions } from '.'; describe('format', () => { test('should format cnpj with mask', () => { @@ -19,22 +19,26 @@ describe('format', () => { expect(format('46843485000186')).toBe('46.843.485/0001-86'); }); - test('should format cnpj alphanumeric with mask', () => { - expect(format('')).toBe(''); - expect(format('Q')).toBe('Q'); - expect(format('Q0')).toBe('Q0'); - expect(format('Q0S')).toBe('Q0.S'); - expect(format('Q0SL')).toBe('Q0.SL'); - expect(format('Q0SLF')).toBe('Q0.SLF'); - expect(format('Q0SLFM')).toBe('Q0.SLF.M'); - expect(format('Q0SLFMB')).toBe('Q0.SLF.MB'); - expect(format('Q0SLFMBD')).toBe('Q0.SLF.MBD'); - expect(format('Q0SLFMBD7')).toBe('Q0.SLF.MBD/7'); - expect(format('Q0SLFMBD7V')).toBe('Q0.SLF.MBD/7V'); - expect(format('Q0SLFMBD7VX')).toBe('Q0.SLF.MBD/7VX'); - expect(format('Q0SLFMBD7VX4')).toBe('Q0.SLF.MBD/7VX4'); - expect(format('Q0SLFMBD7VX43')).toBe('Q0.SLF.MBD/7VX4-3'); - expect(format('q0SLFMBD7VX439')).toBe('Q0.SLF.MBD/7VX4-39'); + test('should format cnpj alphanumeric with mask for version 2', () => { + const options: FormatCnpjOptions = { + version: '2', + }; + + expect(format('', options)).toBe(''); + expect(format('Q', options)).toBe('Q'); + expect(format('Q0', options)).toBe('Q0'); + expect(format('Q0S', options)).toBe('Q0.S'); + expect(format('Q0SL', options)).toBe('Q0.SL'); + expect(format('Q0SLF', options)).toBe('Q0.SLF'); + expect(format('Q0SLFM', options)).toBe('Q0.SLF.M'); + expect(format('Q0SLFMB', options)).toBe('Q0.SLF.MB'); + expect(format('Q0SLFMBD', options)).toBe('Q0.SLF.MBD'); + expect(format('Q0SLFMBD7', options)).toBe('Q0.SLF.MBD/7'); + expect(format('Q0SLFMBD7V', options)).toBe('Q0.SLF.MBD/7V'); + expect(format('Q0SLFMBD7VX', options)).toBe('Q0.SLF.MBD/7VX'); + expect(format('Q0SLFMBD7VX4', options)).toBe('Q0.SLF.MBD/7VX4'); + expect(format('Q0SLFMBD7VX43', options)).toBe('Q0.SLF.MBD/7VX4-3'); + expect(format('q0SLFMBD7VX439', options)).toBe('Q0.SLF.MBD/7VX4-39'); }); test('should format number cnpj with mask', () => { @@ -94,7 +98,11 @@ describe('format', () => { }); test('should remove all non numeric characters', () => { - expect(format('46.?ABC843.485/0001-86abc')).toBe('46.ABC.843/4850-00'); + 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: '2' })).toBe('46.ABC.843/4850-00'); }); }); @@ -109,6 +117,14 @@ describe('generate', () => { expect(isValid(generate())).toBe(true); } }); + + test('should return valid CNPJ version 2', () => { + const options: { version?: CnpjVersions } = { version: '2' }; + // iterate over 100 to insure that random generated CPNJ is valid + for (let i = 0; i < 100; i++) { + expect(isValid(generate(options), options)).toBe(true); + } + }); }); describe('isValid', () => { diff --git a/src/utilities/cnpj/index.ts b/src/utilities/cnpj/index.ts index 61de7dbb..4f1a068e 100644 --- a/src/utilities/cnpj/index.ts +++ b/src/utilities/cnpj/index.ts @@ -1,4 +1,4 @@ -import { isLastChar } from '../../helpers'; +import { isLastChar, onlyNumbers, generateRandomNumber } from '../../helpers'; export const LENGTH = 14; @@ -27,15 +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_CNPJ = '0123456789ABCDFGHIJKLMNPQRSVWXYZ'; +const VALID_CHARS = '0123456789ABCDFGHIJKLMNPQRSVWXYZ'; +export type CnpjVersions = '1' | '2'; export interface FormatCnpjOptions { pad?: boolean; + version?: CnpjVersions; } export function format(cnpj: string | number, options: FormatCnpjOptions = {}): string { - const valueCNPJUppercased = String(cnpj).toUpperCase(); - let digits = onlyValidCNPJAlphanumeric(valueCNPJUppercased); + let digits = options.version === '2' ? onlyValidCNPJAlphanumeric(String(cnpj).toUpperCase()) : onlyNumbers(cnpj); if (options.pad) { digits = digits.padStart(LENGTH, '0'); @@ -57,41 +58,43 @@ export function format(cnpj: string | number, options: FormatCnpjOptions = {}): }, ''); } -function charToAsciiValue(char: string): number { - return char.charCodeAt(0) - 48; -} - function onlyValidCNPJAlphanumeric(input: string): string { return input .split('') - .filter((char) => VALID_CHARS_CNPJ.includes(char)) + .filter((char) => VALID_CHARS.includes(char)) .join(''); } -function generateChecksumCNPJ(base: string, weight: number[]): number { +function generateChecksum(base: string, weight: number[]): number { const digits = onlyValidCNPJAlphanumeric(base); return digits.split('').reduce((acc, char, i) => { - const value = charToAsciiValue(char); + const value = char.charCodeAt(0) - 48; return acc + value * weight[i]; }, 0); } function generateCNPJAlphanumericChars(length: number): string { - const charset = VALID_CHARS_CNPJ; + const charset = VALID_CHARS; return Array(length) .fill('') .map(() => charset[Math.floor(Math.random() * charset.length)]) .join(''); } -export function generate(): string { - const baseCNPJ = generateCNPJAlphanumericChars(LENGTH - 2); +export interface GenerateCnpjOptions { + // botar um type Vesions + version?: CnpjVersions; +} + +export function generate(options: GenerateCnpjOptions = {}): string { + const baseCNPJ = + options.version === '2' ? generateCNPJAlphanumericChars(LENGTH - 2) : generateRandomNumber(LENGTH - 2); - const firstCheckDigitMod = generateChecksumCNPJ(baseCNPJ, FIRST_CHECK_DIGIT_WEIGHTS) % 11; + const firstCheckDigitMod = generateChecksum(baseCNPJ, FIRST_CHECK_DIGIT_WEIGHTS) % 11; const firstCheckDigit = (firstCheckDigitMod < 2 ? 0 : 11 - firstCheckDigitMod).toString(); - const secondCheckDigitMod = generateChecksumCNPJ(baseCNPJ + firstCheckDigit, SECOND_CHECK_DIGIT_WEIGHTS) % 11; + const secondCheckDigitMod = generateChecksum(baseCNPJ + firstCheckDigit, SECOND_CHECK_DIGIT_WEIGHTS) % 11; const secondCheckDigit = (secondCheckDigitMod < 2 ? 0 : 11 - secondCheckDigitMod).toString(); return `${baseCNPJ}${firstCheckDigit}${secondCheckDigit}`; @@ -117,7 +120,7 @@ export function isValidChecksum(cnpj: string): boolean { } const mod = - generateChecksumCNPJ( + generateChecksum( cnpj .slice(0, i) .split('') @@ -129,10 +132,15 @@ export function isValidChecksum(cnpj: string): boolean { }); } -export function isValid(cnpj: string): boolean { +export interface isValidCnpjOptions { + // botar um type Vesions + version?: CnpjVersions; +} + +export function isValid(cnpj: string, options: isValidCnpjOptions = {}): boolean { if (!cnpj || typeof cnpj !== 'string') return false; - const validValue = onlyValidCNPJAlphanumeric(cnpj); + const validValue = options.version === '2' ? onlyValidCNPJAlphanumeric(cnpj) : onlyNumbers(cnpj); return isValidFormat(cnpj) && !isReservedNumber(validValue) && isValidChecksum(validValue); } diff --git a/src/utilities/email/index.ts b/src/utilities/email/index.ts index 3f646ecc..a22c6113 100644 --- a/src/utilities/email/index.ts +++ b/src/utilities/email/index.ts @@ -2,8 +2,7 @@ const MAX_RECIPIENT_LENGTH = 64; const MAX_DOMAIN_LENGTH = 253; const MAX_EMAIL_LENGTH = MAX_RECIPIENT_LENGTH + 1 + MAX_DOMAIN_LENGTH; -const validEmailRegex = - /^([!#$%&'*+\-/=?^_`{|}~]{0,1}([a-zA-Z0-9][!#$%&'*+\-/=?^_`{|}~.]{0,1})+)@(([a-zA-Z0-9][-.]{0,1})+)([.]{1}[a-zA-Z0-9]+)$/; +const validEmailRegex = /^([!#$%&'*+\-/=?^_`{|}~]{0,1}([a-zA-Z0-9][!#$%&'*+\-/=?^_`{|}~.]{0,1})+)@(([a-zA-Z0-9][-.]{0,1})+)([.]{1}[a-zA-Z0-9]+)$/; const stringIsBiggerThan = (len: number, ...strs: string[]): boolean => strs.reduce((length, s) => length + s.length, 0) > len; From 29954e7942c2fd370bab0c9a050b32f7cfa62238 Mon Sep 17 00:00:00 2001 From: Amauri Dias Date: Mon, 29 Jul 2024 08:49:57 -0300 Subject: [PATCH 4/5] doc: add doc version 2 cnpj --- docs/pt-br/utilities.md | 26 +++++++++++++++++++++++++- docs/utilities.md | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) 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 From 2416a3c81e95ce901314b59b637fe66e36f3f7b3 Mon Sep 17 00:00:00 2001 From: Amauri Dias Date: Tue, 30 Jul 2024 14:01:02 -0300 Subject: [PATCH 5/5] feat: adjust cnpj version option string or number --- src/utilities/cnpj/index.test.ts | 46 +++++++++++++++++--------------- src/utilities/cnpj/index.ts | 11 +++----- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/utilities/cnpj/index.test.ts b/src/utilities/cnpj/index.test.ts index c12c41e3..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, FormatCnpjOptions, CnpjVersions } 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', () => { @@ -20,25 +24,21 @@ describe('format', () => { }); test('should format cnpj alphanumeric with mask for version 2', () => { - const options: FormatCnpjOptions = { - version: '2', - }; - - expect(format('', options)).toBe(''); - expect(format('Q', options)).toBe('Q'); - expect(format('Q0', options)).toBe('Q0'); - expect(format('Q0S', options)).toBe('Q0.S'); - expect(format('Q0SL', options)).toBe('Q0.SL'); - expect(format('Q0SLF', options)).toBe('Q0.SLF'); - expect(format('Q0SLFM', options)).toBe('Q0.SLF.M'); - expect(format('Q0SLFMB', options)).toBe('Q0.SLF.MB'); - expect(format('Q0SLFMBD', options)).toBe('Q0.SLF.MBD'); - expect(format('Q0SLFMBD7', options)).toBe('Q0.SLF.MBD/7'); - expect(format('Q0SLFMBD7V', options)).toBe('Q0.SLF.MBD/7V'); - expect(format('Q0SLFMBD7VX', options)).toBe('Q0.SLF.MBD/7VX'); - expect(format('Q0SLFMBD7VX4', options)).toBe('Q0.SLF.MBD/7VX4'); - expect(format('Q0SLFMBD7VX43', options)).toBe('Q0.SLF.MBD/7VX4-3'); - expect(format('q0SLFMBD7VX439', options)).toBe('Q0.SLF.MBD/7VX4-39'); + 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', () => { @@ -102,7 +102,9 @@ describe('format', () => { }); test('should remove non-alphanumeric characters for version 2', () => { - expect(format('46.?ABC843.485/0001-86abc', { version: '2' })).toBe('46.ABC.843/4850-00'); + expect(format('46.?ABC843.485/0001-86abc', { version: randomCnpjOptionVersion2StringOrNumber() })).toBe( + '46.ABC.843/4850-00' + ); }); }); @@ -119,9 +121,9 @@ describe('generate', () => { }); test('should return valid CNPJ version 2', () => { - const options: { version?: CnpjVersions } = { 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); } }); diff --git a/src/utilities/cnpj/index.ts b/src/utilities/cnpj/index.ts index 4f1a068e..69e02eda 100644 --- a/src/utilities/cnpj/index.ts +++ b/src/utilities/cnpj/index.ts @@ -29,14 +29,14 @@ export const SECOND_CHECK_DIGIT_WEIGHTS = [6, ...FIRST_CHECK_DIGIT_WEIGHTS]; const VALID_CHARS = '0123456789ABCDFGHIJKLMNPQRSVWXYZ'; -export type CnpjVersions = '1' | '2'; +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 = options.version === '2' ? onlyValidCNPJAlphanumeric(String(cnpj).toUpperCase()) : onlyNumbers(cnpj); + let digits = options.version == 2 ? onlyValidCNPJAlphanumeric(String(cnpj).toUpperCase()) : onlyNumbers(cnpj); if (options.pad) { digits = digits.padStart(LENGTH, '0'); @@ -83,13 +83,11 @@ function generateCNPJAlphanumericChars(length: number): string { } export interface GenerateCnpjOptions { - // botar um type Vesions version?: CnpjVersions; } export function generate(options: GenerateCnpjOptions = {}): string { - const baseCNPJ = - options.version === '2' ? generateCNPJAlphanumericChars(LENGTH - 2) : generateRandomNumber(LENGTH - 2); + 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(); @@ -133,14 +131,13 @@ export function isValidChecksum(cnpj: string): boolean { } export interface isValidCnpjOptions { - // botar um type Vesions version?: CnpjVersions; } export function isValid(cnpj: string, options: isValidCnpjOptions = {}): boolean { if (!cnpj || typeof cnpj !== 'string') return false; - const validValue = options.version === '2' ? onlyValidCNPJAlphanumeric(cnpj) : onlyNumbers(cnpj); + const validValue = options.version == 2 ? onlyValidCNPJAlphanumeric(cnpj) : onlyNumbers(cnpj); return isValidFormat(cnpj) && !isReservedNumber(validValue) && isValidChecksum(validValue); }