Skip to content
Closed
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
26 changes: 25 additions & 1 deletion docs/pt-br/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,16 +60,32 @@ 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.

```javascript
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
Expand Down
24 changes: 24 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
38 changes: 37 additions & 1 deletion src/utilities/cnpj/index.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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');
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
56 changes: 47 additions & 9 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, onlyNumbers, generateRandomNumber } from '../../helpers';

export const LENGTH = 14;

Expand Down Expand Up @@ -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');
Expand All @@ -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();
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}