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
34 changes: 34 additions & 0 deletions docs/pt-br/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,40 @@ generateCnpj({ branch: 3 }); // bloco de ordem '0003', ex. '12345678000372'
generateCnpj({ version: 2, branch: 1 }); // CNPJ alfanumérico cujo bloco de ordem é '0001'
```

### getCnpjInfo

Interpreta um CNPJ nos campos que o número codifica. Aceita as mesmas formas de entrada que `isValidCnpj` e retorna `null` sempre que ela retornaria `false` para os mesmos argumentos, então um CNPJ alfanumérico lido na versão `1` é `null`.

- **Opções** (`GetCnpjInfoOptions`): `version` é lida como `isValidCnpj` a lê, `1` (padrão) apenas o formato numérico, `2` tanto o numérico quanto o alfanumérico.
- Retorna um `CnpjInfo`, as 14 posições como o Anexo XV as dispõe: 8 (`root`, a raiz que identifica a entidade) + 4 (`branch`, o número de ordem do estabelecimento) + 2 (`checkDigits`, os dígitos verificadores, sempre numéricos). O nome `branch` segue o parâmetro `branch` do `generateCnpj`, que preenche as mesmas quatro posições.
- `isInitialHeadquarters` diz se o número de ordem é `0001`, a que a Receita Federal atribui à matriz quando a raiz é inscrita. Uma filial pode depois se tornar a matriz mantendo o seu número de ordem, então só o cadastro da Receita Federal diz qual é a matriz atual.
- Os campos de um CNPJ alfanumérico são retornados em maiúsculas.

```javascript
import { getCnpjInfo } from '@brazilian-utils/brazilian-utils';

getCnpjInfo('12.345.678/0001-95');
// {
// root: '12345678',
// branch: '0001',
// checkDigits: '95',
// isInitialHeadquarters: true
// }

getCnpjInfo('12.abc.345/01de-35', { version: 2 });
// {
// root: '12ABC345',
// branch: '01DE',
// checkDigits: '35',
// isInitialHeadquarters: false
// }

getCnpjInfo('12.ABC.345/01DE-35'); // null (alfanumérico, lido na versão 1)
getCnpjInfo('12.345.678/0001-90'); // null (dígitos verificadores incorretos)
```

Fonte: [Instrução Normativa RFB nº 2.229/2024](http://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=141102), cujo Anexo Único é o Anexo XV da IN RFB nº 2.119/2022 e dispõe as 14 posições, [Perguntas e Respostas da Receita Federal sobre o CNPJ alfanumérico](https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/perguntas-e-respostas/cnpj/cnpj-alfanumerico.pdf) (perguntas 21, 23 e 25).

## CEP e endereço

### isValidCep
Expand Down
34 changes: 34 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,40 @@ generateCnpj({ branch: 3 }); // ordem block '0003', e.g. '12345678000372'
generateCnpj({ version: 2, branch: 1 }); // alphanumeric CNPJ whose ordem block is '0001'
```

### getCnpjInfo

Parse a CNPJ into the fields the number encodes. Accepts the same input forms as `isValidCnpj` and returns `null` whenever it would return `false` for the same arguments, so an alphanumeric CNPJ read under version `1` is `null`.

- **Options** (`GetCnpjInfoOptions`): `version` is read the way `isValidCnpj` reads it, `1` (default) the numeric-only format, `2` both the numeric and the alphanumeric one.
- Returns a `CnpjInfo`, the 14 positions as Anexo XV lays them out: 8 (`root`, the raiz that identifies the entity) + 4 (`branch`, the establishment, called número de ordem by the Receita Federal) + 2 (`checkDigits`, always numeric). `branch` is named after the `branch` parameter of `generateCnpj`, which fills the same four positions.
- `isInitialHeadquarters` tells whether the branch is `0001`, the one the Receita Federal gives the headquarters (matriz) when the root is registered. A filial can later become the headquarters while keeping its número de ordem, so only the Receita Federal registry tells the current headquarters.
- The fields of an alphanumeric CNPJ are returned upper cased.

```javascript
import { getCnpjInfo } from '@brazilian-utils/brazilian-utils';

getCnpjInfo('12.345.678/0001-95');
// {
// root: '12345678',
// branch: '0001',
// checkDigits: '95',
// isInitialHeadquarters: true
// }

getCnpjInfo('12.abc.345/01de-35', { version: 2 });
// {
// root: '12ABC345',
// branch: '01DE',
// checkDigits: '35',
// isInitialHeadquarters: false
// }

getCnpjInfo('12.ABC.345/01DE-35'); // null (alphanumeric, read under version 1)
getCnpjInfo('12.345.678/0001-90'); // null (bad check digits)
```

Source: [Instrução Normativa RFB nº 2.229/2024](http://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=141102), whose Anexo Único is the Anexo XV of IN RFB nº 2.119/2022 and lays the 14 positions out, [Receita Federal Q&A on the alphanumeric CNPJ](https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/perguntas-e-respostas/cnpj/cnpj-alfanumerico.pdf) (questions 21, 23 and 25).

## CEP and address

### isValidCep
Expand Down
1 change: 1 addition & 0 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"./get-cfop": "./src/get-cfop/get-cfop.ts",
"./get-cities": "./src/get-cities/get-cities.ts",
"./get-cnae": "./src/get-cnae/get-cnae.ts",
"./get-cnpj-info": "./src/get-cnpj-info/get-cnpj-info.ts",
"./get-cpf-info": "./src/get-cpf-info/get-cpf-info.ts",
"./get-format-license-plate": "./src/get-format-license-plate/get-format-license-plate.ts",
"./get-holidays": "./src/get-holidays/get-holidays.ts",
Expand Down
14 changes: 14 additions & 0 deletions reports/api/brazilian-utils.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ export type Cnae = {
description: string;
};

// @public
export type CnpjInfo = {
root: string;
branch: string;
checkDigits: string;
isInitialHeadquarters: boolean;
};

// @public
export const convertCurrencyToWords: (value: number) => string;

Expand Down Expand Up @@ -507,6 +515,12 @@ export const getCities: (state?: StateCode) => string[];
// @public
export const getCnae: (value: string | number) => Cnae | null;

// @public
export const getCnpjInfo: (value: string, options?: GetCnpjInfoOptions) => CnpjInfo | null;

// @public
export type GetCnpjInfoOptions = Pick<IsValidCnpjOptions, "version">;

// @public
export const getCpfInfo: (value: string) => CpfInfo | null;

Expand Down
7 changes: 7 additions & 0 deletions src/_internals/constants/cnpj.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ export const CNPJ_LENGTH = 14;
export const CNPJ_FIRST_DIGIT_WEIGHTS = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];

export const CNPJ_SECOND_DIGIT_WEIGHTS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];

/**
* Whether a sanitized CNPJ carries a letter, which makes it alphanumeric. The root and the branch
* take the digits `0` to `9` and the upper case letters `A` to `Z`, so the sanitized value is
* upper cased before the test.
*/
export const CNPJ_LETTER_REGEX = /[A-Z]/;
242 changes: 242 additions & 0 deletions src/get-cnpj-info/get-cnpj-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import * as fc from "fast-check";

import { anyText, anyValue } from "../_internals/test/arbitraries";
import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime";
import { formatCnpj } from "../format-cnpj/format-cnpj";
import { generateCnpj } from "../generate-cnpj/generate-cnpj";
import { isValidCnpj } from "../is-valid-cnpj/is-valid-cnpj";
import { type CnpjInfo, getCnpjInfo, type GetCnpjInfoOptions } from "./get-cnpj-info";

describe("getCnpjInfo", () => {
describe("should return the parsed numeric CNPJ", () => {
test("for the headquarters branch", () => {
expect(getCnpjInfo("12345678000195")).toEqual({
root: "12345678",
branch: "0001",
checkDigits: "95",
isInitialHeadquarters: true,
});
});

test("for another branch", () => {
expect(getCnpjInfo("12345678000276")).toEqual({
root: "12345678",
branch: "0002",
checkDigits: "76",
isInitialHeadquarters: false,
});
});

test("for a root with leading zeros", () => {
expect(getCnpjInfo("00000001000136")).toEqual({
root: "00000001",
branch: "0001",
checkDigits: "36",
isInitialHeadquarters: true,
});
});

test("for a masked value", () => {
expect(getCnpjInfo("12.345.678/0001-95")).toEqual({
root: "12345678",
branch: "0001",
checkDigits: "95",
isInitialHeadquarters: true,
});
});

test("for a value with a whitespace mask and surrounding whitespace", () => {
expect(getCnpjInfo(" 12 345 678 0002 76 ")).toEqual({
root: "12345678",
branch: "0002",
checkDigits: "76",
isInitialHeadquarters: false,
});
});

test("under version 2, which reads both formats", () => {
expect(getCnpjInfo("12.345.678/0001-95", { version: 2 })).toEqual({
root: "12345678",
branch: "0001",
checkDigits: "95",
isInitialHeadquarters: true,
});
});

test("under an unknown version, read as version 1", () => {
// @ts-expect-error: intentionally invalid option
expect(getCnpjInfo("12345678000195", { version: 3 })).toEqual({
root: "12345678",
branch: "0001",
checkDigits: "95",
isInitialHeadquarters: true,
});
});
});

describe("should return the parsed alphanumeric CNPJ under version 2", () => {
test("for the example of the Receita Federal check digit manual", () => {
expect(getCnpjInfo("12.ABC.345/01DE-35", { version: 2 })).toEqual({
root: "12ABC345",
branch: "01DE",
checkDigits: "35",
isInitialHeadquarters: false,
});
});

test("for a lowercase value, upper casing the fields", () => {
expect(getCnpjInfo("12abc34501de35", { version: 2 })).toEqual({
root: "12ABC345",
branch: "01DE",
checkDigits: "35",
isInitialHeadquarters: false,
});
});

test("for an alphanumeric root with the headquarters branch", () => {
expect(getCnpjInfo("AB.12C.D34/0001-84", { version: 2 })).toEqual({
root: "AB12CD34",
branch: "0001",
checkDigits: "84",
isInitialHeadquarters: true,
});
});

test("for an alphanumeric root and branch (Receita Federal Q&A, question 23)", () => {
expect(getCnpjInfo("AA345678/000A-29", { version: 2 })).toEqual({
root: "AA345678",
branch: "000A",
checkDigits: "29",
isInitialHeadquarters: false,
});
});

test("for a numeric root with an alphanumeric branch (Receita Federal Q&A, question 23)", () => {
expect(getCnpjInfo("12.345.678/000A-08", { version: 2 })).toEqual({
root: "12345678",
branch: "000A",
checkDigits: "08",
isInitialHeadquarters: false,
});
});
});

describe("should return null", () => {
test("when an alphanumeric CNPJ is read under the default version", () => {
expect(getCnpjInfo("12.ABC.345/01DE-35")).toBeNull();
expect(getCnpjInfo("12.ABC.345/01DE-35", {})).toBeNull();
expect(getCnpjInfo("12.ABC.345/01DE-35", { version: 1 })).toBeNull();
});

test("when the check digits do not match", () => {
expect(getCnpjInfo("12345678000190")).toBeNull();
expect(getCnpjInfo("12ABC34501DE34", { version: 2 })).toBeNull();
});

test("when a check digit is a letter", () => {
expect(getCnpjInfo("12ABC34501DE3A", { version: 2 })).toBeNull();
});

test("when it is a reserved repeated digits number", () => {
expect(getCnpjInfo("00000000000000")).toBeNull();
expect(getCnpjInfo("11111111111111", { version: 2 })).toBeNull();
});

test("when it is shorter or longer than 14 characters", () => {
expect(getCnpjInfo("1234567800019")).toBeNull();
expect(getCnpjInfo("123456780001955")).toBeNull();
});

test("when it carries a character outside the mask", () => {
expect(getCnpjInfo("12.345.678/0001-95x")).toBeNull();
expect(getCnpjInfo("12_345_678_0001_95")).toBeNull();
});

test("when it is an empty string", () => {
expect(getCnpjInfo("")).toBeNull();
});

test("when it is not a string", () => {
// @ts-expect-error: intentionally invalid input
expect(getCnpjInfo(null)).toBeNull();
// @ts-expect-error: intentionally invalid input
expect(getCnpjInfo()).toBeNull();
// @ts-expect-error: intentionally invalid input
expect(getCnpjInfo(12_345_678_000_195)).toBeNull();
// @ts-expect-error: intentionally invalid input
expect(getCnpjInfo({})).toBeNull();
// @ts-expect-error: intentionally invalid input
expect(getCnpjInfo(["12345678000195"])).toBeNull();
});

test("when the options are null", () => {
// @ts-expect-error: intentionally invalid options
expect(getCnpjInfo("12.ABC.345/01DE-35", null)).toBeNull();
});
});

describe("properties", () => {
const version = fc.constantFrom(1 as const, 2 as const);

test("should split a generated CNPJ into fields that spell it back, masked or not", () => {
fc.assert(
fc.property(version, fc.boolean(), (currentVersion, masked) => {
const cnpj = generateCnpj(currentVersion);
const written = masked ? formatCnpj(cnpj, { version: 2 }) : cnpj;
const parsed = getCnpjInfo(written.toLowerCase(), { version: 2 });

expect(`${parsed?.root}${parsed?.branch}${parsed?.checkDigits}`).toBe(cnpj);
}),
);
});

test("should flag the branch 0001 of a generated CNPJ and no other", () => {
fc.assert(
fc.property(version, fc.integer({ min: 1, max: 9999 }), (currentVersion, branch) => {
const cnpj = generateCnpj({ version: currentVersion, branch });
const parsed = getCnpjInfo(cnpj, { version: currentVersion });

expect(parsed?.branch).toBe(String(branch).padStart(4, "0"));
expect(parsed?.isInitialHeadquarters).toBe(branch === 1);
}),
);
});

test("should return a value exactly when the CNPJ is valid", () => {
fc.assert(
fc.property(fc.oneof(anyText, anyValue), version, (value, currentVersion) => {
const options = { version: currentVersion };

expect(getCnpjInfo(value as string, options) !== null).toBe(
isValidCnpj(value as string, options),
);
}),
);
});

test("should never throw and always return a CNPJ or null", () => {
fc.assert(
fc.property(anyValue, fc.anything(), (value, options) => {
const parsed = getCnpjInfo(value as string, options as GetCnpjInfoOptions);

expect(parsed === null || parsed.root.length === 8).toBe(true);
}),
);
});
});
});

describe("getCnpjInfo types", () => {
test("should take a string and options and return a CnpjInfo or null", () => {
expectTypeOf(getCnpjInfo).parameter(0).toEqualTypeOf<string>();
expectTypeOf(getCnpjInfo).parameter(1).toEqualTypeOf<GetCnpjInfoOptions | undefined>();
expectTypeOf(getCnpjInfo).returns.toEqualTypeOf<CnpjInfo | null>();
expectTypeOf<GetCnpjInfoOptions>().toEqualTypeOf<{ version?: 1 | 2 }>();
expectTypeOf<CnpjInfo>().toEqualTypeOf<{
root: string;
branch: string;
checkDigits: string;
isInitialHeadquarters: boolean;
}>();
});
});
Loading
Loading