From bb1b22a1efc16decb881b2aea582ec3d2bdb5b7e Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:52:28 -0300 Subject: [PATCH 1/4] feat(get-cnpj-info): add getCnpjInfo to read the root, order and check digits of a CNPJ A CNPJ encodes more than its validity: Anexo XV of IN RFB 2.119/2022, added by IN RFB 2.229/2024, lays the 14 positions out as 8 (root, the entity) + 4 (order, the establishment) + 2 (numeric check digits), for the numeric and for the alphanumeric format assigned from July 2026. getCnpjInfo returns those fields, the format the value is written in and whether the order is 0001, or null when isValidCnpj turns the same arguments down. The version option is the one of isValidCnpj (1 numeric only and the default, 2 both formats), so the CNPJ utils agree on what they read and nothing already exported changes. The 0001 flag is named isInitialHeadquarters because the Receita Federal Q&A (question 25) states that a branch can become the headquarters while keeping its order: the number only tells which establishment was the headquarters when the root was registered. --- docs/pt-br/utilities.md | 37 ++++ docs/utilities.md | 37 ++++ jsr.json | 1 + reports/api/brazilian-utils.api.md | 18 ++ src/get-cnpj-info/get-cnpj-info.test.ts | 262 ++++++++++++++++++++++++ src/get-cnpj-info/get-cnpj-info.ts | 111 ++++++++++ src/index.test.ts | 7 + src/index.ts | 6 + 8 files changed, 479 insertions(+) create mode 100644 src/get-cnpj-info/get-cnpj-info.test.ts create mode 100644 src/get-cnpj-info/get-cnpj-info.ts diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 75d119ef..e4ae30f8 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -170,6 +170,43 @@ 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 (`order`, o número de ordem do estabelecimento) + 2 (`checkDigits`, os dígitos verificadores, sempre numéricos). +- `format` é `'alphanumeric'` quando a raiz ou a ordem têm uma letra e `'numeric'` caso contrário (tipado como `CnpjFormat`). +- `isInitialHeadquarters` diz se a 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', +// order: '0001', +// checkDigits: '95', +// format: 'numeric', +// isInitialHeadquarters: true +// } + +getCnpjInfo('12.abc.345/01de-35', { version: 2 }); +// { +// root: '12ABC345', +// order: '01DE', +// checkDigits: '35', +// format: 'alphanumeric', +// 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 diff --git a/docs/utilities.md b/docs/utilities.md index b10fc7bc..deef9e65 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -170,6 +170,43 @@ 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 (`order`, the número de ordem of the establishment) + 2 (`checkDigits`, always numeric). +- `format` is `'alphanumeric'` when the root or the order carries a letter and `'numeric'` otherwise (typed as `CnpjFormat`). +- `isInitialHeadquarters` tells whether the order 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 order, 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', +// order: '0001', +// checkDigits: '95', +// format: 'numeric', +// isInitialHeadquarters: true +// } + +getCnpjInfo('12.abc.345/01de-35', { version: 2 }); +// { +// root: '12ABC345', +// order: '01DE', +// checkDigits: '35', +// format: 'alphanumeric', +// 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 diff --git a/jsr.json b/jsr.json index 6a2299eb..e8ceb850 100644 --- a/jsr.json +++ b/jsr.json @@ -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", diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md index 0d630e3f..987745f4 100644 --- a/reports/api/brazilian-utils.api.md +++ b/reports/api/brazilian-utils.api.md @@ -114,6 +114,18 @@ export type Cnae = { description: string; }; +// @public +export type CnpjFormat = "numeric" | "alphanumeric"; + +// @public +export type CnpjInfo = { + root: string; + order: string; + checkDigits: string; + format: CnpjFormat; + isInitialHeadquarters: boolean; +}; + // @public export const convertCurrencyToWords: (value: number) => string; @@ -507,6 +519,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; + // @public export const getCpfInfo: (value: string) => CpfInfo | null; diff --git a/src/get-cnpj-info/get-cnpj-info.test.ts b/src/get-cnpj-info/get-cnpj-info.test.ts new file mode 100644 index 00000000..17505536 --- /dev/null +++ b/src/get-cnpj-info/get-cnpj-info.test.ts @@ -0,0 +1,262 @@ +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 CnpjFormat, + type CnpjInfo, + getCnpjInfo, + type GetCnpjInfoOptions, +} from "./get-cnpj-info"; + +describe("getCnpjInfo", () => { + describe("should return the parsed numeric CNPJ", () => { + test("for a headquarters order", () => { + expect(getCnpjInfo("12345678000195")).toEqual({ + root: "12345678", + order: "0001", + checkDigits: "95", + format: "numeric", + isInitialHeadquarters: true, + }); + }); + + test("for a branch order", () => { + expect(getCnpjInfo("12345678000276")).toEqual({ + root: "12345678", + order: "0002", + checkDigits: "76", + format: "numeric", + isInitialHeadquarters: false, + }); + }); + + test("for a root with leading zeros", () => { + expect(getCnpjInfo("00000001000136")).toEqual({ + root: "00000001", + order: "0001", + checkDigits: "36", + format: "numeric", + isInitialHeadquarters: true, + }); + }); + + test("for a masked value", () => { + expect(getCnpjInfo("12.345.678/0001-95")).toEqual({ + root: "12345678", + order: "0001", + checkDigits: "95", + format: "numeric", + isInitialHeadquarters: true, + }); + }); + + test("for a value with a whitespace mask and surrounding whitespace", () => { + expect(getCnpjInfo(" 12 345 678 0002 76 ")).toEqual({ + root: "12345678", + order: "0002", + checkDigits: "76", + format: "numeric", + isInitialHeadquarters: false, + }); + }); + + test("under version 2, which reads both formats", () => { + expect(getCnpjInfo("12.345.678/0001-95", { version: 2 })).toEqual({ + root: "12345678", + order: "0001", + checkDigits: "95", + format: "numeric", + 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", + order: "0001", + checkDigits: "95", + format: "numeric", + 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", + order: "01DE", + checkDigits: "35", + format: "alphanumeric", + isInitialHeadquarters: false, + }); + }); + + test("for a lowercase value, upper casing the fields", () => { + expect(getCnpjInfo("12abc34501de35", { version: 2 })).toEqual({ + root: "12ABC345", + order: "01DE", + checkDigits: "35", + format: "alphanumeric", + isInitialHeadquarters: false, + }); + }); + + test("for an alphanumeric root with the headquarters order", () => { + expect(getCnpjInfo("AB.12C.D34/0001-84", { version: 2 })).toEqual({ + root: "AB12CD34", + order: "0001", + checkDigits: "84", + format: "alphanumeric", + isInitialHeadquarters: true, + }); + }); + + test("for an alphanumeric root and order (Receita Federal Q&A, question 23)", () => { + expect(getCnpjInfo("AA345678/000A-29", { version: 2 })).toEqual({ + root: "AA345678", + order: "000A", + checkDigits: "29", + format: "alphanumeric", + isInitialHeadquarters: false, + }); + }); + + test("for a numeric root with an alphanumeric order (Receita Federal Q&A, question 23)", () => { + expect(getCnpjInfo("12.345.678/000A-08", { version: 2 })).toEqual({ + root: "12345678", + order: "000A", + checkDigits: "08", + format: "alphanumeric", + 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?.order}${parsed?.checkDigits}`).toBe(cnpj); + expect(parsed?.format).toBe(/[A-Z]/.test(cnpj) ? "alphanumeric" : "numeric"); + }), + ); + }); + + test("should flag the order 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?.order).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(); + expectTypeOf(getCnpjInfo).parameter(1).toEqualTypeOf(); + expectTypeOf(getCnpjInfo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ version?: 1 | 2 }>(); + expectTypeOf().toEqualTypeOf<"numeric" | "alphanumeric">(); + expectTypeOf().toEqualTypeOf<{ + root: string; + order: string; + checkDigits: string; + format: CnpjFormat; + isInitialHeadquarters: boolean; + }>(); + }); +}); diff --git a/src/get-cnpj-info/get-cnpj-info.ts b/src/get-cnpj-info/get-cnpj-info.ts new file mode 100644 index 00000000..879e6c04 --- /dev/null +++ b/src/get-cnpj-info/get-cnpj-info.ts @@ -0,0 +1,111 @@ +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; +import { isValidCnpj, type IsValidCnpjOptions } from "../is-valid-cnpj/is-valid-cnpj"; + +/** Options of `getCnpjInfo`. */ +export type GetCnpjInfoOptions = Pick; + +/** How a CNPJ is written: `"numeric"` digits only, `"alphanumeric"` with a letter in the root or the order. */ +export type CnpjFormat = "numeric" | "alphanumeric"; + +/** The fields `getCnpjInfo` reads out of a CNPJ. */ +export type CnpjInfo = { + /** The 8 character root (raiz), positions 1 to 8, shared by every establishment of the entity. */ + root: string; + /** The 4 character order (número de ordem) of the establishment, positions 9 to 12. */ + order: string; + /** The 2 numeric check digits (dígitos verificadores), positions 13 and 14. */ + checkDigits: string; + /** `"alphanumeric"` when the root or the order carries a letter, `"numeric"` otherwise. */ + format: CnpjFormat; + /** + * Whether the order is `0001`, the one the Receita Federal gives the headquarters (matriz) + * when the root is registered. A branch (filial) that later becomes the headquarters keeps + * its order, so only the Receita Federal registry tells the current headquarters. + */ + isInitialHeadquarters: boolean; +}; + +const ROOT_END = 8; + +const ORDER_END = 12; + +const INITIAL_HEADQUARTERS_ORDER = "0001"; + +const LETTER_REGEX = /[A-Z]/; + +/** + * Parses a CNPJ (Cadastro Nacional da Pessoa Jurídica) into the fields the number encodes. + * + * Anexo XV of Instrução Normativa RFB nº 2.119/2022, added by Instrução Normativa RFB + * nº 2.229/2024, lays the 14 positions out as 8 (root, raiz, the entity) + 4 (order, número de + * ordem, the establishment) + 2 (check digits, always numeric). In the alphanumeric format, + * assigned to new registrations from July 2026, the root and the order take the digits `0` to + * `9` and the upper case letters `A` to `Z`, and either of them may still come out all numeric. + * + * Accepts the same input forms and reads `options.version` the same way as `isValidCnpj`: `1` + * (the default) recognizes the numeric format only, `2` recognizes both, and any other value is + * read as `1`. Returns `null` whenever `isValidCnpj` would return `false` for the same + * arguments, so an alphanumeric CNPJ read under version `1` is `null`. The fields of an + * alphanumeric CNPJ are returned upper cased. + * + * The order `0001` marks the headquarters (matriz) only at registration: the Receita Federal + * Q&A (question 25) states that a branch (filial) can become the headquarters while keeping + * its order, hence `isInitialHeadquarters` instead of a definitive headquarters flag. + * + * @param {string} value - The CNPJ to be parsed. + * @param {GetCnpjInfoOptions} [options] - Optional options. + * @param {1|2} [options.version] - `1` reads the numeric-only format (the default), `2` reads + * both the numeric and the alphanumeric formats. + * @returns {CnpjInfo|null} The parsed CNPJ, or `null` when it is not valid. + * + * @example + * ```typescript + * getCnpjInfo("12.345.678/0001-95"); + * // { + * // root: "12345678", + * // order: "0001", + * // checkDigits: "95", + * // format: "numeric", + * // isInitialHeadquarters: true, + * // } + * + * getCnpjInfo("12.abc.345/01de-35", { version: 2 }); + * // { + * // root: "12ABC345", + * // order: "01DE", + * // checkDigits: "35", + * // format: "alphanumeric", + * // isInitialHeadquarters: false, + * // } + * + * getCnpjInfo("12.ABC.345/01DE-35"); // null (alphanumeric, read under version 1) + * getCnpjInfo("12.345.678/0001-90"); // null (bad check digits) + * ``` + * + * @see Official: http://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=141102 + * Instrução Normativa RFB nº 2.229/2024, whose Anexo Único is the Anexo XV of IN RFB + * nº 2.119/2022: positions 1 to 8 root, 9 to 12 order, 13 and 14 check digits. + * @see Official: https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/perguntas-e-respostas/cnpj/cnpj-alfanumerico.pdf + * Receita Federal Q&A on the alphanumeric CNPJ: questions 21 and 23 (root and order), 25 (the + * order `0001` and the headquarters) and the `AA345678/000A-29` and `12.345.678/000A-08` + * examples. + * @see Official: https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/documentos-tecnicos/cnpj/manual-dv-cnpj.pdf + * Check digit manual, source of the `12.ABC.345/01DE-35` example. + * @see Official: https://www.gov.br/receitafederal/pt-br/acesso-a-informacao/acoes-e-programas/programas-e-atividades/cnpj-alfanumerico + * @see Based on: https://anvisalegis.datalegis.net/action/ActionDatalegis.php?acao=detalharAto&tipo=INM&numeroAto=00002229&seqAto=000&valorAno=2024&orgao=RFB/MF + * Mirror of IN RFB nº 2.229/2024 where the Anexo Único was read. + */ +export const getCnpjInfo = (value: string, options?: GetCnpjInfoOptions): CnpjInfo | null => { + if (!isValidCnpj(value, options)) return null; + + const cnpj = sanitizeToAlphanumeric(value); + const order = cnpj.slice(ROOT_END, ORDER_END); + + return { + root: cnpj.slice(0, ROOT_END), + order, + checkDigits: cnpj.slice(ORDER_END), + format: LETTER_REGEX.test(cnpj) ? "alphanumeric" : "numeric", + isInitialHeadquarters: order === INITIAL_HEADQUARTERS_ORDER, + }; +}; diff --git a/src/index.test.ts b/src/index.test.ts index a4e4cf4f..8e64effc 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -13,6 +13,8 @@ import { type CertidaoType, type Cfop, type Cnae, + type CnpjFormat, + type CnpjInfo, type ConvertDateToWordsOptions, type ConvertNumberToWordsOptions, type CpfInfo, @@ -46,6 +48,7 @@ import { type GetBoletoInfoOptions, type GetCepInfoByAddressOptions, type GetCepInfoByAddressParams, + type GetCnpjInfoOptions, type GetHolidaysOptions, type GetHolidaysParams, type GetLegalNaturesByCategoryOptions, @@ -175,6 +178,7 @@ const PUBLIC = [ "getCfop", "getCities", "getCnae", + "getCnpjInfo", "getCpfInfo", "getFormatLicensePlate", "getHolidays", @@ -308,6 +312,8 @@ describe("Public API", () => { CertidaoType: CertidaoType; Cfop: Cfop; Cnae: Cnae; + CnpjFormat: CnpjFormat; + CnpjInfo: CnpjInfo; ConvertDateToWordsOptions: ConvertDateToWordsOptions; ConvertNumberToWordsOptions: ConvertNumberToWordsOptions; CpfInfo: CpfInfo; @@ -341,6 +347,7 @@ describe("Public API", () => { GetBoletoInfoOptions: GetBoletoInfoOptions; GetCepInfoByAddressOptions: GetCepInfoByAddressOptions; GetCepInfoByAddressParams: GetCepInfoByAddressParams; + GetCnpjInfoOptions: GetCnpjInfoOptions; GetHolidaysOptions: GetHolidaysOptions; GetHolidaysParams: GetHolidaysParams; GetLegalNaturesByCategoryOptions: GetLegalNaturesByCategoryOptions; diff --git a/src/index.ts b/src/index.ts index 98c3dd61..4a088b1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,6 +116,12 @@ export { export { type Cfop, getCfop } from "./get-cfop/get-cfop"; export { getCities } from "./get-cities/get-cities"; export { type Cnae, getCnae } from "./get-cnae/get-cnae"; +export { + type CnpjFormat, + type CnpjInfo, + getCnpjInfo, + type GetCnpjInfoOptions, +} from "./get-cnpj-info/get-cnpj-info"; export { type CpfInfo, getCpfInfo } from "./get-cpf-info/get-cpf-info"; export { getFormatLicensePlate, From d16836c884455743f95c9383fb965c899d4daac2 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:49:50 -0300 Subject: [PATCH 2/4] refactor(cnpj): share the letter regex between isValidCnpj and getCnpjInfo Both modules asked the same question of a sanitized CNPJ, whether it carries a letter, with their own copy of /[A-Z]/. A constant used by two modules belongs in src/_internals/constants, next to CNPJ_LENGTH and the check digit weights, so it moves there as CNPJ_LETTER_REGEX. The docs of CnpjInfo.order also now say that those four positions are the ones generateCnpj takes as branch, so a reader moving between the two functions can tell the two names mean the same block. --- docs/pt-br/utilities.md | 2 +- docs/utilities.md | 2 +- src/_internals/constants/cnpj.ts | 7 +++++++ src/get-cnpj-info/get-cnpj-info.ts | 7 +++---- src/is-valid-cnpj/is-valid-cnpj.ts | 10 ++++++---- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index e4ae30f8..37f005a2 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -175,7 +175,7 @@ generateCnpj({ version: 2, branch: 1 }); // CNPJ alfanumérico cujo bloco de ord 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 (`order`, o número de ordem do estabelecimento) + 2 (`checkDigits`, os dígitos verificadores, sempre numéricos). +- Retorna um `CnpjInfo`, as 14 posições como o Anexo XV as dispõe: 8 (`root`, a raiz que identifica a entidade) + 4 (`order`, o número de ordem do estabelecimento, as mesmas quatro posições que o `generateCnpj` recebe no parâmetro `branch`) + 2 (`checkDigits`, os dígitos verificadores, sempre numéricos). - `format` é `'alphanumeric'` quando a raiz ou a ordem têm uma letra e `'numeric'` caso contrário (tipado como `CnpjFormat`). - `isInitialHeadquarters` diz se a 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. diff --git a/docs/utilities.md b/docs/utilities.md index deef9e65..a5d17f88 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -175,7 +175,7 @@ generateCnpj({ version: 2, branch: 1 }); // alphanumeric CNPJ whose ordem block 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 (`order`, the número de ordem of the establishment) + 2 (`checkDigits`, always numeric). +- Returns a `CnpjInfo`, the 14 positions as Anexo XV lays them out: 8 (`root`, the raiz that identifies the entity) + 4 (`order`, the número de ordem of the establishment, the same four positions `generateCnpj` takes as its `branch` parameter) + 2 (`checkDigits`, always numeric). - `format` is `'alphanumeric'` when the root or the order carries a letter and `'numeric'` otherwise (typed as `CnpjFormat`). - `isInitialHeadquarters` tells whether the order 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 order, so only the Receita Federal registry tells the current headquarters. - The fields of an alphanumeric CNPJ are returned upper cased. diff --git a/src/_internals/constants/cnpj.ts b/src/_internals/constants/cnpj.ts index 3b1de61c..112b64e7 100644 --- a/src/_internals/constants/cnpj.ts +++ b/src/_internals/constants/cnpj.ts @@ -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 order + * take the digits `0` to `9` and the upper case letters `A` to `Z`, so the sanitized value is + * upper cased before the test. Shared by `isValidCnpj` and `getCnpjInfo`. + */ +export const CNPJ_LETTER_REGEX = /[A-Z]/; diff --git a/src/get-cnpj-info/get-cnpj-info.ts b/src/get-cnpj-info/get-cnpj-info.ts index 879e6c04..9420e1cf 100644 --- a/src/get-cnpj-info/get-cnpj-info.ts +++ b/src/get-cnpj-info/get-cnpj-info.ts @@ -1,3 +1,4 @@ +import { CNPJ_LETTER_REGEX } from "../_internals/constants/cnpj"; import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { isValidCnpj, type IsValidCnpjOptions } from "../is-valid-cnpj/is-valid-cnpj"; @@ -11,7 +12,7 @@ export type CnpjFormat = "numeric" | "alphanumeric"; export type CnpjInfo = { /** The 8 character root (raiz), positions 1 to 8, shared by every establishment of the entity. */ root: string; - /** The 4 character order (número de ordem) of the establishment, positions 9 to 12. */ + /** The 4 character order (número de ordem) of the establishment, positions 9 to 12, the ones `generateCnpj` takes as `branch`. */ order: string; /** The 2 numeric check digits (dígitos verificadores), positions 13 and 14. */ checkDigits: string; @@ -31,8 +32,6 @@ const ORDER_END = 12; const INITIAL_HEADQUARTERS_ORDER = "0001"; -const LETTER_REGEX = /[A-Z]/; - /** * Parses a CNPJ (Cadastro Nacional da Pessoa Jurídica) into the fields the number encodes. * @@ -105,7 +104,7 @@ export const getCnpjInfo = (value: string, options?: GetCnpjInfoOptions): CnpjIn root: cnpj.slice(0, ROOT_END), order, checkDigits: cnpj.slice(ORDER_END), - format: LETTER_REGEX.test(cnpj) ? "alphanumeric" : "numeric", + format: CNPJ_LETTER_REGEX.test(cnpj) ? "alphanumeric" : "numeric", isInitialHeadquarters: order === INITIAL_HEADQUARTERS_ORDER, }; }; diff --git a/src/is-valid-cnpj/is-valid-cnpj.ts b/src/is-valid-cnpj/is-valid-cnpj.ts index 8709c831..34730ed7 100644 --- a/src/is-valid-cnpj/is-valid-cnpj.ts +++ b/src/is-valid-cnpj/is-valid-cnpj.ts @@ -1,5 +1,9 @@ import { calculateCnpjCheckDigit } from "../_internals/calculate-cnpj-check-digit/calculate-cnpj-check-digit"; -import { CNPJ_FIRST_DIGIT_WEIGHTS, CNPJ_SECOND_DIGIT_WEIGHTS } from "../_internals/constants/cnpj"; +import { + CNPJ_FIRST_DIGIT_WEIGHTS, + CNPJ_LETTER_REGEX, + CNPJ_SECOND_DIGIT_WEIGHTS, +} from "../_internals/constants/cnpj"; import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; @@ -15,8 +19,6 @@ const FORMAT_REGEX = const NUMERIC_FORMAT_REGEX = /^\d{2}[\s.\-/]*\d{3}[\s.\-/]*\d{3}[\s.\-/]*\d{4}[\s.\-/]*\d{2}$/; -const LETTER_REGEX = /[A-Z]/; - const isValidChecksum = (cnpj: string): boolean => cnpj.charCodeAt(12) - 48 === calculateCnpjCheckDigit(cnpj, CNPJ_FIRST_DIGIT_WEIGHTS) && cnpj.charCodeAt(13) - 48 === calculateCnpjCheckDigit(cnpj, CNPJ_SECOND_DIGIT_WEIGHTS); @@ -64,7 +66,7 @@ export const isValidCnpj = (cnpj: string, options?: IsValidCnpjOptions): boolean if (options?.version === 2) { const cleaned = sanitizeToAlphanumeric(cnpj); - if (LETTER_REGEX.test(cleaned)) { + if (CNPJ_LETTER_REGEX.test(cleaned)) { return FORMAT_REGEX.test(trimmed.toUpperCase()) && isValidChecksum(cleaned); } } From b70395a28f6816768e6fa79ea5afe03d6d9978fe Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:13:04 -0300 Subject: [PATCH 3/4] refactor(get-cnpj-info): name the order block branch like generateCnpj MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Positions 9 to 12 of a CNPJ were exposed as CnpjInfo.order, after the "número de ordem" of the Receita Federal. The already released generateCnpj fills those same positions through its branch option, so reading them back under a different name would make the two utilities disagree about one concept. The key is now branch, and its JSDoc keeps the Receita Federal term so the mapping to the official layout stays visible. --- docs/pt-br/utilities.md | 10 +++--- docs/utilities.md | 10 +++--- reports/api/brazilian-utils.api.md | 2 +- src/_internals/constants/cnpj.ts | 2 +- src/get-cnpj-info/get-cnpj-info.test.ts | 42 +++++++++++----------- src/get-cnpj-info/get-cnpj-info.ts | 46 ++++++++++++------------- 6 files changed, 56 insertions(+), 56 deletions(-) diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 37f005a2..7014a7ae 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -175,9 +175,9 @@ generateCnpj({ version: 2, branch: 1 }); // CNPJ alfanumérico cujo bloco de ord 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 (`order`, o número de ordem do estabelecimento, as mesmas quatro posições que o `generateCnpj` recebe no parâmetro `branch`) + 2 (`checkDigits`, os dígitos verificadores, sempre numéricos). -- `format` é `'alphanumeric'` quando a raiz ou a ordem têm uma letra e `'numeric'` caso contrário (tipado como `CnpjFormat`). -- `isInitialHeadquarters` diz se a 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. +- 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. +- `format` é `'alphanumeric'` quando a raiz ou o número de ordem têm uma letra e `'numeric'` caso contrário (tipado como `CnpjFormat`). +- `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 @@ -186,7 +186,7 @@ import { getCnpjInfo } from '@brazilian-utils/brazilian-utils'; getCnpjInfo('12.345.678/0001-95'); // { // root: '12345678', -// order: '0001', +// branch: '0001', // checkDigits: '95', // format: 'numeric', // isInitialHeadquarters: true @@ -195,7 +195,7 @@ getCnpjInfo('12.345.678/0001-95'); getCnpjInfo('12.abc.345/01de-35', { version: 2 }); // { // root: '12ABC345', -// order: '01DE', +// branch: '01DE', // checkDigits: '35', // format: 'alphanumeric', // isInitialHeadquarters: false diff --git a/docs/utilities.md b/docs/utilities.md index a5d17f88..be443cf6 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -175,9 +175,9 @@ generateCnpj({ version: 2, branch: 1 }); // alphanumeric CNPJ whose ordem block 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 (`order`, the número de ordem of the establishment, the same four positions `generateCnpj` takes as its `branch` parameter) + 2 (`checkDigits`, always numeric). -- `format` is `'alphanumeric'` when the root or the order carries a letter and `'numeric'` otherwise (typed as `CnpjFormat`). -- `isInitialHeadquarters` tells whether the order 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 order, so only the Receita Federal registry tells the current headquarters. +- 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. +- `format` is `'alphanumeric'` when the root or the branch carries a letter and `'numeric'` otherwise (typed as `CnpjFormat`). +- `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 @@ -186,7 +186,7 @@ import { getCnpjInfo } from '@brazilian-utils/brazilian-utils'; getCnpjInfo('12.345.678/0001-95'); // { // root: '12345678', -// order: '0001', +// branch: '0001', // checkDigits: '95', // format: 'numeric', // isInitialHeadquarters: true @@ -195,7 +195,7 @@ getCnpjInfo('12.345.678/0001-95'); getCnpjInfo('12.abc.345/01de-35', { version: 2 }); // { // root: '12ABC345', -// order: '01DE', +// branch: '01DE', // checkDigits: '35', // format: 'alphanumeric', // isInitialHeadquarters: false diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md index 987745f4..3319c9b7 100644 --- a/reports/api/brazilian-utils.api.md +++ b/reports/api/brazilian-utils.api.md @@ -120,7 +120,7 @@ export type CnpjFormat = "numeric" | "alphanumeric"; // @public export type CnpjInfo = { root: string; - order: string; + branch: string; checkDigits: string; format: CnpjFormat; isInitialHeadquarters: boolean; diff --git a/src/_internals/constants/cnpj.ts b/src/_internals/constants/cnpj.ts index 112b64e7..8312f465 100644 --- a/src/_internals/constants/cnpj.ts +++ b/src/_internals/constants/cnpj.ts @@ -6,7 +6,7 @@ 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 order + * 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. Shared by `isValidCnpj` and `getCnpjInfo`. */ diff --git a/src/get-cnpj-info/get-cnpj-info.test.ts b/src/get-cnpj-info/get-cnpj-info.test.ts index 17505536..ede4fb80 100644 --- a/src/get-cnpj-info/get-cnpj-info.test.ts +++ b/src/get-cnpj-info/get-cnpj-info.test.ts @@ -14,20 +14,20 @@ import { describe("getCnpjInfo", () => { describe("should return the parsed numeric CNPJ", () => { - test("for a headquarters order", () => { + test("for the headquarters branch", () => { expect(getCnpjInfo("12345678000195")).toEqual({ root: "12345678", - order: "0001", + branch: "0001", checkDigits: "95", format: "numeric", isInitialHeadquarters: true, }); }); - test("for a branch order", () => { + test("for another branch", () => { expect(getCnpjInfo("12345678000276")).toEqual({ root: "12345678", - order: "0002", + branch: "0002", checkDigits: "76", format: "numeric", isInitialHeadquarters: false, @@ -37,7 +37,7 @@ describe("getCnpjInfo", () => { test("for a root with leading zeros", () => { expect(getCnpjInfo("00000001000136")).toEqual({ root: "00000001", - order: "0001", + branch: "0001", checkDigits: "36", format: "numeric", isInitialHeadquarters: true, @@ -47,7 +47,7 @@ describe("getCnpjInfo", () => { test("for a masked value", () => { expect(getCnpjInfo("12.345.678/0001-95")).toEqual({ root: "12345678", - order: "0001", + branch: "0001", checkDigits: "95", format: "numeric", isInitialHeadquarters: true, @@ -57,7 +57,7 @@ describe("getCnpjInfo", () => { test("for a value with a whitespace mask and surrounding whitespace", () => { expect(getCnpjInfo(" 12 345 678 0002 76 ")).toEqual({ root: "12345678", - order: "0002", + branch: "0002", checkDigits: "76", format: "numeric", isInitialHeadquarters: false, @@ -67,7 +67,7 @@ describe("getCnpjInfo", () => { test("under version 2, which reads both formats", () => { expect(getCnpjInfo("12.345.678/0001-95", { version: 2 })).toEqual({ root: "12345678", - order: "0001", + branch: "0001", checkDigits: "95", format: "numeric", isInitialHeadquarters: true, @@ -78,7 +78,7 @@ describe("getCnpjInfo", () => { // @ts-expect-error: intentionally invalid option expect(getCnpjInfo("12345678000195", { version: 3 })).toEqual({ root: "12345678", - order: "0001", + branch: "0001", checkDigits: "95", format: "numeric", isInitialHeadquarters: true, @@ -90,7 +90,7 @@ describe("getCnpjInfo", () => { test("for the example of the Receita Federal check digit manual", () => { expect(getCnpjInfo("12.ABC.345/01DE-35", { version: 2 })).toEqual({ root: "12ABC345", - order: "01DE", + branch: "01DE", checkDigits: "35", format: "alphanumeric", isInitialHeadquarters: false, @@ -100,37 +100,37 @@ describe("getCnpjInfo", () => { test("for a lowercase value, upper casing the fields", () => { expect(getCnpjInfo("12abc34501de35", { version: 2 })).toEqual({ root: "12ABC345", - order: "01DE", + branch: "01DE", checkDigits: "35", format: "alphanumeric", isInitialHeadquarters: false, }); }); - test("for an alphanumeric root with the headquarters order", () => { + test("for an alphanumeric root with the headquarters branch", () => { expect(getCnpjInfo("AB.12C.D34/0001-84", { version: 2 })).toEqual({ root: "AB12CD34", - order: "0001", + branch: "0001", checkDigits: "84", format: "alphanumeric", isInitialHeadquarters: true, }); }); - test("for an alphanumeric root and order (Receita Federal Q&A, question 23)", () => { + test("for an alphanumeric root and branch (Receita Federal Q&A, question 23)", () => { expect(getCnpjInfo("AA345678/000A-29", { version: 2 })).toEqual({ root: "AA345678", - order: "000A", + branch: "000A", checkDigits: "29", format: "alphanumeric", isInitialHeadquarters: false, }); }); - test("for a numeric root with an alphanumeric order (Receita Federal Q&A, question 23)", () => { + 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", - order: "000A", + branch: "000A", checkDigits: "08", format: "alphanumeric", isInitialHeadquarters: false, @@ -202,19 +202,19 @@ describe("getCnpjInfo", () => { const written = masked ? formatCnpj(cnpj, { version: 2 }) : cnpj; const parsed = getCnpjInfo(written.toLowerCase(), { version: 2 }); - expect(`${parsed?.root}${parsed?.order}${parsed?.checkDigits}`).toBe(cnpj); + expect(`${parsed?.root}${parsed?.branch}${parsed?.checkDigits}`).toBe(cnpj); expect(parsed?.format).toBe(/[A-Z]/.test(cnpj) ? "alphanumeric" : "numeric"); }), ); }); - test("should flag the order 0001 of a generated CNPJ and no other", () => { + 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?.order).toBe(String(branch).padStart(4, "0")); + expect(parsed?.branch).toBe(String(branch).padStart(4, "0")); expect(parsed?.isInitialHeadquarters).toBe(branch === 1); }), ); @@ -253,7 +253,7 @@ describe("getCnpjInfo types", () => { expectTypeOf().toEqualTypeOf<"numeric" | "alphanumeric">(); expectTypeOf().toEqualTypeOf<{ root: string; - order: string; + branch: string; checkDigits: string; format: CnpjFormat; isInitialHeadquarters: boolean; diff --git a/src/get-cnpj-info/get-cnpj-info.ts b/src/get-cnpj-info/get-cnpj-info.ts index 9420e1cf..42bc54fe 100644 --- a/src/get-cnpj-info/get-cnpj-info.ts +++ b/src/get-cnpj-info/get-cnpj-info.ts @@ -5,40 +5,40 @@ import { isValidCnpj, type IsValidCnpjOptions } from "../is-valid-cnpj/is-valid- /** Options of `getCnpjInfo`. */ export type GetCnpjInfoOptions = Pick; -/** How a CNPJ is written: `"numeric"` digits only, `"alphanumeric"` with a letter in the root or the order. */ +/** How a CNPJ is written: `"numeric"` digits only, `"alphanumeric"` with a letter in the root or the branch. */ export type CnpjFormat = "numeric" | "alphanumeric"; /** The fields `getCnpjInfo` reads out of a CNPJ. */ export type CnpjInfo = { /** The 8 character root (raiz), positions 1 to 8, shared by every establishment of the entity. */ root: string; - /** The 4 character order (número de ordem) of the establishment, positions 9 to 12, the ones `generateCnpj` takes as `branch`. */ - order: string; + /** The 4 character branch of the establishment, positions 9 to 12, called "número de ordem" by the Receita Federal. */ + branch: string; /** The 2 numeric check digits (dígitos verificadores), positions 13 and 14. */ checkDigits: string; - /** `"alphanumeric"` when the root or the order carries a letter, `"numeric"` otherwise. */ + /** `"alphanumeric"` when the root or the branch carries a letter, `"numeric"` otherwise. */ format: CnpjFormat; /** - * Whether the order is `0001`, the one the Receita Federal gives the headquarters (matriz) - * when the root is registered. A branch (filial) that later becomes the headquarters keeps - * its order, so only the Receita Federal registry tells the current headquarters. + * Whether the branch is `0001`, the one the Receita Federal gives the headquarters (matriz) + * when the root is registered. A filial that later becomes the headquarters keeps its + * number, so only the Receita Federal registry tells the current headquarters. */ isInitialHeadquarters: boolean; }; const ROOT_END = 8; -const ORDER_END = 12; +const BRANCH_END = 12; -const INITIAL_HEADQUARTERS_ORDER = "0001"; +const INITIAL_HEADQUARTERS_BRANCH = "0001"; /** * Parses a CNPJ (Cadastro Nacional da Pessoa Jurídica) into the fields the number encodes. * * Anexo XV of Instrução Normativa RFB nº 2.119/2022, added by Instrução Normativa RFB - * nº 2.229/2024, lays the 14 positions out as 8 (root, raiz, the entity) + 4 (order, número de + * nº 2.229/2024, lays the 14 positions out as 8 (root, raiz, the entity) + 4 (branch, número de * ordem, the establishment) + 2 (check digits, always numeric). In the alphanumeric format, - * assigned to new registrations from July 2026, the root and the order take the digits `0` to + * assigned to new registrations from July 2026, the root and the branch take the digits `0` to * `9` and the upper case letters `A` to `Z`, and either of them may still come out all numeric. * * Accepts the same input forms and reads `options.version` the same way as `isValidCnpj`: `1` @@ -47,9 +47,9 @@ const INITIAL_HEADQUARTERS_ORDER = "0001"; * arguments, so an alphanumeric CNPJ read under version `1` is `null`. The fields of an * alphanumeric CNPJ are returned upper cased. * - * The order `0001` marks the headquarters (matriz) only at registration: the Receita Federal - * Q&A (question 25) states that a branch (filial) can become the headquarters while keeping - * its order, hence `isInitialHeadquarters` instead of a definitive headquarters flag. + * The branch `0001` marks the headquarters (matriz) only at registration: the Receita Federal + * Q&A (question 25) states that a filial can become the headquarters while keeping its + * número de ordem, hence `isInitialHeadquarters` instead of a definitive headquarters flag. * * @param {string} value - The CNPJ to be parsed. * @param {GetCnpjInfoOptions} [options] - Optional options. @@ -62,7 +62,7 @@ const INITIAL_HEADQUARTERS_ORDER = "0001"; * getCnpjInfo("12.345.678/0001-95"); * // { * // root: "12345678", - * // order: "0001", + * // branch: "0001", * // checkDigits: "95", * // format: "numeric", * // isInitialHeadquarters: true, @@ -71,7 +71,7 @@ const INITIAL_HEADQUARTERS_ORDER = "0001"; * getCnpjInfo("12.abc.345/01de-35", { version: 2 }); * // { * // root: "12ABC345", - * // order: "01DE", + * // branch: "01DE", * // checkDigits: "35", * // format: "alphanumeric", * // isInitialHeadquarters: false, @@ -83,10 +83,10 @@ const INITIAL_HEADQUARTERS_ORDER = "0001"; * * @see Official: http://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=141102 * Instrução Normativa RFB nº 2.229/2024, whose Anexo Único is the Anexo XV of IN RFB - * nº 2.119/2022: positions 1 to 8 root, 9 to 12 order, 13 and 14 check digits. + * nº 2.119/2022: positions 1 to 8 root, 9 to 12 número de ordem (branch), 13 and 14 check digits. * @see Official: https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/perguntas-e-respostas/cnpj/cnpj-alfanumerico.pdf - * Receita Federal Q&A on the alphanumeric CNPJ: questions 21 and 23 (root and order), 25 (the - * order `0001` and the headquarters) and the `AA345678/000A-29` and `12.345.678/000A-08` + * Receita Federal Q&A on the alphanumeric CNPJ: questions 21 and 23 (root and branch), 25 (the + * branch `0001` and the headquarters) and the `AA345678/000A-29` and `12.345.678/000A-08` * examples. * @see Official: https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/documentos-tecnicos/cnpj/manual-dv-cnpj.pdf * Check digit manual, source of the `12.ABC.345/01DE-35` example. @@ -98,13 +98,13 @@ export const getCnpjInfo = (value: string, options?: GetCnpjInfoOptions): CnpjIn if (!isValidCnpj(value, options)) return null; const cnpj = sanitizeToAlphanumeric(value); - const order = cnpj.slice(ROOT_END, ORDER_END); + const branch = cnpj.slice(ROOT_END, BRANCH_END); return { root: cnpj.slice(0, ROOT_END), - order, - checkDigits: cnpj.slice(ORDER_END), + branch, + checkDigits: cnpj.slice(BRANCH_END), format: CNPJ_LETTER_REGEX.test(cnpj) ? "alphanumeric" : "numeric", - isInitialHeadquarters: order === INITIAL_HEADQUARTERS_ORDER, + isInitialHeadquarters: branch === INITIAL_HEADQUARTERS_BRANCH, }; }; From 214ce29de471e96683652769c95752e1075e5ad8 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Tue, 22 Sep 2026 01:42:56 -0300 Subject: [PATCH 4/4] refactor(get-cnpj-info): drop the format field from CnpjInfo The field only said whether the root or the branch carried a letter, which the caller can read off root and branch it already has. Adding a field to a returned object later is not a breaking change while removing one is, so it stays out until someone asks for it, and CnpjFormat leaves the public surface with it. The version option is untouched: getCnpjInfo(value, { version }) still decides which formats are read, and an alphanumeric CNPJ under version 1 is still null. CNPJ_LETTER_REGEX stays in _internals/constants/cnpj.ts, next to CNPJ_LENGTH which parse-cnpj is the only reader of, but its doc no longer claims getCnpjInfo as a second user. --- docs/pt-br/utilities.md | 3 --- docs/utilities.md | 3 --- reports/api/brazilian-utils.api.md | 4 ---- src/_internals/constants/cnpj.ts | 2 +- src/get-cnpj-info/get-cnpj-info.test.ts | 22 +--------------------- src/get-cnpj-info/get-cnpj-info.ts | 9 --------- src/index.test.ts | 2 -- src/index.ts | 7 +------ 8 files changed, 3 insertions(+), 49 deletions(-) diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 7014a7ae..5736ff28 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -176,7 +176,6 @@ Interpreta um CNPJ nos campos que o número codifica. Aceita as mesmas formas de - **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. -- `format` é `'alphanumeric'` quando a raiz ou o número de ordem têm uma letra e `'numeric'` caso contrário (tipado como `CnpjFormat`). - `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. @@ -188,7 +187,6 @@ getCnpjInfo('12.345.678/0001-95'); // root: '12345678', // branch: '0001', // checkDigits: '95', -// format: 'numeric', // isInitialHeadquarters: true // } @@ -197,7 +195,6 @@ getCnpjInfo('12.abc.345/01de-35', { version: 2 }); // root: '12ABC345', // branch: '01DE', // checkDigits: '35', -// format: 'alphanumeric', // isInitialHeadquarters: false // } diff --git a/docs/utilities.md b/docs/utilities.md index be443cf6..dc8b8b3e 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -176,7 +176,6 @@ Parse a CNPJ into the fields the number encodes. Accepts the same input forms as - **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. -- `format` is `'alphanumeric'` when the root or the branch carries a letter and `'numeric'` otherwise (typed as `CnpjFormat`). - `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. @@ -188,7 +187,6 @@ getCnpjInfo('12.345.678/0001-95'); // root: '12345678', // branch: '0001', // checkDigits: '95', -// format: 'numeric', // isInitialHeadquarters: true // } @@ -197,7 +195,6 @@ getCnpjInfo('12.abc.345/01de-35', { version: 2 }); // root: '12ABC345', // branch: '01DE', // checkDigits: '35', -// format: 'alphanumeric', // isInitialHeadquarters: false // } diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md index 3319c9b7..8539554b 100644 --- a/reports/api/brazilian-utils.api.md +++ b/reports/api/brazilian-utils.api.md @@ -114,15 +114,11 @@ export type Cnae = { description: string; }; -// @public -export type CnpjFormat = "numeric" | "alphanumeric"; - // @public export type CnpjInfo = { root: string; branch: string; checkDigits: string; - format: CnpjFormat; isInitialHeadquarters: boolean; }; diff --git a/src/_internals/constants/cnpj.ts b/src/_internals/constants/cnpj.ts index 8312f465..93818db8 100644 --- a/src/_internals/constants/cnpj.ts +++ b/src/_internals/constants/cnpj.ts @@ -8,6 +8,6 @@ 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. Shared by `isValidCnpj` and `getCnpjInfo`. + * upper cased before the test. */ export const CNPJ_LETTER_REGEX = /[A-Z]/; diff --git a/src/get-cnpj-info/get-cnpj-info.test.ts b/src/get-cnpj-info/get-cnpj-info.test.ts index ede4fb80..c330c67a 100644 --- a/src/get-cnpj-info/get-cnpj-info.test.ts +++ b/src/get-cnpj-info/get-cnpj-info.test.ts @@ -5,12 +5,7 @@ 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 CnpjFormat, - type CnpjInfo, - getCnpjInfo, - type GetCnpjInfoOptions, -} from "./get-cnpj-info"; +import { type CnpjInfo, getCnpjInfo, type GetCnpjInfoOptions } from "./get-cnpj-info"; describe("getCnpjInfo", () => { describe("should return the parsed numeric CNPJ", () => { @@ -19,7 +14,6 @@ describe("getCnpjInfo", () => { root: "12345678", branch: "0001", checkDigits: "95", - format: "numeric", isInitialHeadquarters: true, }); }); @@ -29,7 +23,6 @@ describe("getCnpjInfo", () => { root: "12345678", branch: "0002", checkDigits: "76", - format: "numeric", isInitialHeadquarters: false, }); }); @@ -39,7 +32,6 @@ describe("getCnpjInfo", () => { root: "00000001", branch: "0001", checkDigits: "36", - format: "numeric", isInitialHeadquarters: true, }); }); @@ -49,7 +41,6 @@ describe("getCnpjInfo", () => { root: "12345678", branch: "0001", checkDigits: "95", - format: "numeric", isInitialHeadquarters: true, }); }); @@ -59,7 +50,6 @@ describe("getCnpjInfo", () => { root: "12345678", branch: "0002", checkDigits: "76", - format: "numeric", isInitialHeadquarters: false, }); }); @@ -69,7 +59,6 @@ describe("getCnpjInfo", () => { root: "12345678", branch: "0001", checkDigits: "95", - format: "numeric", isInitialHeadquarters: true, }); }); @@ -80,7 +69,6 @@ describe("getCnpjInfo", () => { root: "12345678", branch: "0001", checkDigits: "95", - format: "numeric", isInitialHeadquarters: true, }); }); @@ -92,7 +80,6 @@ describe("getCnpjInfo", () => { root: "12ABC345", branch: "01DE", checkDigits: "35", - format: "alphanumeric", isInitialHeadquarters: false, }); }); @@ -102,7 +89,6 @@ describe("getCnpjInfo", () => { root: "12ABC345", branch: "01DE", checkDigits: "35", - format: "alphanumeric", isInitialHeadquarters: false, }); }); @@ -112,7 +98,6 @@ describe("getCnpjInfo", () => { root: "AB12CD34", branch: "0001", checkDigits: "84", - format: "alphanumeric", isInitialHeadquarters: true, }); }); @@ -122,7 +107,6 @@ describe("getCnpjInfo", () => { root: "AA345678", branch: "000A", checkDigits: "29", - format: "alphanumeric", isInitialHeadquarters: false, }); }); @@ -132,7 +116,6 @@ describe("getCnpjInfo", () => { root: "12345678", branch: "000A", checkDigits: "08", - format: "alphanumeric", isInitialHeadquarters: false, }); }); @@ -203,7 +186,6 @@ describe("getCnpjInfo", () => { const parsed = getCnpjInfo(written.toLowerCase(), { version: 2 }); expect(`${parsed?.root}${parsed?.branch}${parsed?.checkDigits}`).toBe(cnpj); - expect(parsed?.format).toBe(/[A-Z]/.test(cnpj) ? "alphanumeric" : "numeric"); }), ); }); @@ -250,12 +232,10 @@ describe("getCnpjInfo types", () => { expectTypeOf(getCnpjInfo).parameter(1).toEqualTypeOf(); expectTypeOf(getCnpjInfo).returns.toEqualTypeOf(); expectTypeOf().toEqualTypeOf<{ version?: 1 | 2 }>(); - expectTypeOf().toEqualTypeOf<"numeric" | "alphanumeric">(); expectTypeOf().toEqualTypeOf<{ root: string; branch: string; checkDigits: string; - format: CnpjFormat; isInitialHeadquarters: boolean; }>(); }); diff --git a/src/get-cnpj-info/get-cnpj-info.ts b/src/get-cnpj-info/get-cnpj-info.ts index 42bc54fe..520a65d6 100644 --- a/src/get-cnpj-info/get-cnpj-info.ts +++ b/src/get-cnpj-info/get-cnpj-info.ts @@ -1,13 +1,9 @@ -import { CNPJ_LETTER_REGEX } from "../_internals/constants/cnpj"; import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { isValidCnpj, type IsValidCnpjOptions } from "../is-valid-cnpj/is-valid-cnpj"; /** Options of `getCnpjInfo`. */ export type GetCnpjInfoOptions = Pick; -/** How a CNPJ is written: `"numeric"` digits only, `"alphanumeric"` with a letter in the root or the branch. */ -export type CnpjFormat = "numeric" | "alphanumeric"; - /** The fields `getCnpjInfo` reads out of a CNPJ. */ export type CnpjInfo = { /** The 8 character root (raiz), positions 1 to 8, shared by every establishment of the entity. */ @@ -16,8 +12,6 @@ export type CnpjInfo = { branch: string; /** The 2 numeric check digits (dígitos verificadores), positions 13 and 14. */ checkDigits: string; - /** `"alphanumeric"` when the root or the branch carries a letter, `"numeric"` otherwise. */ - format: CnpjFormat; /** * Whether the branch is `0001`, the one the Receita Federal gives the headquarters (matriz) * when the root is registered. A filial that later becomes the headquarters keeps its @@ -64,7 +58,6 @@ const INITIAL_HEADQUARTERS_BRANCH = "0001"; * // root: "12345678", * // branch: "0001", * // checkDigits: "95", - * // format: "numeric", * // isInitialHeadquarters: true, * // } * @@ -73,7 +66,6 @@ const INITIAL_HEADQUARTERS_BRANCH = "0001"; * // root: "12ABC345", * // branch: "01DE", * // checkDigits: "35", - * // format: "alphanumeric", * // isInitialHeadquarters: false, * // } * @@ -104,7 +96,6 @@ export const getCnpjInfo = (value: string, options?: GetCnpjInfoOptions): CnpjIn root: cnpj.slice(0, ROOT_END), branch, checkDigits: cnpj.slice(BRANCH_END), - format: CNPJ_LETTER_REGEX.test(cnpj) ? "alphanumeric" : "numeric", isInitialHeadquarters: branch === INITIAL_HEADQUARTERS_BRANCH, }; }; diff --git a/src/index.test.ts b/src/index.test.ts index 8e64effc..deb4cf5b 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -13,7 +13,6 @@ import { type CertidaoType, type Cfop, type Cnae, - type CnpjFormat, type CnpjInfo, type ConvertDateToWordsOptions, type ConvertNumberToWordsOptions, @@ -312,7 +311,6 @@ describe("Public API", () => { CertidaoType: CertidaoType; Cfop: Cfop; Cnae: Cnae; - CnpjFormat: CnpjFormat; CnpjInfo: CnpjInfo; ConvertDateToWordsOptions: ConvertDateToWordsOptions; ConvertNumberToWordsOptions: ConvertNumberToWordsOptions; diff --git a/src/index.ts b/src/index.ts index 4a088b1a..3267ba77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,12 +116,7 @@ export { export { type Cfop, getCfop } from "./get-cfop/get-cfop"; export { getCities } from "./get-cities/get-cities"; export { type Cnae, getCnae } from "./get-cnae/get-cnae"; -export { - type CnpjFormat, - type CnpjInfo, - getCnpjInfo, - type GetCnpjInfoOptions, -} from "./get-cnpj-info/get-cnpj-info"; +export { type CnpjInfo, getCnpjInfo, type GetCnpjInfoOptions } from "./get-cnpj-info/get-cnpj-info"; export { type CpfInfo, getCpfInfo } from "./get-cpf-info/get-cpf-info"; export { getFormatLicensePlate,