diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 75d119ef..5736ff28 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -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 diff --git a/docs/utilities.md b/docs/utilities.md index b10fc7bc..dc8b8b3e 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -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 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..8539554b 100644 --- a/reports/api/brazilian-utils.api.md +++ b/reports/api/brazilian-utils.api.md @@ -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; @@ -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; + // @public export const getCpfInfo: (value: string) => CpfInfo | null; diff --git a/src/_internals/constants/cnpj.ts b/src/_internals/constants/cnpj.ts index 3b1de61c..93818db8 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 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]/; 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..c330c67a --- /dev/null +++ b/src/get-cnpj-info/get-cnpj-info.test.ts @@ -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(); + expectTypeOf(getCnpjInfo).parameter(1).toEqualTypeOf(); + expectTypeOf(getCnpjInfo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ version?: 1 | 2 }>(); + expectTypeOf().toEqualTypeOf<{ + root: string; + branch: string; + checkDigits: string; + 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..520a65d6 --- /dev/null +++ b/src/get-cnpj-info/get-cnpj-info.ts @@ -0,0 +1,101 @@ +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; + +/** 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 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; + /** + * 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 BRANCH_END = 12; + +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 (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 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` + * (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 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. + * @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", + * // 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) + * ``` + * + * @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 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 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. + * @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 branch = cnpj.slice(ROOT_END, BRANCH_END); + + return { + root: cnpj.slice(0, ROOT_END), + branch, + checkDigits: cnpj.slice(BRANCH_END), + isInitialHeadquarters: branch === INITIAL_HEADQUARTERS_BRANCH, + }; +}; diff --git a/src/index.test.ts b/src/index.test.ts index a4e4cf4f..deb4cf5b 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -13,6 +13,7 @@ import { type CertidaoType, type Cfop, type Cnae, + type CnpjInfo, type ConvertDateToWordsOptions, type ConvertNumberToWordsOptions, type CpfInfo, @@ -46,6 +47,7 @@ import { type GetBoletoInfoOptions, type GetCepInfoByAddressOptions, type GetCepInfoByAddressParams, + type GetCnpjInfoOptions, type GetHolidaysOptions, type GetHolidaysParams, type GetLegalNaturesByCategoryOptions, @@ -175,6 +177,7 @@ const PUBLIC = [ "getCfop", "getCities", "getCnae", + "getCnpjInfo", "getCpfInfo", "getFormatLicensePlate", "getHolidays", @@ -308,6 +311,7 @@ describe("Public API", () => { CertidaoType: CertidaoType; Cfop: Cfop; Cnae: Cnae; + CnpjInfo: CnpjInfo; ConvertDateToWordsOptions: ConvertDateToWordsOptions; ConvertNumberToWordsOptions: ConvertNumberToWordsOptions; CpfInfo: CpfInfo; @@ -341,6 +345,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..3267ba77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,6 +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 CnpjInfo, getCnpjInfo, type GetCnpjInfoOptions } from "./get-cnpj-info/get-cnpj-info"; export { type CpfInfo, getCpfInfo } from "./get-cpf-info/get-cpf-info"; export { getFormatLicensePlate, 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); } }