From 8ddffc7b4d8e121579849d2163968fb32646b69d Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:53 -0300 Subject: [PATCH 01/22] feat(nfe-key): add formatNfeKey, isValidNfeKey and parseNfeKey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New util family for the 44-digit NFe (Nota Fiscal Eletrônica) access key, validated against the IBGE UF codes. --- src/_internals/constants/ibge-uf-codes.ts | 39 +++++++ src/_internals/constants/nfe-key.ts | 2 + src/format-nfe-key/constants.ts | 6 + src/format-nfe-key/format-nfe-key.test.ts | 46 ++++++++ src/format-nfe-key/format-nfe-key.ts | 23 ++++ src/is-valid-nfe-key/constants.ts | 2 + src/is-valid-nfe-key/is-valid-nfe-key.test.ts | 106 ++++++++++++++++++ src/is-valid-nfe-key/is-valid-nfe-key.ts | 66 +++++++++++ src/parse-nfe-key/parse-nfe-key.test.ts | 87 ++++++++++++++ src/parse-nfe-key/parse-nfe-key.ts | 84 ++++++++++++++ 10 files changed, 461 insertions(+) create mode 100644 src/_internals/constants/ibge-uf-codes.ts create mode 100644 src/_internals/constants/nfe-key.ts create mode 100644 src/format-nfe-key/constants.ts create mode 100644 src/format-nfe-key/format-nfe-key.test.ts create mode 100644 src/format-nfe-key/format-nfe-key.ts create mode 100644 src/is-valid-nfe-key/constants.ts create mode 100644 src/is-valid-nfe-key/is-valid-nfe-key.test.ts create mode 100644 src/is-valid-nfe-key/is-valid-nfe-key.ts create mode 100644 src/parse-nfe-key/parse-nfe-key.test.ts create mode 100644 src/parse-nfe-key/parse-nfe-key.ts diff --git a/src/_internals/constants/ibge-uf-codes.ts b/src/_internals/constants/ibge-uf-codes.ts new file mode 100644 index 00000000..99caf18f --- /dev/null +++ b/src/_internals/constants/ibge-uf-codes.ts @@ -0,0 +1,39 @@ +import type { StateCode } from "./states"; + +/** + * IBGE code of the Federative Unit ("cUF"), keyed by the 2 digit code found in the first + * field of every DF-e access key (chave de acesso): NF-e (modelo 55), NFC-e (modelo 65), + * CT-e (modelo 57) and MDF-e (modelo 58). + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * (Manual de Orientação do Contribuinte, "chave de acesso" / "Tabela do IBGE"). + */ +export const IBGE_UF_CODES: Record = { + "11": "RO", + "12": "AC", + "13": "AM", + "14": "RR", + "15": "PA", + "16": "AP", + "17": "TO", + "21": "MA", + "22": "PI", + "23": "CE", + "24": "RN", + "25": "PB", + "26": "PE", + "27": "AL", + "28": "SE", + "29": "BA", + "31": "MG", + "32": "ES", + "33": "RJ", + "35": "SP", + "41": "PR", + "42": "SC", + "43": "RS", + "50": "MS", + "51": "MT", + "52": "GO", + "53": "DF", +}; diff --git a/src/_internals/constants/nfe-key.ts b/src/_internals/constants/nfe-key.ts new file mode 100644 index 00000000..d9b06eb8 --- /dev/null +++ b/src/_internals/constants/nfe-key.ts @@ -0,0 +1,2 @@ +/** Digits of a DF-e (NF-e, NFC-e, CT-e or MDF-e) access key (chave de acesso). */ +export const NFE_KEY_LENGTH = 44; diff --git a/src/format-nfe-key/constants.ts b/src/format-nfe-key/constants.ts new file mode 100644 index 00000000..2d5f8692 --- /dev/null +++ b/src/format-nfe-key/constants.ts @@ -0,0 +1,6 @@ +/** + * 11 groups of 4 digits, the common display form printed on the DANFE. Spelled out + * instead of built with `Array(11).fill(...).join(...)`: a top-level call cannot be + * proven pure by consumer bundlers and would pin this module into their output. + */ +export const PATTERN = "0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000"; diff --git a/src/format-nfe-key/format-nfe-key.test.ts b/src/format-nfe-key/format-nfe-key.test.ts new file mode 100644 index 00000000..d320e067 --- /dev/null +++ b/src/format-nfe-key/format-nfe-key.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { formatNfeKey } from "./format-nfe-key"; + +const KEY = "35170458716523000119550010000000121000123458"; +const FORMATTED = "3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458"; + +describe("formatNfeKey", () => { + test("should format a full access key into groups of 4 digits", () => { + expect(formatNfeKey(KEY)).toBe(FORMATTED); + }); + + test("should format partial values as far as they go", () => { + expect(formatNfeKey("")).toBe(""); + expect(formatNfeKey("1")).toBe("1"); + expect(formatNfeKey("123")).toBe("123"); + expect(formatNfeKey("1234")).toBe("1234"); + expect(formatNfeKey("12345")).toBe("1234 5"); + }); + + test("should NOT add digits after the access key length (44)", () => { + expect(formatNfeKey(`${KEY}999999`)).toBe(FORMATTED); + }); + + test("should remove all non numeric characters, including the NFe prefix", () => { + expect(formatNfeKey(`NFe${KEY}`)).toBe(FORMATTED); + expect(formatNfeKey(FORMATTED)).toBe(FORMATTED); + }); + + test("should return an empty string for nullish input", () => { + // @ts-expect-error + expect(formatNfeKey(null)).toBe(""); + // @ts-expect-error + expect(formatNfeKey(undefined)).toBe(""); + }); + + test("should not throw for other bad input types", () => { + // @ts-expect-error + expect(formatNfeKey(123)).toBe("123"); + // @ts-expect-error + expect(formatNfeKey({})).toBe(""); + // @ts-expect-error + expect(formatNfeKey([])).toBe(""); + // @ts-expect-error + expect(formatNfeKey(true)).toBe(""); + }); +}); diff --git a/src/format-nfe-key/format-nfe-key.ts b/src/format-nfe-key/format-nfe-key.ts new file mode 100644 index 00000000..78c08078 --- /dev/null +++ b/src/format-nfe-key/format-nfe-key.ts @@ -0,0 +1,23 @@ +import { format } from "../_internals/format/format"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { PATTERN } from "./constants"; + +/** + * Formats a DF-e (NF-e, NFC-e, CT-e or MDF-e) access key (chave de acesso) into groups of 4 + * digits separated by spaces, the common display form printed on the DANFE. + * + * @param {string} value - The access key value to be formatted. + * @returns {string} The formatted access key, e.g. "3520 0612 3456 ...". + * + * @example + * ```typescript + * formatNfeKey("35170458716523000119550010000000121000123458"); + * // "3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458" + * ``` + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso". + */ +export const formatNfeKey = (value: string): string => + isNullish(value) ? "" : format({ value: sanitizeToDigits(value), pattern: PATTERN }); diff --git a/src/is-valid-nfe-key/constants.ts b/src/is-valid-nfe-key/constants.ts new file mode 100644 index 00000000..5d3436f7 --- /dev/null +++ b/src/is-valid-nfe-key/constants.ts @@ -0,0 +1,2 @@ +/** Valid `mod` (modelo do documento) values shared by every DF-e access key. */ +export const VALID_MODELS = ["55", "57", "58", "65"] as const; diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts new file mode 100644 index 00000000..c903ae69 --- /dev/null +++ b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidNfeKey } from "./is-valid-nfe-key"; + +const VALID_A = "35120859597245000190550000000095831710040056"; +const VALID_B = "35170458716523000119550010000000121000123458"; +const VALID_C = "35170358716523000119550010000000301000000300"; +const VALID_D = "43160472202112000136550000000010571048440722"; +const INVALID_TYPE = "42100484684182000157550010000000020108042108"; + +describe("isValidNfeKey", () => { + describe("should return true", () => { + test("for a real NF-e access key without a mask, the br-validate-dfe-access-key README/tests example (SP)", () => { + expect(isValidNfeKey(VALID_A)).toBe(true); + }); + + test("for a real NF-e access key without a mask, the NFePHP `Keys::build` doc example (SP)", () => { + expect(isValidNfeKey(VALID_B)).toBe(true); + }); + + test("for a real NF-e access key without a mask, the NFePHP `Keys::isValid` doc example (SP)", () => { + expect(isValidNfeKey(VALID_C)).toBe(true); + }); + + test("for a real NF-e access key without a mask, the NFePHP sped-cte `$infNFe->chave` example (RS, NF-e referenced by a CT-e)", () => { + expect(isValidNfeKey(VALID_D)).toBe(true); + }); + + test("when it has the NFe prefix found in the XML Id attribute", () => { + expect(isValidNfeKey(`NFe${VALID_B}`)).toBe(true); + }); + + test("when it is grouped in spaces of 4 digits", () => { + expect(isValidNfeKey("3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458")).toBe(true); + }); + + test("when it has the NFe prefix and a whitespace mask combined", () => { + expect(isValidNfeKey("NFe 3512 0859 5972 4500 0190 5500 0000 0095 8317 1004 0056")).toBe( + true, + ); + }); + }); + + describe("should return false", () => { + test("when it is null", () => { + // @ts-expect-error + expect(isValidNfeKey(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidNfeKey(undefined)).toBe(false); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(isValidNfeKey(123)).toBe(false); + }); + + test("when it is a boolean", () => { + // @ts-expect-error + expect(isValidNfeKey(true)).toBe(false); + }); + + test("when it is an object or an array", () => { + // @ts-expect-error + expect(isValidNfeKey({})).toBe(false); + // @ts-expect-error + expect(isValidNfeKey([])).toBe(false); + }); + + test("when it is an empty string", () => { + expect(isValidNfeKey("")).toBe(false); + }); + + test("when it has letters mixed with the digits", () => { + expect(isValidNfeKey(`foo${VALID_B}bar`)).toBe(false); + }); + + test("when it does not have 44 digits", () => { + expect(isValidNfeKey(VALID_B.slice(0, 43))).toBe(false); + expect(isValidNfeKey(`${VALID_B}9`)).toBe(false); + }); + + test("when the cUF is not a valid IBGE UF code", () => { + expect(isValidNfeKey(`00${VALID_B.slice(2)}`)).toBe(false); + }); + + test("when the mod is not 55, 57, 58 or 65", () => { + expect(isValidNfeKey(`${VALID_B.slice(0, 20)}99${VALID_B.slice(22)}`)).toBe(false); + }); + + test("when the month is not between 01 and 12", () => { + expect(isValidNfeKey(`${VALID_B.slice(0, 4)}13${VALID_B.slice(6)}`)).toBe(false); + expect(isValidNfeKey(`${VALID_B.slice(0, 4)}00${VALID_B.slice(6)}`)).toBe(false); + }); + + test("when tpEmis is not between 1 and 9, using the br-validate-dfe-access-key doc example with a valid check digit but tpEmis '0'", () => { + expect(isValidNfeKey(INVALID_TYPE)).toBe(false); + }); + + test("when the check digit does not match", () => { + const brokenDv = `${VALID_B.slice(0, 43)}${VALID_B.at(-1) === "8" ? "7" : "8"}`; + expect(isValidNfeKey(brokenDv)).toBe(false); + }); + }); +}); diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.ts b/src/is-valid-nfe-key/is-valid-nfe-key.ts new file mode 100644 index 00000000..bae7a43d --- /dev/null +++ b/src/is-valid-nfe-key/is-valid-nfe-key.ts @@ -0,0 +1,66 @@ +import { IBGE_UF_CODES } from "../_internals/constants/ibge-uf-codes"; +import { NFE_KEY_LENGTH } from "../_internals/constants/nfe-key"; +import { mod11 } from "../_internals/mod11/mod11"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { VALID_MODELS } from "./constants"; + +const MODELS: readonly string[] = VALID_MODELS; + +const FORMAT_REGEX = /^(?:nfe)?[\d\s]+$/i; + +/** + * Validates a DF-e (Documento Fiscal eletrônico) access key (chave de acesso). + * + * Covers every document that shares the same 44 digit layout: NF-e (modelo 55), NFC-e + * (modelo 65), CT-e (modelo 57) and MDF-e (modelo 58). Accepts whitespace between digit + * groups (the common display mask) and the `NFe` prefix found in the `Id` attribute of the + * document's XML (e.g. `Id="NFe3517...`), which is stripped before validation. + * + * The key is `cUF(2) AAMM(4) CNPJ/CPF(14) mod(2) serie(3) nNF(9) tpEmis(1) cNF(8) cDV(1)`. + * The check digit (`cDV`) is a modulus 11 over the first 43 digits, weights 2-9 cycling from + * the right, where a remainder of 0 or 1 maps to check digit 0. + * + * @param {string} value - The access key value to be validated. + * @returns {boolean} True if the access key is valid, false otherwise. + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso". + * @see Based on: https://github.com/nfephp-org/sped-common/blob/master/src/Keys.php + * NFePHP `Keys::build`/`Keys::isValid` reference implementation. + * @see Based on: https://github.com/vmarchesin/br-validate-dfe-access-key + * Second reference implementation and source of additional test vectors. + * + * @example + * ```typescript + * isValidNfeKey("35170458716523000119550010000000121000123458"); // true (NF-e, SP) + * isValidNfeKey("NFe35170458716523000119550010000000121000123458"); // true (XML Id prefix) + * isValidNfeKey("3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458"); // true (masked) + * isValidNfeKey("99170458716523000119550010000000121000123458"); // false (invalid cUF) + * isValidNfeKey("35170458716523000119010010000000121000123450"); // false (invalid mod) + * ``` + */ +export const isValidNfeKey = (value: string): boolean => { + if (typeof value !== "string" || value === "") return false; + + if (!FORMAT_REGEX.test(value.trim())) return false; + + const digits = sanitizeToDigits(value); + + if (digits.length !== NFE_KEY_LENGTH) return false; + + if (!(digits.slice(0, 2) in IBGE_UF_CODES)) return false; + + const month = Number(digits.slice(4, 6)); + + if (month < 1 || month > 12) return false; + + if (!MODELS.includes(digits.slice(20, 22))) return false; + + const emissionType = Number(digits[34]); + + if (emissionType < 1 || emissionType > 9) return false; + + const checkDigit = Number(digits[43]); + + return mod11(digits.slice(0, 43), { variant: "arrecadacao" }) === checkDigit; +}; diff --git a/src/parse-nfe-key/parse-nfe-key.test.ts b/src/parse-nfe-key/parse-nfe-key.test.ts new file mode 100644 index 00000000..226bdc47 --- /dev/null +++ b/src/parse-nfe-key/parse-nfe-key.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { parseNfeKey } from "./parse-nfe-key"; + +const KEY_SP = "35170458716523000119550010000000121000123458"; +const KEY_RS = "43160472202112000136550000000010571048440722"; +const KEY_CPF_PADDED = "35170400040364478829550010000000121000123457"; + +describe("parseNfeKey", () => { + describe("should return null", () => { + test("when it is null", () => { + // @ts-expect-error + expect(parseNfeKey(null)).toBeNull(); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(parseNfeKey(undefined)).toBeNull(); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(parseNfeKey(123)).toBeNull(); + }); + + test("when it is an empty string", () => { + expect(parseNfeKey("")).toBeNull(); + }); + + test("when the check digit does not match", () => { + expect(parseNfeKey(`${KEY_SP.slice(0, 43)}9`)).toBeNull(); + }); + + test("when the access key is otherwise invalid", () => { + expect(parseNfeKey("not-a-key")).toBeNull(); + }); + }); + + describe("should return the parsed access key", () => { + test("for a NF-e access key (SP), the NFePHP `Keys::build` doc example also used in is-valid-nfe-key.test.ts", () => { + expect(parseNfeKey(KEY_SP)).toEqual({ + state: "SP", + year: 2017, + month: 4, + taxId: "58716523000119", + model: "55", + series: 1, + number: 12, + emissionType: 1, + code: "00012345", + checkDigit: 8, + }); + }); + + test("for a NF-e access key (RS), the NFePHP sped-cte `$infNFe->chave` example (NF-e referenced by a CT-e)", () => { + expect(parseNfeKey(KEY_RS)).toEqual({ + state: "RS", + year: 2016, + month: 4, + taxId: "72202112000136", + model: "55", + series: 0, + number: 1057, + emissionType: 1, + code: "04844072", + checkDigit: 2, + }); + }); + + test("accepting the NFe XML prefix and a whitespace mask", () => { + expect(parseNfeKey(`NFe${KEY_SP}`)?.taxId).toBe("58716523000119"); + expect(parseNfeKey("3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458")?.number).toBe( + 12, + ); + }); + + test("keeping the left zero padding of a CPF issuer, using a synthetic key with an 11-digit CPF left-padded to 14 digits in the tax id field and the check digit recalculated", () => { + expect(parseNfeKey(KEY_CPF_PADDED)?.taxId).toBe("00040364478829"); + expect(parseNfeKey(KEY_CPF_PADDED)?.taxId).toHaveLength(14); + }); + + test("for every other DF-e model (CT-e, MDF-e, NFC-e), same shape as the SP key with the model field changed and the check digit recalculated", () => { + expect(parseNfeKey("35170458716523000119570010000000121000123455")?.model).toBe("57"); + expect(parseNfeKey("35170458716523000119580010000000121000123459")?.model).toBe("58"); + expect(parseNfeKey("35170458716523000119650010000000121000123450")?.model).toBe("65"); + }); + }); +}); diff --git a/src/parse-nfe-key/parse-nfe-key.ts b/src/parse-nfe-key/parse-nfe-key.ts new file mode 100644 index 00000000..9010f5d4 --- /dev/null +++ b/src/parse-nfe-key/parse-nfe-key.ts @@ -0,0 +1,84 @@ +import { IBGE_UF_CODES } from "../_internals/constants/ibge-uf-codes"; +import { NFE_KEY_LENGTH } from "../_internals/constants/nfe-key"; +import type { StateCode } from "../_internals/constants/states"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { isValidNfeKey } from "../is-valid-nfe-key/is-valid-nfe-key"; + +export type NfeKeyModel = "55" | "57" | "58" | "65"; + +const isNfeKeyModel = (value: string): value is NfeKeyModel => + value === "55" || value === "57" || value === "58" || value === "65"; + +export type NfeKey = { + /** Two letter code of the issuing state, read from the IBGE UF code. */ + state: StateCode; + /** Four digit issue year. */ + year: number; + /** Issue month, 1 to 12. */ + month: number; + /** The 14 digit CNPJ (or zero padded CPF) of the issuer. */ + taxId: string; + /** Document model: "55" NF-e, "57" CT-e, "58" MDF-e, "65" NFC-e. */ + model: NfeKeyModel; + /** Document series, 0 to 999. */ + series: number; + /** Document number, 1 to 999999999. */ + number: number; + /** Emission type code (tpEmis), 1 to 9. */ + emissionType: number; + /** The 8 digit numeric code (cNF) drawn by the issuer. */ + code: string; + /** The modulo 11 check digit of the key. */ + checkDigit: number; +}; + +/** + * Parses a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) into its fields. + * + * Covers every document that shares the same 44 digit layout: NF-e (modelo 55), NFC-e + * (modelo 65), CT-e (modelo 57) and MDF-e (modelo 58). Accepts the same input forms as + * `isValidNfeKey` (whitespace mask, `NFe` XML `Id` prefix) and returns `null` when the key + * is not valid. + * + * @param {string} value - The access key value to be parsed. + * @returns {NfeKey | null} The parsed access key, or `null` when it is not valid. + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso". + * @see Based on: https://github.com/nfephp-org/sped-common/blob/master/src/Keys.php + * NFePHP `Keys::build` reference implementation, source of the SP and RS test vectors. + * @see Based on: https://github.com/vmarchesin/br-validate-dfe-access-key + * Second reference implementation. + * + * @example + * ```typescript + * parseNfeKey("35170458716523000119550010000000121000123458"); + * // { state: "SP", year: 2017, month: 4, taxId: "58716523000119", model: "55", + * // series: 1, number: 12, emissionType: 1, code: "00012345", checkDigit: 8 } + * + * parseNfeKey("invalid"); // null + * ``` + */ +export const parseNfeKey = (value: string): NfeKey | null => { + if (!isValidNfeKey(value)) return null; + + const digits = sanitizeToDigits(value).slice(0, NFE_KEY_LENGTH); + + const model = digits.slice(20, 22); + + /* v8 ignore next */ + if (!isNfeKeyModel(model)) return null; + + return { + state: IBGE_UF_CODES[digits.slice(0, 2)], + year: 2000 + Number(digits.slice(2, 4)), + month: Number(digits.slice(4, 6)), + taxId: digits.slice(6, 20), + model, + series: Number(digits.slice(22, 25)), + number: Number(digits.slice(25, 34)), + emissionType: Number(digits[34]), + code: digits.slice(35, 43), + checkDigit: Number(digits[43]), + }; +}; From 402c4fb3d783acfda62190b15d08b2d15c548aa4 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:53 -0300 Subject: [PATCH 02/22] feat(pix): add generatePixPayload, isValidPixPayload, isValidPixKey, parsePixPayload and parsePixKey New util family for Pix BR Code (EMV/TLV) payload generation/parsing and Pix key validation/parsing (CPF/CNPJ/email/phone/random key). Renamed from the original generatePix/isValidPix/parsePix names to the *PixPayload family to read clearly next to the *PixKey utils. Adds shared crc16-ccitt, format-tlv/parse-tlv internals. --- src/_internals/constants/pix.ts | 78 +++++ .../crc16-ccitt/crc16-ccitt.test.ts | 46 +++ src/_internals/crc16-ccitt/crc16-ccitt.ts | 40 +++ src/_internals/format-tlv/format-tlv.test.ts | 25 ++ src/_internals/format-tlv/format-tlv.ts | 26 ++ src/_internals/parse-tlv/parse-tlv.test.ts | 48 +++ src/_internals/parse-tlv/parse-tlv.ts | 47 +++ .../sanitize-to-ascii.test.ts | 29 ++ .../sanitize-to-ascii/sanitize-to-ascii.ts | 27 ++ src/generate-pix-payload/constants.ts | 10 + .../generate-pix-payload.test.ts | 326 ++++++++++++++++++ .../generate-pix-payload.ts | 204 +++++++++++ src/is-valid-pix-key/is-valid-pix-key.test.ts | 98 ++++++ src/is-valid-pix-key/is-valid-pix-key.ts | 43 +++ .../is-valid-pix-payload.test.ts | 190 ++++++++++ .../is-valid-pix-payload.ts | 36 ++ src/parse-pix-key/constants.ts | 16 + src/parse-pix-key/parse-pix-key.test.ts | 251 ++++++++++++++ src/parse-pix-key/parse-pix-key.ts | 89 +++++ .../parse-pix-payload.test.ts | 203 +++++++++++ src/parse-pix-payload/parse-pix-payload.ts | 205 +++++++++++ 21 files changed, 2037 insertions(+) create mode 100644 src/_internals/constants/pix.ts create mode 100644 src/_internals/crc16-ccitt/crc16-ccitt.test.ts create mode 100644 src/_internals/crc16-ccitt/crc16-ccitt.ts create mode 100644 src/_internals/format-tlv/format-tlv.test.ts create mode 100644 src/_internals/format-tlv/format-tlv.ts create mode 100644 src/_internals/parse-tlv/parse-tlv.test.ts create mode 100644 src/_internals/parse-tlv/parse-tlv.ts create mode 100644 src/_internals/sanitize-to-ascii/sanitize-to-ascii.test.ts create mode 100644 src/_internals/sanitize-to-ascii/sanitize-to-ascii.ts create mode 100644 src/generate-pix-payload/constants.ts create mode 100644 src/generate-pix-payload/generate-pix-payload.test.ts create mode 100644 src/generate-pix-payload/generate-pix-payload.ts create mode 100644 src/is-valid-pix-key/is-valid-pix-key.test.ts create mode 100644 src/is-valid-pix-key/is-valid-pix-key.ts create mode 100644 src/is-valid-pix-payload/is-valid-pix-payload.test.ts create mode 100644 src/is-valid-pix-payload/is-valid-pix-payload.ts create mode 100644 src/parse-pix-key/constants.ts create mode 100644 src/parse-pix-key/parse-pix-key.test.ts create mode 100644 src/parse-pix-key/parse-pix-key.ts create mode 100644 src/parse-pix-payload/parse-pix-payload.test.ts create mode 100644 src/parse-pix-payload/parse-pix-payload.ts diff --git a/src/_internals/constants/pix.ts b/src/_internals/constants/pix.ts new file mode 100644 index 00000000..4570f9bb --- /dev/null +++ b/src/_internals/constants/pix.ts @@ -0,0 +1,78 @@ +/** + * BR Code (EMV® QRCPS-MPM) field identifiers and Pix specific limits shared by the Pix + * utilities. + * + * The payload is a flat list of TLV objects: a 2 digit ID, a 2 digit length and a value of + * exactly that length. The Pix arrangement lives in one of the "Merchant Account Information" + * templates (IDs 26 to 51), the one whose GUI (sub-object `00`) is `br.gov.bcb.pix`. + * + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf + */ + +export const PIX_GUI = "br.gov.bcb.pix"; + +export const PIX_PAYLOAD_FORMAT_INDICATOR_ID = "00"; + +export const PIX_PAYLOAD_FORMAT_INDICATOR = "01"; + +export const PIX_POINT_OF_INITIATION_ID = "01"; + +export const PIX_STATIC_POINT_OF_INITIATION = "11"; + +export const PIX_DYNAMIC_POINT_OF_INITIATION = "12"; + +export const PIX_MERCHANT_ACCOUNT_INFORMATION_ID = "26"; + +export const PIX_MERCHANT_ACCOUNT_INFORMATION_FIRST_ID = 26; + +export const PIX_MERCHANT_ACCOUNT_INFORMATION_LAST_ID = 51; + +export const PIX_MERCHANT_ACCOUNT_INFORMATION_MAX_LENGTH = 99; + +export const PIX_GUI_ID = "00"; + +export const PIX_KEY_ID = "01"; + +export const PIX_DESCRIPTION_ID = "02"; + +export const PIX_URL_ID = "25"; + +export const PIX_MERCHANT_CATEGORY_CODE_ID = "52"; + +export const PIX_MERCHANT_CATEGORY_CODE = "0000"; + +export const PIX_TRANSACTION_CURRENCY_ID = "53"; + +export const PIX_TRANSACTION_CURRENCY = "986"; + +export const PIX_TRANSACTION_AMOUNT_ID = "54"; + +export const PIX_TRANSACTION_AMOUNT_MAX_LENGTH = 13; + +export const PIX_COUNTRY_CODE_ID = "58"; + +export const PIX_COUNTRY_CODE = "BR"; + +export const PIX_MERCHANT_NAME_ID = "59"; + +export const PIX_MERCHANT_NAME_MAX_LENGTH = 25; + +export const PIX_MERCHANT_CITY_ID = "60"; + +export const PIX_MERCHANT_CITY_MAX_LENGTH = 15; + +export const PIX_ADDITIONAL_DATA_ID = "62"; + +export const PIX_TXID_ID = "05"; + +export const PIX_ABSENT_TXID = "***"; + +export const PIX_CRC_TAG = "6304"; + +export const PIX_CRC_LENGTH = 4; + +export const PIX_KEY_MAX_LENGTH = 77; + +export const PIX_URL_MAX_LENGTH = 77; + +export const PIX_DESCRIPTION_MAX_LENGTH = 72; diff --git a/src/_internals/crc16-ccitt/crc16-ccitt.test.ts b/src/_internals/crc16-ccitt/crc16-ccitt.test.ts new file mode 100644 index 00000000..8b21760c --- /dev/null +++ b/src/_internals/crc16-ccitt/crc16-ccitt.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "../test/runtime"; +import { crc16Ccitt } from "./crc16-ccitt"; + +describe("crc16Ccitt", () => { + test("should match the CRC-16/CCITT-FALSE check value", () => { + expect(crc16Ccitt("123456789")).toBe("29B1"); + }); + + test("should return the initial value for an empty string", () => { + expect(crc16Ccitt("")).toBe("FFFF"); + }); + + test("should always return four uppercase hexadecimal digits", () => { + for (let index = 0; index < 500; index++) { + expect(crc16Ccitt(`payload-${index}`)).toMatch(/^[0-9A-F]{4}$/); + } + }); + + test("should match the static QR Code example of the Bacen manual", () => { + expect( + crc16Ccitt( + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***6304", + ), + ).toBe("1D3D"); + }); + + test("should match the dynamic QR Code example of the Bacen manual", () => { + expect( + crc16Ccitt( + "00020101021226700014br.gov.bcb.pix2548pix.example.com/8b3da2f39a4140d1a91abd93113bd4415204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***6304", + ), + ).toBe("64E4"); + }); + + test("should match the BR Code manual example", () => { + expect( + crc16Ccitt( + "00020104141234567890123426580014BR.GOV.BCB.PIX0136123e4567-e12b-12d1-a456-42665544000027300012BR.COM.OUTRO011001234567895204000053039865406123.455802BR5917NOME DO RECEBEDOR6008BRASILIA61087007490062190515RP12345678-201980390012BR.COM.OUTRO01190123.ABCD.3456.WXYZ6304", + ), + ).toBe("AD38"); + }); + + test("should change when the payload changes", () => { + expect(crc16Ccitt("A")).not.toBe(crc16Ccitt("B")); + }); +}); diff --git a/src/_internals/crc16-ccitt/crc16-ccitt.ts b/src/_internals/crc16-ccitt/crc16-ccitt.ts new file mode 100644 index 00000000..cd858355 --- /dev/null +++ b/src/_internals/crc16-ccitt/crc16-ccitt.ts @@ -0,0 +1,40 @@ +const POLYNOMIAL = 0x1021; + +const INITIAL_VALUE = 0xffff; + +const MASK = 0xffff; + +const HEX_LENGTH = 4; + +/** + * Calculates the CRC-16/CCITT-FALSE checksum of a string and returns it as four uppercase + * hexadecimal digits. + * + * The variant is the one required by the BR Code standard: polynomial `0x1021`, initial value + * `0xFFFF`, no input or output reflection and no final xor. The bytes fed to the checksum are + * the UTF-8 encoding of the string, which for an ASCII BR Code payload is the payload itself. + * + * @param {string} value - The string to checksum. + * @returns {string} The checksum as four uppercase hexadecimal digits. + * + * @example + * ```typescript + * crc16Ccitt("123456789"); // "29B1" + * crc16Ccitt(""); // "FFFF" + * ``` + */ +export const crc16Ccitt = (value: string): string => { + const bytes = new TextEncoder().encode(value); + + let crc = INITIAL_VALUE; + + for (let index = 0; index < bytes.length; index++) { + crc ^= bytes[index] << 8; + + for (let bit = 0; bit < 8; bit++) { + crc = (crc & 0x8000) === 0 ? (crc << 1) & MASK : ((crc << 1) ^ POLYNOMIAL) & MASK; + } + } + + return crc.toString(16).toUpperCase().padStart(HEX_LENGTH, "0"); +}; diff --git a/src/_internals/format-tlv/format-tlv.test.ts b/src/_internals/format-tlv/format-tlv.test.ts new file mode 100644 index 00000000..9d410551 --- /dev/null +++ b/src/_internals/format-tlv/format-tlv.test.ts @@ -0,0 +1,25 @@ +import { parseTlv } from "../parse-tlv/parse-tlv"; +import { describe, expect, test } from "../test/runtime"; +import { formatTlv } from "./format-tlv"; + +describe("formatTlv", () => { + test("should pad the length to two digits", () => { + expect(formatTlv({ id: "00", value: "01" })).toBe("000201"); + }); + + test("should keep a two digit length as is", () => { + expect(formatTlv({ id: "59", value: "NOME DO RECEBEDOR" })).toBe("5917NOME DO RECEBEDOR"); + }); + + test("should serialize an empty value", () => { + expect(formatTlv({ id: "62", value: "" })).toBe("6200"); + }); + + test("should round-trip through parseTlv", () => { + for (let length = 0; length <= 99; length++) { + const value = "x".repeat(length); + + expect(parseTlv(formatTlv({ id: "26", value }))).toEqual({ "26": value }); + } + }); +}); diff --git a/src/_internals/format-tlv/format-tlv.ts b/src/_internals/format-tlv/format-tlv.ts new file mode 100644 index 00000000..21b2648f --- /dev/null +++ b/src/_internals/format-tlv/format-tlv.ts @@ -0,0 +1,26 @@ +export type FormatTlvParams = { + /** The two digit object ID. */ + id: string; + /** The value of the object, whose length is written in front of it. */ + value: string; +}; + +const LENGTH_SEGMENT_LENGTH = 2; + +/** + * Serializes one EMV® style TLV (tag-length-value) object: the ID, the value length written as + * two digits and the value itself. + * + * @param {FormatTlvParams} params - The object to serialize. + * @param {string} params.id - The 2 digit object ID. + * @param {string} params.value - The object value, at most 99 characters long. + * @returns {string} The serialized object. + * + * @example + * ```typescript + * formatTlv({ id: "00", value: "01" }); // "000201" + * formatTlv({ id: "58", value: "BR" }); // "5802BR" + * ``` + */ +export const formatTlv = ({ id, value }: FormatTlvParams): string => + `${id}${value.length.toString().padStart(LENGTH_SEGMENT_LENGTH, "0")}${value}`; diff --git a/src/_internals/parse-tlv/parse-tlv.test.ts b/src/_internals/parse-tlv/parse-tlv.test.ts new file mode 100644 index 00000000..f7ed40ed --- /dev/null +++ b/src/_internals/parse-tlv/parse-tlv.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "../test/runtime"; +import { parseTlv } from "./parse-tlv"; + +describe("parseTlv", () => { + describe("should return the objects", () => { + test("when the string holds a single object", () => { + expect(parseTlv("000201")).toEqual({ "00": "01" }); + }); + + test("when the string holds several objects", () => { + expect(parseTlv("00020153039865802BR")).toEqual({ + "00": "01", + "53": "986", + "58": "BR", + }); + }); + + test("when an object has an empty value", () => { + expect(parseTlv("0000")).toEqual({ "00": "" }); + }); + + test("when the string is empty", () => { + expect(parseTlv("")).toEqual({}); + }); + + test("when an id repeats, keeping the last one", () => { + expect(parseTlv("0001A0001B")).toEqual({ "00": "B" }); + }); + }); + + describe("should return null", () => { + test("when a value runs past the end of the string", () => { + expect(parseTlv("0003ab")).toBeNull(); + }); + + test("when an id is not made of two digits", () => { + expect(parseTlv("0A0201")).toBeNull(); + }); + + test("when a length is not made of two digits", () => { + expect(parseTlv("00A201")).toBeNull(); + }); + + test("when the string is too short to hold an object", () => { + expect(parseTlv("00")).toBeNull(); + }); + }); +}); diff --git a/src/_internals/parse-tlv/parse-tlv.ts b/src/_internals/parse-tlv/parse-tlv.ts new file mode 100644 index 00000000..9eca603c --- /dev/null +++ b/src/_internals/parse-tlv/parse-tlv.ts @@ -0,0 +1,47 @@ +export type TlvFields = Record; + +const SEGMENT_LENGTH = 2; + +const SEGMENT_REGEX = /^\d{2}$/; + +/** + * Parses an EMV® style TLV (tag-length-value) string into its objects. + * + * Every object is a 2 digit ID, a 2 digit length and a value of exactly that many characters, + * laid out back to back. Parsing stops with `null` as soon as the string stops being + * well-formed, i.e. when an ID or a length is not made of two digits or when a value runs past + * the end of the string. Repeated IDs are not expected at the root of a BR Code; when they do + * occur, the last one wins. + * + * @param {string} value - The TLV string to parse. + * @returns {TlvFields|null} The objects keyed by ID, or `null` when the string is malformed. + * + * @example + * ```typescript + * parseTlv("0002015303986"); // { "00": "01", "53": "986" } + * parseTlv("00020153039865802BR"); // { "00": "01", "53": "986", "58": "BR" } + * parseTlv("0003ab"); // null, the value is shorter than its declared length + * ``` + */ +export const parseTlv = (value: string): TlvFields | null => { + const fields: TlvFields = {}; + + let index = 0; + + while (index < value.length) { + const id = value.slice(index, index + SEGMENT_LENGTH); + const length = value.slice(index + SEGMENT_LENGTH, index + SEGMENT_LENGTH * 2); + + if (!SEGMENT_REGEX.test(id) || !SEGMENT_REGEX.test(length)) return null; + + const start = index + SEGMENT_LENGTH * 2; + const end = start + Number(length); + + if (end > value.length) return null; + + fields[id] = value.slice(start, end); + index = end; + } + + return fields; +}; diff --git a/src/_internals/sanitize-to-ascii/sanitize-to-ascii.test.ts b/src/_internals/sanitize-to-ascii/sanitize-to-ascii.test.ts new file mode 100644 index 00000000..c2ce1589 --- /dev/null +++ b/src/_internals/sanitize-to-ascii/sanitize-to-ascii.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "../test/runtime"; +import { sanitizeToAscii } from "./sanitize-to-ascii"; + +describe("sanitizeToAscii", () => { + test("should drop diacritics", () => { + expect(sanitizeToAscii("São Paulo")).toBe("Sao Paulo"); + expect(sanitizeToAscii("BRASÍLIA")).toBe("BRASILIA"); + expect(sanitizeToAscii("José Antônio Nuñez")).toBe("Jose Antonio Nunez"); + }); + + test("should drop characters outside printable ASCII", () => { + expect(sanitizeToAscii("Loja 💸 Feliz")).toBe("Loja Feliz"); + expect(sanitizeToAscii(`a${String.fromCharCode(0)}b`)).toBe("ab"); + }); + + test("should collapse whitespace and trim", () => { + expect(sanitizeToAscii(" Fulano de \n Tal ")).toBe("Fulano de Tal"); + }); + + test("should keep printable ASCII untouched", () => { + expect(sanitizeToAscii("Fulano de Tal")).toBe("Fulano de Tal"); + expect(sanitizeToAscii("ACME LTDA. #1")).toBe("ACME LTDA. #1"); + }); + + test("should return an empty string when nothing survives", () => { + expect(sanitizeToAscii(" ")).toBe(""); + expect(sanitizeToAscii("💸")).toBe(""); + }); +}); diff --git a/src/_internals/sanitize-to-ascii/sanitize-to-ascii.ts b/src/_internals/sanitize-to-ascii/sanitize-to-ascii.ts new file mode 100644 index 00000000..d819f71b --- /dev/null +++ b/src/_internals/sanitize-to-ascii/sanitize-to-ascii.ts @@ -0,0 +1,27 @@ +const COMBINING_MARKS_REGEX = /[\u0300-\u036f]/g; + +const NON_PRINTABLE_ASCII_REGEX = /[^\u0020-\u007e]/g; + +const WHITESPACE_REGEX = /\s+/g; + +/** + * Folds a string down to printable ASCII: accented letters lose their diacritics, anything + * still outside the printable ASCII range is dropped and runs of whitespace collapse into a + * single space. + * + * @param {string} value - The value to fold. + * @returns {string} The trimmed, printable ASCII form of the value. + * + * @example + * ```typescript + * sanitizeToAscii("São Paulo"); // "Sao Paulo" + * sanitizeToAscii(" Fulano de Tal "); // "Fulano de Tal" + * ``` + */ +export const sanitizeToAscii = (value: string): string => + value + .normalize("NFD") + .replace(COMBINING_MARKS_REGEX, "") + .replace(NON_PRINTABLE_ASCII_REGEX, "") + .replace(WHITESPACE_REGEX, " ") + .trim(); diff --git a/src/generate-pix-payload/constants.ts b/src/generate-pix-payload/constants.ts new file mode 100644 index 00000000..e26df6c9 --- /dev/null +++ b/src/generate-pix-payload/constants.ts @@ -0,0 +1,10 @@ +export const AMOUNT_DECIMAL_PLACES = 2; + +/** + * How many characters one TLV object spends besides its value: the 2 digit ID plus the 2 digit + * length. + */ +export const TLV_OVERHEAD = 4; + +/** The characters the Pix manual allows in a `txid`, capped at the 25 the BR Code holds. */ +export const TXID_REGEX = /^[A-Za-z0-9]{1,25}$/; diff --git a/src/generate-pix-payload/generate-pix-payload.test.ts b/src/generate-pix-payload/generate-pix-payload.test.ts new file mode 100644 index 00000000..80af53d4 --- /dev/null +++ b/src/generate-pix-payload/generate-pix-payload.test.ts @@ -0,0 +1,326 @@ +import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; +import { describe, expect, test } from "../_internals/test/runtime"; +import { generateCnpj } from "../generate-cnpj/generate-cnpj"; +import { generateCpf } from "../generate-cpf/generate-cpf"; +import { isValidPixPayload } from "../is-valid-pix-payload/is-valid-pix-payload"; +import { parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; +import { generatePixPayload } from "./generate-pix-payload"; + +const BASE = { + key: "123e4567-e12b-12d1-a456-426655440000", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", +}; + +const EVP = "71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d"; + +describe("generatePixPayload", () => { + describe("should return null", () => { + test("when it is null", () => { + // @ts-expect-error + expect(generatePixPayload(null)).toBeNull(); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(generatePixPayload(undefined)).toBeNull(); + }); + + test("when it is not an object", () => { + // @ts-expect-error + expect(generatePixPayload("12345678909")).toBeNull(); + // @ts-expect-error + expect(generatePixPayload(123)).toBeNull(); + // @ts-expect-error + expect(generatePixPayload(true)).toBeNull(); + }); + + test("when the key is invalid", () => { + expect( + generatePixPayload({ + key: "11257245286", + merchantName: "Fulano", + merchantCity: "Brasilia", + }), + ).toBeNull(); + }); + + test("when neither key nor url is given", () => { + expect(generatePixPayload({ merchantName: "Fulano", merchantCity: "Brasilia" })).toBeNull(); + }); + + test("when both key and url are given", () => { + expect( + generatePixPayload({ + key: EVP, + url: "pix.example.com/qr/v2/1234", + merchantName: "Fulano", + merchantCity: "Brasilia", + }), + ).toBeNull(); + }); + + test("when url is not a string", () => { + expect( + // @ts-expect-error + generatePixPayload({ url: 123, merchantName: "Fulano", merchantCity: "Brasilia" }), + ).toBeNull(); + }); + + test("when url is an empty string", () => { + expect( + generatePixPayload({ url: "", merchantName: "Fulano", merchantCity: "Brasilia" }), + ).toBeNull(); + }); + + test("when url is longer than 77 characters", () => { + expect( + generatePixPayload({ + url: `pix.example.com/${"a".repeat(65)}`, + merchantName: "Fulano", + merchantCity: "Brasilia", + }), + ).toBeNull(); + }); + + test("when the merchant name is missing or empty after folding", () => { + // @ts-expect-error + expect(generatePixPayload({ key: EVP, merchantCity: "Brasilia" })).toBeNull(); + expect( + generatePixPayload({ key: EVP, merchantName: " ", merchantCity: "Brasilia" }), + ).toBeNull(); + expect( + generatePixPayload({ key: EVP, merchantName: "💸", merchantCity: "Brasilia" }), + ).toBeNull(); + }); + + test("when the merchant city is missing or empty after folding", () => { + // @ts-expect-error + expect(generatePixPayload({ key: EVP, merchantName: "Fulano" })).toBeNull(); + expect( + generatePixPayload({ key: EVP, merchantName: "Fulano", merchantCity: " " }), + ).toBeNull(); + }); + + test("when the amount is not a positive finite number", () => { + expect(generatePixPayload({ ...BASE, amount: 0 })).toBeNull(); + expect(generatePixPayload({ ...BASE, amount: -1 })).toBeNull(); + expect(generatePixPayload({ ...BASE, amount: Number.NaN })).toBeNull(); + expect(generatePixPayload({ ...BASE, amount: Number.POSITIVE_INFINITY })).toBeNull(); + // @ts-expect-error + expect(generatePixPayload({ ...BASE, amount: "10" })).toBeNull(); + }); + + test("when the amount does not fit in 13 characters", () => { + expect(generatePixPayload({ ...BASE, amount: 12_345_678_901_2 })).toBeNull(); + }); + + test("when the txid is not alphanumeric or is too long", () => { + expect(generatePixPayload({ ...BASE, txid: "Um-Id-Qualquer" })).toBeNull(); + expect(generatePixPayload({ ...BASE, txid: "" })).toBeNull(); + expect(generatePixPayload({ ...BASE, txid: "a".repeat(26) })).toBeNull(); + // @ts-expect-error + expect(generatePixPayload({ ...BASE, txid: 123 })).toBeNull(); + }); + }); + + describe("should generate a valid payload", () => { + test("matching the static example of the Bacen manual", () => { + expect(generatePixPayload(BASE)).toBe( + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D", + ); + }); + + test("that isValidPixPayload accepts", () => { + expect(isValidPixPayload(generatePixPayload(BASE) ?? "")).toBe(true); + }); + + test("whose CRC covers the payload up to and including 6304", () => { + const payload = generatePixPayload(BASE) ?? ""; + + expect(payload.slice(-4)).toBe(crc16Ccitt(payload.slice(0, -4))); + }); + + test("with the amount formatted with two decimal places", () => { + expect(generatePixPayload({ ...BASE, amount: 10 })).toContain("540510.00"); + expect(generatePixPayload({ ...BASE, amount: 123.456 })).toContain("5406123.46"); + expect(generatePixPayload({ ...BASE, amount: 0.01 })).toContain("54040.01"); + }); + + test("with *** as the txid when it is omitted", () => { + expect(generatePixPayload(BASE)).toContain("62070503***"); + }); + + test("with the txid when it is given", () => { + expect(generatePixPayload({ ...BASE, txid: "RP123456782019" })).toContain( + "62180514RP123456782019", + ); + }); + }); + + describe("should generate a dynamic payload when url is given", () => { + const DYNAMIC_BASE = { + url: "pix.example.com/qr/v2/1234", + merchantName: "Fulano de Tal", + merchantCity: "Brasilia", + }; + + test("with the point of initiation method set to dynamic (12)", () => { + expect(generatePixPayload(DYNAMIC_BASE)).toContain("010212"); + }); + + test("with the url in the merchant account information as sub-object 25", () => { + expect(generatePixPayload(DYNAMIC_BASE)).toContain("2526pix.example.com/qr/v2/1234"); + }); + + test("that isValidPixPayload accepts", () => { + expect(isValidPixPayload(generatePixPayload(DYNAMIC_BASE) ?? "")).toBe(true); + }); + + test("that parsePixPayload parses back with pointOfInitiation dynamic and no key", () => { + expect(parsePixPayload(generatePixPayload(DYNAMIC_BASE) ?? "")).toEqual({ + url: "pix.example.com/qr/v2/1234", + merchantName: "Fulano de Tal", + merchantCity: "Brasilia", + pointOfInitiation: "dynamic", + }); + }); + + test("accepting a url of exactly 77 characters", () => { + const url = `pix.example.com/${"a".repeat(61)}`; + + expect(url.length).toBe(77); + + const payload = generatePixPayload({ ...DYNAMIC_BASE, url }); + + expect(payload).not.toBeNull(); + expect(parsePixPayload(payload ?? "")?.url).toBe(url); + }); + }); + + describe("should normalize its parameters", () => { + test("folding accents out of the merchant name and city", () => { + expect( + parsePixPayload(generatePixPayload({ ...BASE, merchantCity: "Brasília" }) ?? ""), + ).toMatchObject({ + merchantCity: "Brasilia", + }); + expect( + parsePixPayload(generatePixPayload({ ...BASE, merchantName: "José Antônio" }) ?? ""), + ).toMatchObject({ + merchantName: "Jose Antonio", + }); + }); + + test("truncating the merchant name to 25 characters", () => { + const pix = parsePixPayload( + generatePixPayload({ ...BASE, merchantName: "A".repeat(40) }) ?? "", + ); + + expect(pix?.merchantName).toBe("A".repeat(25)); + }); + + test("truncating the merchant city to 15 characters", () => { + const pix = parsePixPayload( + generatePixPayload({ ...BASE, merchantCity: "B".repeat(40) }) ?? "", + ); + + expect(pix?.merchantCity).toBe("B".repeat(15)); + }); + + test("normalizing the key to its DICT canonical form", () => { + expect( + parsePixPayload(generatePixPayload({ ...BASE, key: "123.456.789-09" }) ?? "")?.key, + ).toBe("12345678909"); + expect( + parsePixPayload(generatePixPayload({ ...BASE, key: "(11) 98765-4321" }) ?? "")?.key, + ).toBe("+5511987654321"); + expect( + parsePixPayload(generatePixPayload({ ...BASE, key: " Fulano@Example.COM " }) ?? "")?.key, + ).toBe("fulano@example.com"); + expect( + parsePixPayload(generatePixPayload({ ...BASE, key: EVP.toUpperCase() }) ?? "")?.key, + ).toBe(EVP); + }); + + test("truncating the description to what the 99 character template leaves", () => { + const payload = generatePixPayload({ + ...BASE, + key: "12345678909", + description: "y".repeat(90), + }); + + expect(parsePixPayload(payload ?? "")?.description).toBe("y".repeat(62)); + }); + + test("truncating the description to what a phone key leaves", () => { + const payload = generatePixPayload({ + ...BASE, + key: "1130000000", + description: "y".repeat(90), + }); + + expect(parsePixPayload(payload ?? "")?.description).toBe("y".repeat(60)); + }); + + test("leaving room for the description on a long key", () => { + const key = `${"a".repeat(56)}@example.com`; + const payload = generatePixPayload({ ...BASE, key, description: "z".repeat(30) }) ?? ""; + + expect(parsePixPayload(payload)?.description).toBe("z".repeat(5)); + }); + + test("dropping a description that does not fit at all", () => { + const key = `${"a".repeat(65)}@example.com`; + const payload = generatePixPayload({ ...BASE, key, description: "z".repeat(30) }) ?? ""; + + expect(parsePixPayload(payload)).not.toHaveProperty("description"); + }); + }); + + describe("should round-trip", () => { + test("through isValidPixPayload and parsePixPayload for randomized CPF keys", () => { + for (let index = 0; index < 200; index++) { + const params = { + key: generateCpf(), + merchantName: "Fulano de Tal", + merchantCity: "Brasilia", + amount: Number(((index + 1) / 100).toFixed(2)), + txid: `TX${index}`, + }; + const payload = generatePixPayload(params) ?? ""; + + expect(isValidPixPayload(payload)).toBe(true); + expect(parsePixPayload(payload)).toEqual(params); + } + }); + + test("through isValidPixPayload and parsePixPayload for randomized CNPJ keys", () => { + for (let index = 0; index < 200; index++) { + const params = { + key: generateCnpj(), + merchantName: "Loja Exemplo", + merchantCity: "Sao Paulo", + }; + const payload = generatePixPayload(params) ?? ""; + + expect(isValidPixPayload(payload)).toBe(true); + expect(parsePixPayload(payload)).toEqual(params); + } + }); + + test("through isValidPixPayload and parsePixPayload for randomized dynamic urls", () => { + for (let index = 0; index < 200; index++) { + const params = { + url: `pix.example.com/qr/v2/${index}`, + merchantName: "Fulano de Tal", + merchantCity: "Brasilia", + }; + const payload = generatePixPayload(params) ?? ""; + + expect(isValidPixPayload(payload)).toBe(true); + expect(parsePixPayload(payload)).toEqual({ ...params, pointOfInitiation: "dynamic" }); + } + }); + }); +}); diff --git a/src/generate-pix-payload/generate-pix-payload.ts b/src/generate-pix-payload/generate-pix-payload.ts new file mode 100644 index 00000000..03922ea2 --- /dev/null +++ b/src/generate-pix-payload/generate-pix-payload.ts @@ -0,0 +1,204 @@ +import { + PIX_ABSENT_TXID, + PIX_ADDITIONAL_DATA_ID, + PIX_COUNTRY_CODE, + PIX_COUNTRY_CODE_ID, + PIX_CRC_TAG, + PIX_DESCRIPTION_ID, + PIX_DESCRIPTION_MAX_LENGTH, + PIX_DYNAMIC_POINT_OF_INITIATION, + PIX_GUI, + PIX_GUI_ID, + PIX_KEY_ID, + PIX_MERCHANT_ACCOUNT_INFORMATION_ID, + PIX_MERCHANT_ACCOUNT_INFORMATION_MAX_LENGTH, + PIX_MERCHANT_CATEGORY_CODE, + PIX_MERCHANT_CATEGORY_CODE_ID, + PIX_MERCHANT_CITY_ID, + PIX_MERCHANT_CITY_MAX_LENGTH, + PIX_MERCHANT_NAME_ID, + PIX_MERCHANT_NAME_MAX_LENGTH, + PIX_PAYLOAD_FORMAT_INDICATOR, + PIX_PAYLOAD_FORMAT_INDICATOR_ID, + PIX_POINT_OF_INITIATION_ID, + PIX_TRANSACTION_AMOUNT_ID, + PIX_TRANSACTION_AMOUNT_MAX_LENGTH, + PIX_TRANSACTION_CURRENCY, + PIX_TRANSACTION_CURRENCY_ID, + PIX_TXID_ID, + PIX_URL_ID, + PIX_URL_MAX_LENGTH, +} from "../_internals/constants/pix"; +import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; +import { formatTlv } from "../_internals/format-tlv/format-tlv"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToAscii } from "../_internals/sanitize-to-ascii/sanitize-to-ascii"; +import { parsePixKey } from "../parse-pix-key/parse-pix-key"; +import { AMOUNT_DECIMAL_PLACES, TLV_OVERHEAD, TXID_REGEX } from "./constants"; + +export type GeneratePixPayloadParams = { + /** The Pix key of the receiver, in any accepted form. Required unless `url` is given. */ + key?: string; + /** + * The PSP location of a dynamic payload (Bacen field 26-25), without a URL scheme, e.g. + * `"pix.example.com/qr/v2/1234"`. When given, the payload is generated as dynamic + * (`pointOfInitiation` `"12"`) and carries this URL instead of a key. Required unless `key` + * is given; giving both `key` and `url` is invalid, just like giving neither. + */ + url?: string; + /** Name of the receiver, folded to ASCII and truncated to 25 characters. */ + merchantName: string; + /** City of the receiver, folded to ASCII and truncated to 15 characters. */ + merchantCity: string; + /** Amount in BRL. Omit it to let the payer type it. */ + amount?: number; + /** Transaction ID, 1 to 25 characters of `[A-Za-z0-9]` (default: the absent marker `***`). */ + txid?: string; + /** Free text shown to the payer, folded to ASCII and truncated to what the template holds. */ + description?: string; +}; + +const toAsciiField = (value: unknown, maxLength: number): string => + typeof value === "string" ? sanitizeToAscii(value).slice(0, maxLength).trim() : ""; + +/** + * Generates the payload of a Pix BR Code, the string behind a Pix QR Code and behind "Pix + * copia e cola". + * + * Exactly one of `params.key` or `params.url` must be given: `null` is returned when both are + * given and when neither is given, since only one of them can occupy the "Merchant Account + * Information" template at a time. + * + * When `params.key` is given, it is normalized to its DICT canonical form by `parsePixKey` and + * the payload is static: the "Point of Initiation Method" object is left out, so the payload + * may be paid more than once, as in the example of the Bacen manual. + * + * When `params.url` is given instead, the payload is dynamic per the Manual de Padrões para + * Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" + * template (sub-object `25` instead of `01`) and the "Point of Initiation Method" object (`01`) + * is set to `"12"`. `params.url` must be at most 77 characters, the length that keeps the + * template within its 99 character limit together with the `br.gov.bcb.pix` GUI. `parsePixPayload` + * already parses both shapes, so `parsePixPayload(generatePixPayload({ url, ... }))` round-trips. + * + * The merchant name, the merchant city and the description are folded to printable ASCII + * (accents are dropped) and truncated to the lengths the BR Code allows, the description to + * whatever is left of the 99 characters the "Merchant Account Information" template holds. + * + * @param {GeneratePixPayloadParams} params - The parameters of the payload. + * @param {string} [params.key] - The Pix key of the receiver. Required unless `url` is given. + * @param {string} [params.url] - The PSP location of a dynamic payload. Required unless `key` + * is given. + * @param {string} params.merchantName - The name of the receiver. + * @param {string} params.merchantCity - The city of the receiver. + * @param {number} [params.amount] - The amount in BRL. Omit it to let the payer type it. + * @param {string} [params.txid] - The transaction ID, 1 to 25 characters of `[A-Za-z0-9]`. + * @param {string} [params.description] - The free text shown to the payer. + * @returns {string|null} The BR Code payload, or `null` when the parameters are invalid. + * + * @example + * ```typescript + * generatePixPayload({ + * key: "123.456.789-09", + * merchantName: "Fulano de Tal", + * merchantCity: "Brasília", + * amount: 123.45, + * }); + * // "00020126330014br.gov.bcb.pix0111123456789095204000053039865406123.455802BR..." + * + * generatePixPayload({ + * url: "pix.example.com/qr/v2/1234", + * merchantName: "Fulano de Tal", + * merchantCity: "Brasília", + * }); + * // "00020101021226480014br.gov.bcb.pix2526pix.example.com/qr/v2/12345204000053039865802BR5913Fulano de Tal6008Brasilia62070503***6304FC66" + * + * generatePixPayload({ merchantName: "Fulano", merchantCity: "Brasília" }); // null (neither key nor url) + * generatePixPayload({ key: "123.456.789-09", url: "pix.example.com/qr/v2/1234", merchantName: "Fulano", merchantCity: "Brasília" }); // null (both key and url) + * ``` + * + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf + * @see Based on: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + * @see Based on: https://github.com/bacen/pix-dict-api DICT OpenAPI spec. + */ +export const generatePixPayload = (params: GeneratePixPayloadParams): string | null => { + if (isNullish(params) || typeof params !== "object") return null; + + const { key: keyInput, url: urlInput } = params; + + if ((keyInput !== undefined) === (urlInput !== undefined)) return null; + + let identifierId: string; + let identifierValue: string; + let pointOfInitiation: string | undefined; + + if (keyInput !== undefined) { + const key = parsePixKey(keyInput); + + if (!key) return null; + + identifierId = PIX_KEY_ID; + identifierValue = key.value; + } else { + const url = urlInput; + + if (typeof url !== "string" || url === "" || url.length > PIX_URL_MAX_LENGTH) return null; + + identifierId = PIX_URL_ID; + identifierValue = url; + pointOfInitiation = PIX_DYNAMIC_POINT_OF_INITIATION; + } + + const merchantName = toAsciiField(params.merchantName, PIX_MERCHANT_NAME_MAX_LENGTH); + + if (!merchantName) return null; + + const merchantCity = toAsciiField(params.merchantCity, PIX_MERCHANT_CITY_MAX_LENGTH); + + if (!merchantCity) return null; + + const { amount, txid } = params; + + if (amount !== undefined && (!Number.isFinite(amount) || amount <= 0)) return null; + + const formattedAmount = amount === undefined ? "" : amount.toFixed(AMOUNT_DECIMAL_PLACES); + + if (formattedAmount.length > PIX_TRANSACTION_AMOUNT_MAX_LENGTH) return null; + + if (txid !== undefined && (typeof txid !== "string" || !TXID_REGEX.test(txid))) return null; + + const gui = formatTlv({ id: PIX_GUI_ID, value: PIX_GUI }); + const identifierObject = formatTlv({ id: identifierId, value: identifierValue }); + const descriptionRoom = Math.min( + PIX_DESCRIPTION_MAX_LENGTH, + PIX_MERCHANT_ACCOUNT_INFORMATION_MAX_LENGTH - + gui.length - + identifierObject.length - + TLV_OVERHEAD, + ); + const description = toAsciiField(params.description, Math.max(descriptionRoom, 0)); + + const merchantAccountInformation = + gui + + identifierObject + + (description ? formatTlv({ id: PIX_DESCRIPTION_ID, value: description }) : ""); + + const payload = + formatTlv({ id: PIX_PAYLOAD_FORMAT_INDICATOR_ID, value: PIX_PAYLOAD_FORMAT_INDICATOR }) + + (pointOfInitiation + ? formatTlv({ id: PIX_POINT_OF_INITIATION_ID, value: pointOfInitiation }) + : "") + + formatTlv({ id: PIX_MERCHANT_ACCOUNT_INFORMATION_ID, value: merchantAccountInformation }) + + formatTlv({ id: PIX_MERCHANT_CATEGORY_CODE_ID, value: PIX_MERCHANT_CATEGORY_CODE }) + + formatTlv({ id: PIX_TRANSACTION_CURRENCY_ID, value: PIX_TRANSACTION_CURRENCY }) + + (formattedAmount ? formatTlv({ id: PIX_TRANSACTION_AMOUNT_ID, value: formattedAmount }) : "") + + formatTlv({ id: PIX_COUNTRY_CODE_ID, value: PIX_COUNTRY_CODE }) + + formatTlv({ id: PIX_MERCHANT_NAME_ID, value: merchantName }) + + formatTlv({ id: PIX_MERCHANT_CITY_ID, value: merchantCity }) + + formatTlv({ + id: PIX_ADDITIONAL_DATA_ID, + value: formatTlv({ id: PIX_TXID_ID, value: txid ?? PIX_ABSENT_TXID }), + }) + + PIX_CRC_TAG; + + return payload + crc16Ccitt(payload); +}; diff --git a/src/is-valid-pix-key/is-valid-pix-key.test.ts b/src/is-valid-pix-key/is-valid-pix-key.test.ts new file mode 100644 index 00000000..8c1122bb --- /dev/null +++ b/src/is-valid-pix-key/is-valid-pix-key.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { generateCnpj } from "../generate-cnpj/generate-cnpj"; +import { generateCpf } from "../generate-cpf/generate-cpf"; +import { isValidPixKey } from "./is-valid-pix-key"; + +describe("isValidPixKey", () => { + describe("should return false", () => { + test("when it is an empty or blank string", () => { + expect(isValidPixKey("")).toBe(false); + expect(isValidPixKey(" ")).toBe(false); + }); + + test("when it is null", () => { + // @ts-expect-error + expect(isValidPixKey(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidPixKey(undefined)).toBe(false); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(isValidPixKey(12345678909)).toBe(false); + }); + + test("when it is a boolean, an object or an array", () => { + // @ts-expect-error + expect(isValidPixKey(true)).toBe(false); + // @ts-expect-error + expect(isValidPixKey({})).toBe(false); + // @ts-expect-error + expect(isValidPixKey([])).toBe(false); + }); + + test("when it is not a key of any accepted kind", () => { + expect(isValidPixKey("chave pix")).toBe(false); + expect(isValidPixKey("11257245286")).toBe(false); + expect(isValidPixKey("fulano@example")).toBe(false); + }); + }); + + describe("should return true", () => { + test("for a CPF", () => { + expect(isValidPixKey("123.456.789-09")).toBe(true); + expect(isValidPixKey("40364478829")).toBe(true); + }); + + test("for a CNPJ", () => { + expect(isValidPixKey("00.038.166/0001-05")).toBe(true); + expect(isValidPixKey("12ABC34501DE35")).toBe(true); + }); + + test("for an e-mail", () => { + expect(isValidPixKey("fulano_da_silva.recebedor@example.com")).toBe(true); + }); + + test("for a phone", () => { + expect(isValidPixKey("+5561912345678")).toBe(true); + expect(isValidPixKey("(11) 98765-4321")).toBe(true); + }); + + test("for a random key", () => { + expect(isValidPixKey("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d")).toBe(true); + expect(isValidPixKey("123e4567-e12b-12d1-a456-426655440000")).toBe(true); + }); + + test("for randomized documents", () => { + for (let index = 0; index < 200; index++) { + expect(isValidPixKey(generateCpf())).toBe(true); + expect(isValidPixKey(generateCnpj())).toBe(true); + } + }); + }); + + describe("should honour options.accept", () => { + test("accepting only the listed kinds", () => { + expect(isValidPixKey("123.456.789-09", { accept: ["cpf"] })).toBe(true); + expect(isValidPixKey("123.456.789-09", { accept: ["email", "evp"] })).toBe(false); + expect(isValidPixKey("fulano@example.com", { accept: ["email", "evp"] })).toBe(true); + expect(isValidPixKey("+5511987654321", { accept: ["phone"] })).toBe(true); + expect(isValidPixKey("00038166000105", { accept: ["cnpj"] })).toBe(true); + }); + + test("accepting nothing for an empty list", () => { + expect(isValidPixKey("123.456.789-09", { accept: [] })).toBe(false); + }); + + test("accepting every kind when the option is absent or not a list", () => { + expect(isValidPixKey("123.456.789-09", {})).toBe(true); + // @ts-expect-error + expect(isValidPixKey("123.456.789-09", { accept: "cpf" })).toBe(true); + // @ts-expect-error + expect(isValidPixKey("123.456.789-09", null)).toBe(true); + }); + }); +}); diff --git a/src/is-valid-pix-key/is-valid-pix-key.ts b/src/is-valid-pix-key/is-valid-pix-key.ts new file mode 100644 index 00000000..78cfef87 --- /dev/null +++ b/src/is-valid-pix-key/is-valid-pix-key.ts @@ -0,0 +1,43 @@ +import { type PixKeyType, parsePixKey } from "../parse-pix-key/parse-pix-key"; + +export type IsValidPixKeyOptions = { + /** Kinds of Pix key that count as valid (default: all of them). */ + accept?: PixKeyType[]; +}; + +/** + * Validates a Pix key (chave Pix) against the DICT key formats. + * + * A value is valid when `parsePixKey` recognizes it as a CPF, a CNPJ, an e-mail address, a + * Brazilian phone number or a random key (EVP), and when that kind is listed in + * `options.accept`. + * + * @param {string} value - The Pix key to validate. + * @param {IsValidPixKeyOptions} [options] - Optional validation options. + * @param {PixKeyType[]} [options.accept] - The kinds of key to accept. Defaults to all of them. + * @returns {boolean} True if the value is a valid Pix key, false otherwise. + * + * @example + * ```typescript + * isValidPixKey("123.456.789-09"); // true + * isValidPixKey("fulano@example.com"); // true + * isValidPixKey("(11) 98765-4321"); // true + * isValidPixKey("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d"); // true + * isValidPixKey("123.456.789-09", { accept: ["email", "evp"] }); // false + * isValidPixKey("not a key"); // false + * ``` + * + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf + * @see Based on: https://github.com/bacen/pix-dict-api DICT (Diretório de Identificadores de + * Contas Transacionais) OpenAPI spec, key format reference. + * @see Based on: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + */ +export const isValidPixKey = (value: string, options?: IsValidPixKeyOptions): boolean => { + const key = parsePixKey(value); + + if (!key) return false; + + const accept = options?.accept; + + return Array.isArray(accept) ? accept.includes(key.type) : true; +}; diff --git a/src/is-valid-pix-payload/is-valid-pix-payload.test.ts b/src/is-valid-pix-payload/is-valid-pix-payload.test.ts new file mode 100644 index 00000000..8bf7d273 --- /dev/null +++ b/src/is-valid-pix-payload/is-valid-pix-payload.test.ts @@ -0,0 +1,190 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidPixPayload } from "./is-valid-pix-payload"; + +const BACEN_STATIC = + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D"; + +const BACEN_DYNAMIC = + "00020101021226700014br.gov.bcb.pix2548pix.example.com/8b3da2f39a4140d1a91abd93113bd4415204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***630464E4"; + +const BACEN_COMPOSITE = + "00020101021226700014br.gov.bcb.pix2548pix.example.com/8b3da2f39a4140d1a91abd93113bd4415204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***80740014br.gov.bcb.pix2552pix.example.com/rec/2353c790eefb11eaadc10242ac1200026304FB42"; + +const BRCODE_MANUAL = + "00020104141234567890123426580014BR.GOV.BCB.PIX0136123e4567-e12b-12d1-a456-42665544000027300012BR.COM.OUTRO011001234567895204000053039865406123.455802BR5917NOME DO RECEBEDOR6008BRASILIA61087007490062190515RP12345678-201980390012BR.COM.OUTRO01190123.ABCD.3456.WXYZ6304AD38"; + +const COMMUNITY_STATIC = + "00020126580014br.gov.bcb.pix0136bee05743-4291-4f3c-9259-595df1307ba1520400005303986540510.005802BR5914Alexandre Lima6019Presidente Prudente62180514Um-Id-Qualquer6304D475"; + +describe("isValidPixPayload", () => { + describe("should return true", () => { + test("for the static QR Code example in the Bacen 'Manual de Padrões para Iniciação do Pix'", () => { + expect(isValidPixPayload(BACEN_STATIC)).toBe(true); + }); + + test("for the dynamic QR Code example in the Bacen 'Manual de Padrões para Iniciação do Pix'", () => { + expect(isValidPixPayload(BACEN_DYNAMIC)).toBe(true); + }); + + test("for the composite QR Code example in the Bacen 'Manual de Padrões para Iniciação do Pix'", () => { + expect(isValidPixPayload(BACEN_COMPOSITE)).toBe(true); + }); + + test("for the multi-arrangement payload from the 'Manual do BR Code' §2.2", () => { + expect(isValidPixPayload(BRCODE_MANUAL)).toBe(true); + }); + + test("for a widely published community payload with an amount and a txid", () => { + expect(isValidPixPayload(COMMUNITY_STATIC)).toBe(true); + }); + + test("when the payload is surrounded by whitespace", () => { + expect(isValidPixPayload(` ${BACEN_STATIC}\n`)).toBe(true); + }); + + test("when the CRC is written in lowercase", () => { + expect(isValidPixPayload(BACEN_STATIC.replace(/1D3D$/, "1d3d"))).toBe(true); + }); + + test("when the additional data template is absent", () => { + expect( + isValidPixPayload( + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA6304740C", + ), + ).toBe(true); + }); + }); + + describe("should return false", () => { + test("when it is an empty or blank string", () => { + expect(isValidPixPayload("")).toBe(false); + expect(isValidPixPayload(" ")).toBe(false); + }); + + test("when it is null", () => { + // @ts-expect-error + expect(isValidPixPayload(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidPixPayload(undefined)).toBe(false); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(isValidPixPayload(20250101)).toBe(false); + }); + + test("when it is a boolean, an object or an array", () => { + // @ts-expect-error + expect(isValidPixPayload(true)).toBe(false); + // @ts-expect-error + expect(isValidPixPayload({})).toBe(false); + // @ts-expect-error + expect(isValidPixPayload([])).toBe(false); + }); + + test("when the CRC does not match", () => { + expect(isValidPixPayload(BACEN_STATIC.replace(/1D3D$/, "1D3E"))).toBe(false); + }); + + test("when the CRC is not hexadecimal", () => { + expect(isValidPixPayload(BACEN_STATIC.replace(/1D3D$/, "ZZZZ"))).toBe(false); + }); + + test("when the payload does not end with the CRC object", () => { + expect(isValidPixPayload(BACEN_STATIC.slice(0, -8))).toBe(false); + }); + + test("when the TLV structure is malformed", () => { + expect(isValidPixPayload("00020126990014br.gov.bcb.pix6304BEFF")).toBe(false); + expect(isValidPixPayload("000X016304EAB2")).toBe(false); + }); + + test("when the payload format indicator is not 01", () => { + expect( + isValidPixPayload( + "00020226580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***6304BAA3", + ), + ).toBe(false); + }); + + test("when the point of initiation method is neither 11 nor 12", () => { + expect( + isValidPixPayload( + "00020101021326580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63047DC6", + ), + ).toBe(false); + }); + + test("when the currency is not 986", () => { + expect( + isValidPixPayload( + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053038405802BR5913Fulano de Tal6008BRASILIA62070503***63040C88", + ), + ).toBe(false); + }); + + test("when the country code is not BR", () => { + expect( + isValidPixPayload( + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802AR5913Fulano de Tal6008BRASILIA62070503***6304F417", + ), + ).toBe(false); + }); + + test("when the merchant category code is missing", () => { + expect( + isValidPixPayload( + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-42665544000053039865802BR5913Fulano de Tal6008BRASILIA62070503***630405E3", + ), + ).toBe(false); + }); + + test("when the merchant name is missing", () => { + expect( + isValidPixPayload( + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR6008BRASILIA62070503***630452B8", + ), + ).toBe(false); + }); + + test("when the merchant city is missing", () => { + expect( + isValidPixPayload( + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal62070503***63047718", + ), + ).toBe(false); + }); + + test("when the GUI is not br.gov.bcb.pix", () => { + expect( + isValidPixPayload( + "00020126560012br.com.outro0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63045803", + ), + ).toBe(false); + }); + + test("when the merchant account information holds neither a key nor a URL", () => { + expect( + isValidPixPayload( + "00020126180014br.gov.bcb.pix5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***6304A335", + ), + ).toBe(false); + }); + + test("when the amount is not a number", () => { + expect( + isValidPixPayload( + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-42665544000052040000530398654061R3.455802BR5913Fulano de Tal6008BRASILIA62070503***63049FEF", + ), + ).toBe(false); + }); + + test("when it is a boleto or free text", () => { + expect(isValidPixPayload("10491443385511900000200000000141325230000093423")).toBe(false); + expect(isValidPixPayload("pix copia e cola")).toBe(false); + }); + }); +}); diff --git a/src/is-valid-pix-payload/is-valid-pix-payload.ts b/src/is-valid-pix-payload/is-valid-pix-payload.ts new file mode 100644 index 00000000..37b09cad --- /dev/null +++ b/src/is-valid-pix-payload/is-valid-pix-payload.ts @@ -0,0 +1,36 @@ +import { parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; + +/** + * Validates a Pix BR Code payload, the string behind a Pix QR Code and behind "Pix copia e + * cola". + * + * The payload is valid when its TLV (tag-length-value) structure is well-formed, when the + * mandatory objects are present and well-formed (payload format indicator `01`, merchant + * category code, currency `986`, country `BR`, merchant name and merchant city), when one of + * the "Merchant Account Information" templates (IDs 26 to 51) carries the `br.gov.bcb.pix` GUI + * together with a key (static QR Code) or a URL (dynamic QR Code), and when the CRC-16 matches + * the rest of the payload. + * + * The key itself is not checked against the DICT formats: the manual states a static QR Code + * can be generated with a key that is not (or is no longer) registered, so use `isValidPixKey` + * when that matters. + * + * @param {string} value - The BR Code payload to validate. + * @returns {boolean} True if the payload is a valid Pix BR Code, false otherwise. + * + * @example + * ```typescript + * isValidPixPayload( + * "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426655440000" + + * "5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D", + * ); // true + * + * isValidPixPayload("00020126580014br.gov.bcb.pix..."); // false (broken CRC) + * ``` + * + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf + * @see Based on: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + * @see Based on: https://github.com/bacen/pix-dict-api DICT OpenAPI spec. + */ +export const isValidPixPayload = (value: string): boolean => parsePixPayload(value) !== null; diff --git a/src/parse-pix-key/constants.ts b/src/parse-pix-key/constants.ts new file mode 100644 index 00000000..c9d7c1a1 --- /dev/null +++ b/src/parse-pix-key/constants.ts @@ -0,0 +1,16 @@ +export const EMAIL_MAX_LENGTH = 77; + +/** + * A DICT random key (EVP) is a lowercase UUID written with its punctuation. The DICT issues + * version 4 UUIDs, but neither the registered pattern nor the example of the manual + * (`123e4567-e12b-12d1-a456-426655440000`, whose version nibble is `1`) constrains the + * version, so the version and variant nibbles are not enforced. + */ +export const EVP_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Marks of a value written as a phone number rather than as a document: an explicit + * international prefix (`+55` or `0055`) or a DDD wrapped in parentheses. A CPF mask uses only + * dots and a dash, so it never matches. + */ +export const PHONE_HINT_REGEX = /^(?:\+|00)\s*55|[()]/; diff --git a/src/parse-pix-key/parse-pix-key.test.ts b/src/parse-pix-key/parse-pix-key.test.ts new file mode 100644 index 00000000..21e8a26f --- /dev/null +++ b/src/parse-pix-key/parse-pix-key.test.ts @@ -0,0 +1,251 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { generateCnpj } from "../generate-cnpj/generate-cnpj"; +import { generateCpf } from "../generate-cpf/generate-cpf"; +import { generatePhone } from "../generate-phone/generate-phone"; +import { parsePixKey } from "./parse-pix-key"; + +const AMBIGUOUS = "51998259765"; + +describe("parsePixKey", () => { + describe("should return null", () => { + test("when it is an empty or blank string", () => { + expect(parsePixKey("")).toBeNull(); + expect(parsePixKey(" ")).toBeNull(); + }); + + test("when it is null", () => { + // @ts-expect-error + expect(parsePixKey(null)).toBeNull(); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(parsePixKey(undefined)).toBeNull(); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(parsePixKey(12345678909)).toBeNull(); + }); + + test("when it is a boolean, an object or an array", () => { + // @ts-expect-error + expect(parsePixKey(true)).toBeNull(); + // @ts-expect-error + expect(parsePixKey({})).toBeNull(); + // @ts-expect-error + expect(parsePixKey([])).toBeNull(); + }); + + test("when it is an invalid CPF", () => { + expect(parsePixKey("11257245286")).toBeNull(); + }); + + test("when it is an invalid CNPJ", () => { + expect(parsePixKey("11222333000182")).toBeNull(); + }); + + test("when it is an invalid e-mail", () => { + expect(parsePixKey("fulano@")).toBeNull(); + expect(parsePixKey("@example.com")).toBeNull(); + expect(parsePixKey("fulano@example")).toBeNull(); + }); + + test("when the e-mail is longer than 77 characters", () => { + expect(parsePixKey(`${"a".repeat(66)}@example.com`)).toBeNull(); + }); + + test("when the random key is not a UUID", () => { + expect(parsePixKey("71c7d9be4b854e439f1c1f3b8b4e9a2d")).toBeNull(); + expect(parsePixKey("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2")).toBeNull(); + expect(parsePixKey("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9azz")).toBeNull(); + }); + + test("when the phone has an invalid area code", () => { + expect(parsePixKey("(00) 98765-4321")).toBeNull(); + }); + + test("when it is free text", () => { + expect(parsePixKey("chave pix")).toBeNull(); + expect(parsePixKey("---")).toBeNull(); + }); + }); + + describe("should return a CPF", () => { + test("when it is masked", () => { + expect(parsePixKey("123.456.789-09")).toEqual({ type: "cpf", value: "12345678909" }); + }); + + test("when it is unmasked", () => { + expect(parsePixKey("40364478829")).toEqual({ type: "cpf", value: "40364478829" }); + }); + + test("when surrounded by whitespace", () => { + expect(parsePixKey(" 40364478829 ")).toEqual({ type: "cpf", value: "40364478829" }); + }); + }); + + describe("should return a CNPJ", () => { + test("when it is masked", () => { + expect(parsePixKey("00.038.166/0001-05")).toEqual({ + type: "cnpj", + value: "00038166000105", + }); + }); + + test("when it is unmasked", () => { + expect(parsePixKey("00038166000105")).toEqual({ + type: "cnpj", + value: "00038166000105", + }); + }); + + test("when it is the alphanumeric format of the manual", () => { + expect(parsePixKey("12ABC34501DE35")).toEqual({ type: "cnpj", value: "12ABC34501DE35" }); + expect(parsePixKey("12.abc.345/01de-35")).toEqual({ + type: "cnpj", + value: "12ABC34501DE35", + }); + }); + }); + + describe("should return an e-mail", () => { + test("when it is the example of the manual", () => { + expect(parsePixKey("fulano_da_silva.recebedor@example.com")).toEqual({ + type: "email", + value: "fulano_da_silva.recebedor@example.com", + }); + }); + + test("when it is uppercased or padded", () => { + expect(parsePixKey(" Fulano@Example.COM ")).toEqual({ + type: "email", + value: "fulano@example.com", + }); + }); + + test("when it is exactly 77 characters long", () => { + const email = `${"a".repeat(65)}@example.com`; + + expect(email).toHaveLength(77); + expect(parsePixKey(email)).toEqual({ type: "email", value: email }); + }); + }); + + describe("should return a phone", () => { + test("when it is the example of the manual", () => { + expect(parsePixKey("+5561912345678")).toEqual({ + type: "phone", + value: "+5561912345678", + }); + }); + + test("when it is masked", () => { + expect(parsePixKey("(11) 98765-4321")).toEqual({ + type: "phone", + value: "+5511987654321", + }); + }); + + test("when it is bare", () => { + expect(parsePixKey("11987654321")).toEqual({ type: "phone", value: "+5511987654321" }); + }); + + test("when it carries the country code in every accepted form", () => { + expect(parsePixKey("+55 11 98765-4321")).toEqual({ + type: "phone", + value: "+5511987654321", + }); + expect(parsePixKey("005511987654321")).toEqual({ + type: "phone", + value: "+5511987654321", + }); + expect(parsePixKey("5511987654321")).toEqual({ + type: "phone", + value: "+5511987654321", + }); + }); + + test("when it is a landline", () => { + expect(parsePixKey("(11) 3000-0000")).toEqual({ type: "phone", value: "+551130000000" }); + }); + + test("and never exceed the 14 characters of the E.164 form", () => { + for (let index = 0; index < 200; index++) { + const key = parsePixKey(`+55${generatePhone()}`); + + expect(key?.type).toBe("phone"); + expect(key?.value.length).toBeLessThanOrEqual(14); + } + }); + }); + + describe("should return a random key", () => { + test("when it is a lowercase UUID version 4", () => { + expect(parsePixKey("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d")).toEqual({ + type: "evp", + value: "71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d", + }); + }); + + test("when it is uppercased, lowercasing it", () => { + expect(parsePixKey("71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D")).toEqual({ + type: "evp", + value: "71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d", + }); + }); + + test("when it is the example of the manual, whose version nibble is not 4", () => { + expect(parsePixKey("123e4567-e12b-12d1-a456-426655440000")).toEqual({ + type: "evp", + value: "123e4567-e12b-12d1-a456-426655440000", + }); + }); + }); + + describe("should resolve the CPF and phone ambiguity", () => { + test("preferring the CPF when the value is valid as both", () => { + expect(parsePixKey(AMBIGUOUS)).toEqual({ type: "cpf", value: AMBIGUOUS }); + }); + + test("preferring the phone when it starts with the country code", () => { + expect(parsePixKey(`+55${AMBIGUOUS}`)).toEqual({ + type: "phone", + value: `+55${AMBIGUOUS}`, + }); + expect(parsePixKey(`0055${AMBIGUOUS}`)).toEqual({ + type: "phone", + value: `+55${AMBIGUOUS}`, + }); + }); + + test("preferring the phone when the DDD is written between parentheses", () => { + expect(parsePixKey("(51) 99825-9765")).toEqual({ + type: "phone", + value: `+55${AMBIGUOUS}`, + }); + }); + + test("keeping the CPF when it is written with its own mask", () => { + expect(parsePixKey("519.982.597-65")).toEqual({ type: "cpf", value: AMBIGUOUS }); + }); + }); + + describe("should normalize randomized keys", () => { + test("for CPFs", () => { + for (let index = 0; index < 200; index++) { + const cpf = generateCpf(); + + expect(parsePixKey(cpf)?.value).toBe(cpf); + } + }); + + test("for CNPJs", () => { + for (let index = 0; index < 200; index++) { + const cnpj = generateCnpj(); + + expect(parsePixKey(cnpj)).toEqual({ type: "cnpj", value: cnpj }); + } + }); + }); +}); diff --git a/src/parse-pix-key/parse-pix-key.ts b/src/parse-pix-key/parse-pix-key.ts new file mode 100644 index 00000000..ecf37555 --- /dev/null +++ b/src/parse-pix-key/parse-pix-key.ts @@ -0,0 +1,89 @@ +import { CPF_LENGTH } from "../_internals/constants/cpf"; +import { PHONE_COUNTRY_CODE } from "../_internals/constants/phone"; +import { normalizePhone } from "../_internals/normalize-phone/normalize-phone"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { isValidCnpj } from "../is-valid-cnpj/is-valid-cnpj"; +import { isValidCpf } from "../is-valid-cpf/is-valid-cpf"; +import { isValidEmail } from "../is-valid-email/is-valid-email"; +import { isValidPhone } from "../is-valid-phone/is-valid-phone"; +import { parseCnpj } from "../parse-cnpj/parse-cnpj"; +import { EMAIL_MAX_LENGTH, EVP_REGEX, PHONE_HINT_REGEX } from "./constants"; + +export type PixKeyType = "cpf" | "cnpj" | "email" | "phone" | "evp"; + +export type PixKey = { + /** Which kind of Pix key the value was recognized as. */ + type: PixKeyType; + /** The key in the canonical DICT form for its kind. */ + value: string; +}; + +/** + * Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR + * Code. + * + * The canonical forms are the ones listed in "Formatação das chaves do DICT no BR Code": + * - `cpf`: 11 digits, no mask; + * - `cnpj`: 14 characters, no mask, uppercase for the alphanumeric format; + * - `email`: trimmed and lowercased, at most 77 characters; + * - `phone`: E.164, `+55` followed by the DDD and the subscriber number, so at most 14 + * characters. Masked, bare and `+55` prefixed inputs are all accepted; + * - `evp`: the random key, a lowercase UUID version 4. + * + * An 11 digit value can be read both as a CPF and as a mobile phone number. When it is valid + * as both, it is read as a CPF, unless it was written as a phone number, i.e. unless it starts + * with `+55`/`0055` or wraps its DDD in parentheses. + * + * @param {string} value - The Pix key to be parsed. + * @returns {PixKey|null} The normalized key, or `null` when the value is not a valid Pix key. + * + * @example + * ```typescript + * parsePixKey("123.456.789-09"); // { type: "cpf", value: "12345678909" } + * parsePixKey("Fulano@Example.COM "); // { type: "email", value: "fulano@example.com" } + * parsePixKey("(11) 98765-4321"); // { type: "phone", value: "+5511987654321" } + * parsePixKey("71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D"); + * // { type: "evp", value: "71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d" } + * parsePixKey("51998259765"); // { type: "cpf", value: "51998259765" } (also a valid phone) + * parsePixKey("+5551998259765"); // { type: "phone", value: "+5551998259765" } + * ``` + * + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf + * @see Based on: https://github.com/bacen/pix-dict-api DICT (Diretório de Identificadores de + * Contas Transacionais) OpenAPI spec, key format reference. + * @see Based on: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + */ +export const parsePixKey = (value: string): PixKey | null => { + if (typeof value !== "string") return null; + + const trimmed = value.trim(); + + if (!trimmed) return null; + + if (EVP_REGEX.test(trimmed)) return { type: "evp", value: trimmed.toLowerCase() }; + + if (trimmed.includes("@")) { + const email = trimmed.toLowerCase(); + + return isValidEmail(email) && email.length <= EMAIL_MAX_LENGTH + ? { type: "email", value: email } + : null; + } + + const national = normalizePhone(trimmed); + const phone: PixKey | null = isValidPhone(national) + ? { type: "phone", value: `+${PHONE_COUNTRY_CODE}${national}` } + : null; + + if (phone && PHONE_HINT_REGEX.test(trimmed)) return phone; + + if (isValidCnpj(trimmed, { version: 2 })) { + return { type: "cnpj", value: parseCnpj(trimmed, { version: 2 }) }; + } + + const digits = sanitizeToDigits(trimmed); + + if (digits.length === CPF_LENGTH && isValidCpf(digits)) return { type: "cpf", value: digits }; + + return phone; +}; diff --git a/src/parse-pix-payload/parse-pix-payload.test.ts b/src/parse-pix-payload/parse-pix-payload.test.ts new file mode 100644 index 00000000..563d18aa --- /dev/null +++ b/src/parse-pix-payload/parse-pix-payload.test.ts @@ -0,0 +1,203 @@ +import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; +import { describe, expect, test } from "../_internals/test/runtime"; +import { generateCpf } from "../generate-cpf/generate-cpf"; +import { generatePixPayload } from "../generate-pix-payload/generate-pix-payload"; +import { parsePixPayload } from "./parse-pix-payload"; + +const BACEN_STATIC = + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D"; + +const BACEN_DYNAMIC = + "00020101021226700014br.gov.bcb.pix2548pix.example.com/8b3da2f39a4140d1a91abd93113bd4415204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***630464E4"; + +const BACEN_COMPOSITE = + "00020101021226700014br.gov.bcb.pix2548pix.example.com/8b3da2f39a4140d1a91abd93113bd4415204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***80740014br.gov.bcb.pix2552pix.example.com/rec/2353c790eefb11eaadc10242ac1200026304FB42"; + +const BRCODE_MANUAL = + "00020104141234567890123426580014BR.GOV.BCB.PIX0136123e4567-e12b-12d1-a456-42665544000027300012BR.COM.OUTRO011001234567895204000053039865406123.455802BR5917NOME DO RECEBEDOR6008BRASILIA61087007490062190515RP12345678-201980390012BR.COM.OUTRO01190123.ABCD.3456.WXYZ6304AD38"; + +const COMMUNITY_STATIC = + "00020126580014br.gov.bcb.pix0136bee05743-4291-4f3c-9259-595df1307ba1520400005303986540510.005802BR5914Alexandre Lima6019Presidente Prudente62180514Um-Id-Qualquer6304D475"; + +const STATIC_POINT_OF_INITIATION = + "00020101021126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***630448CD"; + +const tlv = (id: string, value: string): string => + `${id}${value.length.toString().padStart(2, "0")}${value}`; + +const buildPayload = (merchantAccountInformation: string, additionalData?: string): string => { + const withoutCrc = + tlv("00", "01") + + tlv("26", merchantAccountInformation) + + tlv("52", "0000") + + tlv("53", "986") + + tlv("58", "BR") + + tlv("59", "Fulano de Tal") + + tlv("60", "BRASILIA") + + (additionalData !== undefined ? tlv("62", additionalData) : "") + + "6304"; + + return withoutCrc + crc16Ccitt(withoutCrc); +}; + +describe("parsePixPayload", () => { + describe("should return null", () => { + test("when it is an empty or blank string", () => { + expect(parsePixPayload("")).toBeNull(); + expect(parsePixPayload(" ")).toBeNull(); + }); + + test("when it is null", () => { + // @ts-expect-error + expect(parsePixPayload(null)).toBeNull(); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(parsePixPayload(undefined)).toBeNull(); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(parsePixPayload(20250101)).toBeNull(); + }); + + test("when it is a boolean, an object or an array", () => { + // @ts-expect-error + expect(parsePixPayload(true)).toBeNull(); + // @ts-expect-error + expect(parsePixPayload({})).toBeNull(); + // @ts-expect-error + expect(parsePixPayload([])).toBeNull(); + }); + + test("when the CRC does not match", () => { + expect(parsePixPayload(BACEN_STATIC.replace(/1D3D$/, "1D3E"))).toBeNull(); + }); + + test("when it is free text", () => { + expect(parsePixPayload("pix copia e cola")).toBeNull(); + }); + + test("when the key object is present but empty", () => { + const merchantAccountInformation = tlv("00", "br.gov.bcb.pix") + tlv("01", ""); + + expect(parsePixPayload(buildPayload(merchantAccountInformation))).toBeNull(); + }); + + test("when the url object is present but empty", () => { + const merchantAccountInformation = tlv("00", "br.gov.bcb.pix") + tlv("25", ""); + + expect(parsePixPayload(buildPayload(merchantAccountInformation))).toBeNull(); + }); + + test("when the additional data template is malformed", () => { + const merchantAccountInformation = tlv("00", "br.gov.bcb.pix") + tlv("01", "some-key"); + + expect(parsePixPayload(buildPayload(merchantAccountInformation, "9"))).toBeNull(); + }); + }); + + describe("should parse a static payload", () => { + test("from the static QR Code example in the Bacen 'Manual de Padrões para Iniciação do Pix'", () => { + expect(parsePixPayload(BACEN_STATIC)).toEqual({ + key: "123e4567-e12b-12d1-a456-426655440000", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", + }); + }); + + test("dropping the *** placeholder of an absent txid", () => { + expect(parsePixPayload(BACEN_STATIC)).not.toHaveProperty("txid"); + }); + + test("with an amount and a txid, as in a widely published community example", () => { + expect(parsePixPayload(COMMUNITY_STATIC)).toEqual({ + key: "bee05743-4291-4f3c-9259-595df1307ba1", + merchantName: "Alexandre Lima", + merchantCity: "Presidente Prudente", + amount: 10, + txid: "Um-Id-Qualquer", + }); + }); + + test("picking the Pix arrangement out of the multi-arrangement payload from the 'Manual do BR Code' §2.2", () => { + expect(parsePixPayload(BRCODE_MANUAL)).toEqual({ + key: "123e4567-e12b-12d1-a456-426655440000", + merchantName: "NOME DO RECEBEDOR", + merchantCity: "BRASILIA", + amount: 123.45, + txid: "RP12345678-2019", + }); + }); + + test("with a description", () => { + const payload = generatePixPayload({ + key: "12345678909", + merchantName: "Fulano de Tal", + merchantCity: "Brasilia", + description: "Pedido 42", + }); + + expect(parsePixPayload(payload ?? "")).toEqual({ + key: "12345678909", + description: "Pedido 42", + merchantName: "Fulano de Tal", + merchantCity: "Brasilia", + }); + }); + }); + + describe("should parse a dynamic payload", () => { + test("from the dynamic QR Code example in the Bacen 'Manual de Padrões para Iniciação do Pix'", () => { + expect(parsePixPayload(BACEN_DYNAMIC)).toEqual({ + url: "pix.example.com/8b3da2f39a4140d1a91abd93113bd441", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", + pointOfInitiation: "dynamic", + }); + }); + + test("picking the Pix arrangement out of the composite QR Code example in the Bacen manual", () => { + expect(parsePixPayload(BACEN_COMPOSITE)).toEqual({ + url: "pix.example.com/8b3da2f39a4140d1a91abd93113bd441", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", + pointOfInitiation: "dynamic", + }); + }); + + test("reading the point of initiation method 11 as static, per the Bacen static example with it made explicit", () => { + expect(parsePixPayload(STATIC_POINT_OF_INITIATION)?.pointOfInitiation).toBe("static"); + }); + }); + + describe("should round-trip with generatePixPayload", () => { + test("for a payload with every field", () => { + const pix = { + key: "12345678909", + description: "Pedido 42", + merchantName: "Fulano de Tal", + merchantCity: "Brasilia", + amount: 123.45, + txid: "RP123456782019", + }; + + expect(parsePixPayload(generatePixPayload(pix) ?? "")).toEqual(pix); + }); + + test("for randomized CPF keys", () => { + for (let index = 0; index < 200; index++) { + const pix = { + key: generateCpf(), + merchantName: "Fulano de Tal", + merchantCity: "Brasilia", + amount: Number(((index + 1) / 100).toFixed(2)), + txid: `TX${index}`, + }; + + expect(parsePixPayload(generatePixPayload(pix) ?? "")).toEqual(pix); + } + }); + }); +}); diff --git a/src/parse-pix-payload/parse-pix-payload.ts b/src/parse-pix-payload/parse-pix-payload.ts new file mode 100644 index 00000000..12c511d6 --- /dev/null +++ b/src/parse-pix-payload/parse-pix-payload.ts @@ -0,0 +1,205 @@ +import { + PIX_ABSENT_TXID, + PIX_ADDITIONAL_DATA_ID, + PIX_COUNTRY_CODE, + PIX_COUNTRY_CODE_ID, + PIX_CRC_LENGTH, + PIX_CRC_TAG, + PIX_DESCRIPTION_ID, + PIX_DYNAMIC_POINT_OF_INITIATION, + PIX_GUI, + PIX_GUI_ID, + PIX_KEY_ID, + PIX_MERCHANT_ACCOUNT_INFORMATION_FIRST_ID, + PIX_MERCHANT_ACCOUNT_INFORMATION_LAST_ID, + PIX_MERCHANT_CATEGORY_CODE_ID, + PIX_MERCHANT_CITY_ID, + PIX_MERCHANT_NAME_ID, + PIX_PAYLOAD_FORMAT_INDICATOR, + PIX_PAYLOAD_FORMAT_INDICATOR_ID, + PIX_POINT_OF_INITIATION_ID, + PIX_STATIC_POINT_OF_INITIATION, + PIX_TRANSACTION_AMOUNT_ID, + PIX_TRANSACTION_AMOUNT_MAX_LENGTH, + PIX_TRANSACTION_CURRENCY, + PIX_TRANSACTION_CURRENCY_ID, + PIX_TXID_ID, + PIX_URL_ID, +} from "../_internals/constants/pix"; +import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; +import { type TlvFields, parseTlv } from "../_internals/parse-tlv/parse-tlv"; + +export type PixPointOfInitiation = "static" | "dynamic"; + +export type PixPayload = { + /** The Pix key of the receiver, present in a static payload. */ + key?: string; + /** URL of the dynamic payload, present instead of `key` in a dynamic one. */ + url?: string; + /** Free text the receiver wrote for the payer. */ + description?: string; + /** Name of the receiver, at most 25 ASCII characters. */ + merchantName: string; + /** City of the receiver, at most 15 ASCII characters. */ + merchantCity: string; + /** Amount in BRL, absent when the payer types it. */ + amount?: number; + /** Transaction ID, absent when the payload carries the `***` marker. */ + txid?: string; + /** Whether the payload may be paid once ("dynamic") or many times ("static"). */ + pointOfInitiation?: PixPointOfInitiation; +}; + +const CRC_VALUE_REGEX = /^[0-9a-f]{4}$/i; + +const AMOUNT_REGEX = /^\d+(?:\.\d{1,2})?$/; + +const CRC_TAG_LENGTH = PIX_CRC_TAG.length + PIX_CRC_LENGTH; + +const findMerchantAccountInformation = (fields: TlvFields): TlvFields | null => { + for ( + let id = PIX_MERCHANT_ACCOUNT_INFORMATION_FIRST_ID; + id <= PIX_MERCHANT_ACCOUNT_INFORMATION_LAST_ID; + id++ + ) { + const template = fields[id.toString()]; + + if (template === undefined) continue; + + const objects = parseTlv(template); + + if (objects?.[PIX_GUI_ID]?.toLowerCase() === PIX_GUI) return objects; + } + + return null; +}; + +const isValidCrc = (payload: string): boolean => { + const checksum = payload.slice(-PIX_CRC_LENGTH); + + if (payload.slice(-CRC_TAG_LENGTH, -PIX_CRC_LENGTH) !== PIX_CRC_TAG) return false; + if (!CRC_VALUE_REGEX.test(checksum)) return false; + + return crc16Ccitt(payload.slice(0, -PIX_CRC_LENGTH)) === checksum.toUpperCase(); +}; + +/** + * Parses a Pix BR Code payload, the string behind a Pix QR Code and behind "Pix copia e cola". + * + * The payload is rejected when its TLV (tag-length-value) structure is malformed, when the CRC + * does not match, when a mandatory object is missing or malformed, or when none of the + * "Merchant Account Information" templates (IDs 26 to 51) carries the `br.gov.bcb.pix` GUI + * together with either a key (static) or a URL (dynamic). + * + * The Pix key itself is not validated: the manual states a static QR Code can be generated + * with a key that no longer exists in the DICT, so key ownership is only settled at payment + * time. The "Additional Data Field Template" (ID 62) is mandatory in the BR Code table but + * optional in the EMV® specification it refers to, so it is accepted when absent. The lengths + * the manual reserves for the merchant name (25), the merchant city (15) and the `txid` (25) + * are generator side limits, enforced by `generatePixPayload`; payloads in the wild routinely + * overrun them, so they are not enforced here. + * + * @param {string} value - The BR Code payload to be parsed. + * @returns {PixPayload|null} The Pix data of the payload, or `null` when it is not a valid Pix + * BR Code. + * + * @example + * ```typescript + * parsePixPayload( + * "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426655440000" + + * "5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D", + * ); + * // { + * // key: "123e4567-e12b-12d1-a456-426655440000", + * // merchantName: "Fulano de Tal", + * // merchantCity: "BRASILIA", + * // } + * ``` + * + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf + * @see Based on: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + * @see Based on: https://github.com/bacen/pix-dict-api DICT OpenAPI spec. + */ +export const parsePixPayload = (value: string): PixPayload | null => { + if (typeof value !== "string") return null; + + const payload = value.trim(); + + if (payload.length <= CRC_TAG_LENGTH || !isValidCrc(payload)) return null; + + const fields = parseTlv(payload); + + if (!fields) return null; + + if (fields[PIX_PAYLOAD_FORMAT_INDICATOR_ID] !== PIX_PAYLOAD_FORMAT_INDICATOR) return null; + + const pointOfInitiation = fields[PIX_POINT_OF_INITIATION_ID]; + + if ( + pointOfInitiation !== undefined && + pointOfInitiation !== PIX_STATIC_POINT_OF_INITIATION && + pointOfInitiation !== PIX_DYNAMIC_POINT_OF_INITIATION + ) { + return null; + } + + if (fields[PIX_MERCHANT_CATEGORY_CODE_ID] === undefined) return null; + if (fields[PIX_TRANSACTION_CURRENCY_ID] !== PIX_TRANSACTION_CURRENCY) return null; + if (fields[PIX_COUNTRY_CODE_ID]?.toUpperCase() !== PIX_COUNTRY_CODE) return null; + + const merchantName = fields[PIX_MERCHANT_NAME_ID]; + + if (!merchantName) return null; + + const merchantCity = fields[PIX_MERCHANT_CITY_ID]; + + if (!merchantCity) return null; + + const amount = fields[PIX_TRANSACTION_AMOUNT_ID]; + + if ( + amount !== undefined && + (!AMOUNT_REGEX.test(amount) || amount.length > PIX_TRANSACTION_AMOUNT_MAX_LENGTH) + ) { + return null; + } + + const merchantAccountInformation = findMerchantAccountInformation(fields); + + if (!merchantAccountInformation) return null; + + const key = merchantAccountInformation[PIX_KEY_ID]; + const url = merchantAccountInformation[PIX_URL_ID]; + const description = merchantAccountInformation[PIX_DESCRIPTION_ID]; + + if (key === undefined && url === undefined) return null; + if (key !== undefined && !key) return null; + if (url !== undefined && !url) return null; + + const additionalData = fields[PIX_ADDITIONAL_DATA_ID]; + + let txid: string | undefined; + + if (additionalData !== undefined) { + const objects = parseTlv(additionalData); + + if (!objects) return null; + + txid = objects[PIX_TXID_ID]; + } + + const pix: PixPayload = { merchantName, merchantCity }; + + if (key !== undefined) pix.key = key; + if (url !== undefined) pix.url = url; + if (description !== undefined) pix.description = description; + if (amount !== undefined) pix.amount = Number(amount); + if (txid !== undefined && txid !== PIX_ABSENT_TXID) pix.txid = txid; + + if (pointOfInitiation !== undefined) { + pix.pointOfInitiation = + pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION ? "dynamic" : "static"; + } + + return pix; +}; From fa2c794b957e68e426abc1e176f94d2ec78da9fa Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:53 -0300 Subject: [PATCH 03/22] feat(municipality): add getMunicipalities and getMunicipalityByCode (offline IBGE data) Both resolve against the bundled IBGE dataset, with no network request (unlike getMunicipality, which always calls the IBGE API). --- .../get-municipalities.test.ts | 95 +++++++++++++++++++ src/get-municipalities/get-municipalities.ts | 40 ++++++++ .../get-municipality-by-code.test.ts | 70 ++++++++++++++ .../get-municipality-by-code.ts | 40 ++++++++ 4 files changed, 245 insertions(+) create mode 100644 src/get-municipalities/get-municipalities.test.ts create mode 100644 src/get-municipalities/get-municipalities.ts create mode 100644 src/get-municipality-by-code/get-municipality-by-code.test.ts create mode 100644 src/get-municipality-by-code/get-municipality-by-code.ts diff --git a/src/get-municipalities/get-municipalities.test.ts b/src/get-municipalities/get-municipalities.test.ts new file mode 100644 index 00000000..9371c64a --- /dev/null +++ b/src/get-municipalities/get-municipalities.test.ts @@ -0,0 +1,95 @@ +import { DATA } from "../_internals/constants/cities"; +import { describe, expect, it } from "../_internals/test/runtime"; +import { getStates } from "../get-states/get-states"; +import { getMunicipalities } from "./get-municipalities"; + +const NUMBER_OF_BRAZILIAN_MUNICIPALITIES = 5571; + +const KNOWN_STATE_MUNICIPALITY_COUNTS: Record = { + MG: 853, + MT: 142, + RS: 497, + SP: 645, +}; + +describe("getMunicipalities", () => { + it("should return every municipality when no state is given", () => { + expect(getMunicipalities().length).toBe(NUMBER_OF_BRAZILIAN_MUNICIPALITIES); + }); + + it("should sort the combined list with the pt-BR comparator", () => { + const municipalities = getMunicipalities(); + const names = municipalities.map((municipality) => municipality.name); + const sortedNames = [...names].sort((a, b) => a.localeCompare(b, "pt-BR")); + + expect(names).toEqual(sortedNames); + }); + + it("should return municipality objects shaped as { code, name, stateCode }", () => { + const saoPaulo = getMunicipalities("SP").find( + (municipality) => municipality.name === "São Paulo", + ); + + expect(saoPaulo).toEqual({ code: "3550308", name: "São Paulo", stateCode: "SP" }); + }); + + it("should filter municipalities by state", () => { + for (const [stateCode, expectedCount] of Object.entries(KNOWN_STATE_MUNICIPALITY_COUNTS)) { + expect(getMunicipalities(stateCode).length).toBe(expectedCount); + } + }); + + it("should include Boa Esperança do Norte/MT", () => { + const municipalities = getMunicipalities("MT"); + + expect(municipalities).toContainEqual({ + code: "5101837", + name: "Boa Esperança do Norte", + stateCode: "MT", + }); + }); + + it("should return an empty array for an unknown state", () => { + expect(getMunicipalities("ZZ")).toEqual([]); + }); + + it("should return an empty array for inherited Object property names instead of throwing", () => { + expect(getMunicipalities("toString")).toEqual([]); + expect(getMunicipalities("constructor")).toEqual([]); + }); + + it("should return a fresh copy so mutating the result does not affect subsequent calls", () => { + const all = getMunicipalities(); + all.push({ code: "0000000", name: "MUTATED", stateCode: "SP" }); + + expect(getMunicipalities().length).toBe(NUMBER_OF_BRAZILIAN_MUNICIPALITIES); + + const spMunicipalities = getMunicipalities("SP"); + spMunicipalities[0].name = "MUTATED"; + + expect(getMunicipalities("SP")[0].name).not.toBe("MUTATED"); + }); + + describe("data integrity (IBGE, https://servicodados.ibge.gov.br/api/docs/localidades)", () => { + it(`should total exactly ${NUMBER_OF_BRAZILIAN_MUNICIPALITIES} municipalities across all states`, () => { + const total = Object.values(DATA).reduce( + (sum, municipalities) => sum + municipalities.length, + 0, + ); + + expect(total).toBe(NUMBER_OF_BRAZILIAN_MUNICIPALITIES); + }); + + for (const { code } of getStates()) { + it(`should return municipalities matching DATA for state ${code}`, () => { + const expected = DATA[code].map(([name, municipalityCode]) => ({ + code: municipalityCode, + name, + stateCode: code, + })); + + expect(getMunicipalities(code)).toEqual(expected); + }); + } + }); +}); diff --git a/src/get-municipalities/get-municipalities.ts b/src/get-municipalities/get-municipalities.ts new file mode 100644 index 00000000..0e7272ad --- /dev/null +++ b/src/get-municipalities/get-municipalities.ts @@ -0,0 +1,40 @@ +import { DATA as CITIES_DATA, type Municipality } from "../_internals/constants/cities"; +import type { StateCode } from "../_internals/constants/states"; +import { getStates } from "../get-states/get-states"; + +const buildMunicipalities = (stateCode: StateCode): Municipality[] => + CITIES_DATA[stateCode].map(([name, code]) => ({ code, name, stateCode })); + +/** + * Returns Brazilian municipalities published by the IBGE, optionally filtered by state. + * + * If `stateCode` is provided, only municipalities of that state are returned. If it is + * omitted, every municipality of every state is returned, sorted with `localeCompare` in the + * "pt-BR" locale so accented names land where a Brazilian reader expects them. + * + * @param {string} [stateCode] - The two letter code of the Brazilian state to filter by. + * @returns {Municipality[]} A fresh array of fresh `Municipality` objects. Empty when + * `stateCode` is not a known state. + * + * @example + * ```typescript + * getMunicipalities("SP")[0]; // { code: "3500105", name: "Adamantina", stateCode: "SP" } + * getMunicipalities().length; // every municipality of every state + * getMunicipalities("ZZ"); // [] + * ``` + * + * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades + */ +export const getMunicipalities = (stateCode?: string): Municipality[] => { + if (stateCode === undefined) { + return getStates() + .flatMap((state) => buildMunicipalities(state.code)) + .sort((a, b) => a.name.localeCompare(b.name, "pt-BR")); + } + + const state = getStates().find((candidate) => candidate.code === stateCode); + + if (!state) return []; + + return buildMunicipalities(state.code); +}; diff --git a/src/get-municipality-by-code/get-municipality-by-code.test.ts b/src/get-municipality-by-code/get-municipality-by-code.test.ts new file mode 100644 index 00000000..c158c93d --- /dev/null +++ b/src/get-municipality-by-code/get-municipality-by-code.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getMunicipalityByCode } from "./get-municipality-by-code"; + +describe("getMunicipalityByCode", () => { + it("should return the municipality for a known code (string)", () => { + expect(getMunicipalityByCode("3550308")).toEqual({ + code: "3550308", + name: "São Paulo", + stateCode: "SP", + }); + }); + + it("should return the municipality for a known code (number)", () => { + expect(getMunicipalityByCode(3550308)).toEqual({ + code: "3550308", + name: "São Paulo", + stateCode: "SP", + }); + }); + + it("should resolve Boa Esperança do Norte/MT", () => { + expect(getMunicipalityByCode("5101837")).toEqual({ + code: "5101837", + name: "Boa Esperança do Norte", + stateCode: "MT", + }); + }); + + it("should return a fresh object so mutating the result does not affect subsequent calls", () => { + const municipality = getMunicipalityByCode("3550308"); + + if (municipality) municipality.name = "MUTATED"; + + expect(getMunicipalityByCode("3550308")?.name).toBe("São Paulo"); + }); + + it("should return null for an unknown 7 digit code", () => { + expect(getMunicipalityByCode("0000000")).toBeNull(); + }); + + it("should return null for a code with the wrong number of digits", () => { + expect(getMunicipalityByCode("123")).toBeNull(); + expect(getMunicipalityByCode("12345678")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getMunicipalityByCode("")).toBeNull(); + }); + + it("should return null for a non-string, non-number value", () => { + // @ts-expect-error + expect(getMunicipalityByCode(null)).toBeNull(); + // @ts-expect-error + expect(getMunicipalityByCode(undefined)).toBeNull(); + // @ts-expect-error + expect(getMunicipalityByCode(true)).toBeNull(); + // @ts-expect-error + expect(getMunicipalityByCode({})).toBeNull(); + // @ts-expect-error + expect(getMunicipalityByCode([])).toBeNull(); + }); + + it("should ignore non-digit characters before validating the length", () => { + expect(getMunicipalityByCode("355-030-8")).toEqual({ + code: "3550308", + name: "São Paulo", + stateCode: "SP", + }); + }); +}); diff --git a/src/get-municipality-by-code/get-municipality-by-code.ts b/src/get-municipality-by-code/get-municipality-by-code.ts new file mode 100644 index 00000000..7b107b2e --- /dev/null +++ b/src/get-municipality-by-code/get-municipality-by-code.ts @@ -0,0 +1,40 @@ +import { DATA as CITIES_DATA, type Municipality } from "../_internals/constants/cities"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { getStates } from "../get-states/get-states"; + +const CODE_LENGTH = 7; + +/** + * Looks up a Brazilian municipality by its 7 digit IBGE code, published by the IBGE. + * + * @param {string|number} code - The 7 digit IBGE municipality code, as a string or a number. + * @returns {Municipality|null} A fresh copy of the matching municipality, or `null` when + * `code` is not a 7 digit code or does not match any known municipality. + * + * @example + * ```typescript + * getMunicipalityByCode("3550308"); // { code: "3550308", name: "São Paulo", stateCode: "SP" } + * getMunicipalityByCode(3550308); // { code: "3550308", name: "São Paulo", stateCode: "SP" } + * getMunicipalityByCode("0000000"); // null + * ``` + * + * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades + */ +export const getMunicipalityByCode = (code: string | number): Municipality | null => { + if (isNullish(code) || (typeof code !== "string" && typeof code !== "number")) return null; + + const digits = sanitizeToDigits(code); + + if (digits.length !== CODE_LENGTH) return null; + + for (const state of getStates()) { + const match = CITIES_DATA[state.code].find( + ([, municipalityCode]) => municipalityCode === digits, + ); + + if (match) return { code: digits, name: match[0], stateCode: state.code }; + } + + return null; +}; From 96f3a106c261895cf680dd37740aa9266d011edb Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:53 -0300 Subject: [PATCH 04/22] feat(states): add getStateByIbgeCode, getStateCodeByName, getStateNameByCode and getTimezoneByState MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 3 lookups resolve a UF by IBGE code / name / code, accent- and case-insensitive. getTimezoneByState resolves the IANA timezone(s) for a state (Brasília, Amazonas, Acre and Fernando de Noronha all differ from the rest of the country). --- .../get-state-by-ibge-code.test.ts | 70 ++++++++++++ .../get-state-by-ibge-code.ts | 42 +++++++ .../get-state-code-by-name.test.ts | 65 +++++++++++ .../get-state-code-by-name.ts | 34 ++++++ .../get-state-name-by-code.test.ts | 52 +++++++++ .../get-state-name-by-code.ts | 33 ++++++ src/get-timezone-by-state/constants.ts | 42 +++++++ .../get-timezone-by-state.test.ts | 107 ++++++++++++++++++ .../get-timezone-by-state.ts | 37 ++++++ 9 files changed, 482 insertions(+) create mode 100644 src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts create mode 100644 src/get-state-by-ibge-code/get-state-by-ibge-code.ts create mode 100644 src/get-state-code-by-name/get-state-code-by-name.test.ts create mode 100644 src/get-state-code-by-name/get-state-code-by-name.ts create mode 100644 src/get-state-name-by-code/get-state-name-by-code.test.ts create mode 100644 src/get-state-name-by-code/get-state-name-by-code.ts create mode 100644 src/get-timezone-by-state/constants.ts create mode 100644 src/get-timezone-by-state/get-timezone-by-state.test.ts create mode 100644 src/get-timezone-by-state/get-timezone-by-state.ts diff --git a/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts b/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts new file mode 100644 index 00000000..762bf5c2 --- /dev/null +++ b/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getStateByIbgeCode } from "./get-state-by-ibge-code"; + +describe("getStateByIbgeCode", () => { + it("should return São Paulo for the string code 35, the cUF used in NF-e access keys (MOC 7)", () => { + expect(getStateByIbgeCode("35")).toEqual({ + code: "SP", + name: "São Paulo", + regionCode: "SE", + regionName: "Sudeste", + ibgeCode: 35, + }); + }); + + it("should return São Paulo for the number code 35", () => { + expect(getStateByIbgeCode(35)).toEqual({ + code: "SP", + name: "São Paulo", + regionCode: "SE", + regionName: "Sudeste", + ibgeCode: 35, + }); + }); + + it("should return Rondônia for the code 11, the first cUF in the IBGE table", () => { + expect(getStateByIbgeCode("11")?.code).toBe("RO"); + }); + + it("should return Distrito Federal for the code 53, the last cUF in the IBGE table", () => { + expect(getStateByIbgeCode("53")?.code).toBe("DF"); + }); + + it("should return a fresh copy that does not mutate the underlying constant", () => { + const state = getStateByIbgeCode("35"); + if (state) Object.assign(state, { name: "X" }); + + expect(getStateByIbgeCode("35")?.name).toBe("São Paulo"); + }); + + it("should strip a leading zero before matching", () => { + expect(getStateByIbgeCode("035")?.code).toBe("SP"); + }); + + it("should return null for a code with no matching state", () => { + expect(getStateByIbgeCode("00")).toBeNull(); + expect(getStateByIbgeCode("99")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getStateByIbgeCode("")).toBeNull(); + }); + + it("should return null for whitespace only", () => { + expect(getStateByIbgeCode(" ")).toBeNull(); + }); + + it("should return null for null", () => { + // @ts-expect-error + expect(getStateByIbgeCode(null)).toBeNull(); + }); + + it("should return null for undefined", () => { + // @ts-expect-error + expect(getStateByIbgeCode(undefined)).toBeNull(); + }); + + it("should ignore non-digit characters around the code", () => { + expect(getStateByIbgeCode(" 35 ")?.code).toBe("SP"); + }); +}); diff --git a/src/get-state-by-ibge-code/get-state-by-ibge-code.ts b/src/get-state-by-ibge-code/get-state-by-ibge-code.ts new file mode 100644 index 00000000..b6956b94 --- /dev/null +++ b/src/get-state-by-ibge-code/get-state-by-ibge-code.ts @@ -0,0 +1,42 @@ +import { DATA, type State } from "../_internals/constants/states"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Retrieves the Brazilian state whose 2-digit IBGE code ("cUF", the Código da Unidade da + * Federação) matches the given value. + * + * The IBGE code is the same 2-digit UF code found in the first field of every DF-e access key + * (chave de acesso) issued for NF-e, NFC-e, CT-e and MDF-e documents. + * + * @param {string|number} code - The 2-digit IBGE UF code. Accepts a string or a number, with + * any non-digit characters stripped before matching. + * @returns {State|null} The matching `State` object, or `null` when `code` is not a known + * IBGE UF code. + * + * @see Official: https://servicodados.ibge.gov.br/api/v1/localidades/estados (IBGE Localidades API, field `id`) + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * (Manual de Orientação do Contribuinte, "chave de acesso" / "Tabela do IBGE") + * + * @example + * ```typescript + * getStateByIbgeCode("35"); // { code: "SP", name: "São Paulo", regionCode: "SE", regionName: "Sudeste", ibgeCode: 35 } + * getStateByIbgeCode(35); // { code: "SP", name: "São Paulo", regionCode: "SE", regionName: "Sudeste", ibgeCode: 35 } + * getStateByIbgeCode("11"); // { code: "RO", name: "Rondônia", regionCode: "N", regionName: "Norte", ibgeCode: 11 } + * getStateByIbgeCode("00"); // null + * getStateByIbgeCode(""); // null + * ``` + */ +export const getStateByIbgeCode = (code: string | number): State | null => { + if (isNullish(code)) return null; + + const digits = sanitizeToDigits(code); + + if (digits === "") return null; + + const numericCode = Number(digits); + + const state = DATA.find((entry) => entry.ibgeCode === numericCode); + + return state ? { ...state } : null; +}; diff --git a/src/get-state-code-by-name/get-state-code-by-name.test.ts b/src/get-state-code-by-name/get-state-code-by-name.test.ts new file mode 100644 index 00000000..8c5b963f --- /dev/null +++ b/src/get-state-code-by-name/get-state-code-by-name.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getStateCodeByName } from "./get-state-code-by-name"; + +describe("getStateCodeByName", () => { + it("should return SP for the exact published name", () => { + expect(getStateCodeByName("São Paulo")).toBe("SP"); + }); + + it("should be accent-insensitive", () => { + expect(getStateCodeByName("Sao Paulo")).toBe("SP"); + }); + + it("should be case-insensitive", () => { + expect(getStateCodeByName("sao paulo")).toBe("SP"); + expect(getStateCodeByName("SAO PAULO")).toBe("SP"); + }); + + it("should trim leading and trailing whitespace", () => { + expect(getStateCodeByName(" São Paulo ")).toBe("SP"); + }); + + it("should combine accent removal, casing and trimming together", () => { + expect(getStateCodeByName(" sao PAULO ")).toBe("SP"); + }); + + it("should resolve a multi-word name with accents, the Ceará example", () => { + expect(getStateCodeByName("ceara")).toBe("CE"); + }); + + it("should resolve a name containing 'do'/'de' particles, the Rio Grande do Sul example", () => { + expect(getStateCodeByName("rio grande do sul")).toBe("RS"); + }); + + it("should distinguish Rio Grande do Norte from Rio Grande do Sul", () => { + expect(getStateCodeByName("Rio Grande do Norte")).toBe("RN"); + expect(getStateCodeByName("Rio Grande do Sul")).toBe("RS"); + }); + + it("should return null for a name that matches no state", () => { + expect(getStateCodeByName("Neverland")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getStateCodeByName("")).toBeNull(); + }); + + it("should return null for whitespace only", () => { + expect(getStateCodeByName(" ")).toBeNull(); + }); + + it("should return null for null", () => { + // @ts-expect-error + expect(getStateCodeByName(null)).toBeNull(); + }); + + it("should return null for undefined", () => { + // @ts-expect-error + expect(getStateCodeByName(undefined)).toBeNull(); + }); + + it("should return null for a number", () => { + // @ts-expect-error + expect(getStateCodeByName(35)).toBeNull(); + }); +}); diff --git a/src/get-state-code-by-name/get-state-code-by-name.ts b/src/get-state-code-by-name/get-state-code-by-name.ts new file mode 100644 index 00000000..7177dff7 --- /dev/null +++ b/src/get-state-code-by-name/get-state-code-by-name.ts @@ -0,0 +1,34 @@ +import { DATA, type StateCode } from "../_internals/constants/states"; +import { removeAccents } from "../remove-accents/remove-accents"; + +/** + * Retrieves the two-letter code (sigla) of a Brazilian state given its full name. + * + * The match is accent-insensitive, case-insensitive and ignores leading/trailing whitespace, + * so `" são paulo "`, `"Sao Paulo"` and `"SÃO PAULO"` all resolve to `"SP"`. + * + * @param {string} name - The full name of the state. + * @returns {StateCode|null} The two-letter state code, or `null` when `name` does not match + * any Brazilian state. + * + * @see Official: https://servicodados.ibge.gov.br/api/v1/localidades/estados (IBGE Localidades API) + * + * @example + * ```typescript + * getStateCodeByName("São Paulo"); // "SP" + * getStateCodeByName("sao paulo"); // "SP" + * getStateCodeByName(" Rio de Janeiro "); // "RJ" + * getStateCodeByName("Neverland"); // null + * ``` + */ +export const getStateCodeByName = (name: string): StateCode | null => { + if (typeof name !== "string") return null; + + const normalized = removeAccents(name).trim().toLowerCase(); + + if (normalized === "") return null; + + const state = DATA.find((entry) => removeAccents(entry.name).toLowerCase() === normalized); + + return state ? state.code : null; +}; diff --git a/src/get-state-name-by-code/get-state-name-by-code.test.ts b/src/get-state-name-by-code/get-state-name-by-code.test.ts new file mode 100644 index 00000000..e9f7ff6b --- /dev/null +++ b/src/get-state-name-by-code/get-state-name-by-code.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getStateNameByCode } from "./get-state-name-by-code"; + +describe("getStateNameByCode", () => { + it("should return the full name for an uppercase code", () => { + expect(getStateNameByCode("SP")).toBe("São Paulo"); + }); + + it("should be case-insensitive", () => { + expect(getStateNameByCode("sp")).toBe("São Paulo"); + expect(getStateNameByCode("Sp")).toBe("São Paulo"); + }); + + it("should trim leading and trailing whitespace", () => { + expect(getStateNameByCode(" RJ ")).toBe("Rio de Janeiro"); + }); + + it("should combine casing and trimming together", () => { + expect(getStateNameByCode(" rj ")).toBe("Rio de Janeiro"); + }); + + it("should resolve the Distrito Federal code", () => { + expect(getStateNameByCode("DF")).toBe("Distrito Federal"); + }); + + it("should return null for a code that matches no state", () => { + expect(getStateNameByCode("ZZ")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getStateNameByCode("")).toBeNull(); + }); + + it("should return null for whitespace only", () => { + expect(getStateNameByCode(" ")).toBeNull(); + }); + + it("should return null for null", () => { + // @ts-expect-error + expect(getStateNameByCode(null)).toBeNull(); + }); + + it("should return null for undefined", () => { + // @ts-expect-error + expect(getStateNameByCode(undefined)).toBeNull(); + }); + + it("should return null for a number", () => { + // @ts-expect-error + expect(getStateNameByCode(11)).toBeNull(); + }); +}); diff --git a/src/get-state-name-by-code/get-state-name-by-code.ts b/src/get-state-name-by-code/get-state-name-by-code.ts new file mode 100644 index 00000000..3ec31a05 --- /dev/null +++ b/src/get-state-name-by-code/get-state-name-by-code.ts @@ -0,0 +1,33 @@ +import { DATA, type StateName } from "../_internals/constants/states"; + +/** + * Retrieves the full name of a Brazilian state given its two-letter code (sigla). + * + * The match is case-insensitive and ignores leading/trailing whitespace, so `"sp"`, `"SP"` + * and `" Sp "` all resolve to `"São Paulo"`. + * + * @param {string} code - The two-letter state code. + * @returns {StateName|null} The full state name, or `null` when `code` does not match any + * Brazilian state. + * + * @see Official: https://servicodados.ibge.gov.br/api/v1/localidades/estados (IBGE Localidades API) + * + * @example + * ```typescript + * getStateNameByCode("SP"); // "São Paulo" + * getStateNameByCode("sp"); // "São Paulo" + * getStateNameByCode(" Rj "); // "Rio de Janeiro" + * getStateNameByCode("ZZ"); // null + * ``` + */ +export const getStateNameByCode = (code: string): StateName | null => { + if (typeof code !== "string") return null; + + const normalized = code.trim().toUpperCase(); + + if (normalized === "") return null; + + const state = DATA.find((entry) => entry.code === normalized); + + return state ? state.name : null; +}; diff --git a/src/get-timezone-by-state/constants.ts b/src/get-timezone-by-state/constants.ts new file mode 100644 index 00000000..f226011b --- /dev/null +++ b/src/get-timezone-by-state/constants.ts @@ -0,0 +1,42 @@ +/** + * IANA time zone database (tzdata) name for each Brazilian state, chosen as the zone of the + * state capital per the official `zone1970.tab` comments (some tzdata zones span more than + * one state, e.g. `America/Sao_Paulo` also covers DF, GO, MG, ES, RJ, PR, SC and RS, and + * `America/Fortaleza` also covers MA, PI, RN and PB besides CE). Pernambuco maps to + * `America/Recife`, not `America/Noronha`: Fernando de Noronha is an archipelago district of + * PE, not a state of its own, and its distinct UTC-02:00 offset is out of scope here. + * + * @see Official: https://raw.githubusercontent.com/eggert/tz/main/zone1970.tab (IANA tz + * database, `BR` rows) + * @see Based on: https://en.wikipedia.org/wiki/Time_in_Brazil Used to confirm the state + * coverage of each zone. + */ +export const STATE_TIMEZONES: Record = { + AC: "America/Rio_Branco", + AL: "America/Maceio", + AM: "America/Manaus", + AP: "America/Belem", + BA: "America/Bahia", + CE: "America/Fortaleza", + DF: "America/Sao_Paulo", + ES: "America/Sao_Paulo", + GO: "America/Sao_Paulo", + MA: "America/Fortaleza", + MG: "America/Sao_Paulo", + MS: "America/Campo_Grande", + MT: "America/Cuiaba", + PA: "America/Belem", + PB: "America/Fortaleza", + PE: "America/Recife", + PI: "America/Fortaleza", + PR: "America/Sao_Paulo", + RJ: "America/Sao_Paulo", + RN: "America/Fortaleza", + RO: "America/Porto_Velho", + RR: "America/Boa_Vista", + RS: "America/Sao_Paulo", + SC: "America/Sao_Paulo", + SE: "America/Maceio", + SP: "America/Sao_Paulo", + TO: "America/Araguaina", +}; diff --git a/src/get-timezone-by-state/get-timezone-by-state.test.ts b/src/get-timezone-by-state/get-timezone-by-state.test.ts new file mode 100644 index 00000000..62757a5a --- /dev/null +++ b/src/get-timezone-by-state/get-timezone-by-state.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getTimezoneByState } from "./get-timezone-by-state"; + +describe("getTimezoneByState", () => { + it("should return America/Sao_Paulo for SP", () => { + expect(getTimezoneByState("SP")).toBe("America/Sao_Paulo"); + }); + + it("should return America/Manaus for AM, the tzdata BR row for Amazonas (east)", () => { + expect(getTimezoneByState("AM")).toBe("America/Manaus"); + }); + + it("should return America/Rio_Branco for AC", () => { + expect(getTimezoneByState("AC")).toBe("America/Rio_Branco"); + }); + + it("should return America/Recife for PE, not America/Noronha (Fernando de Noronha is a district of PE, not a state)", () => { + expect(getTimezoneByState("PE")).toBe("America/Recife"); + }); + + it("should return America/Belem for AP, per the tzdata BR row 'Pará (east), Amapá'", () => { + expect(getTimezoneByState("AP")).toBe("America/Belem"); + }); + + it("should return America/Belem for PA", () => { + expect(getTimezoneByState("PA")).toBe("America/Belem"); + }); + + it("should return America/Fortaleza for CE, MA, PI, RN and PB, the tzdata BR row 'Brazil (northeast: MA, PI, CE, RN, PB)'", () => { + expect(getTimezoneByState("CE")).toBe("America/Fortaleza"); + expect(getTimezoneByState("MA")).toBe("America/Fortaleza"); + expect(getTimezoneByState("PI")).toBe("America/Fortaleza"); + expect(getTimezoneByState("RN")).toBe("America/Fortaleza"); + expect(getTimezoneByState("PB")).toBe("America/Fortaleza"); + }); + + it("should return America/Sao_Paulo for every southeast/south/center-west state sharing that zone", () => { + expect(getTimezoneByState("DF")).toBe("America/Sao_Paulo"); + expect(getTimezoneByState("GO")).toBe("America/Sao_Paulo"); + expect(getTimezoneByState("MG")).toBe("America/Sao_Paulo"); + expect(getTimezoneByState("ES")).toBe("America/Sao_Paulo"); + expect(getTimezoneByState("RJ")).toBe("America/Sao_Paulo"); + expect(getTimezoneByState("PR")).toBe("America/Sao_Paulo"); + expect(getTimezoneByState("SC")).toBe("America/Sao_Paulo"); + expect(getTimezoneByState("RS")).toBe("America/Sao_Paulo"); + }); + + it("should return America/Maceio for AL and SE", () => { + expect(getTimezoneByState("AL")).toBe("America/Maceio"); + expect(getTimezoneByState("SE")).toBe("America/Maceio"); + }); + + it("should return America/Bahia for BA", () => { + expect(getTimezoneByState("BA")).toBe("America/Bahia"); + }); + + it("should return America/Cuiaba for MT", () => { + expect(getTimezoneByState("MT")).toBe("America/Cuiaba"); + }); + + it("should return America/Campo_Grande for MS", () => { + expect(getTimezoneByState("MS")).toBe("America/Campo_Grande"); + }); + + it("should return America/Porto_Velho for RO", () => { + expect(getTimezoneByState("RO")).toBe("America/Porto_Velho"); + }); + + it("should return America/Boa_Vista for RR", () => { + expect(getTimezoneByState("RR")).toBe("America/Boa_Vista"); + }); + + it("should return America/Araguaina for TO", () => { + expect(getTimezoneByState("TO")).toBe("America/Araguaina"); + }); + + it("should be case-insensitive", () => { + expect(getTimezoneByState("sp")).toBe("America/Sao_Paulo"); + }); + + it("should trim leading and trailing whitespace", () => { + expect(getTimezoneByState(" SP ")).toBe("America/Sao_Paulo"); + }); + + it("should return null for a code that matches no state", () => { + expect(getTimezoneByState("ZZ")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getTimezoneByState("")).toBeNull(); + }); + + it("should return null for null", () => { + // @ts-expect-error + expect(getTimezoneByState(null)).toBeNull(); + }); + + it("should return null for undefined", () => { + // @ts-expect-error + expect(getTimezoneByState(undefined)).toBeNull(); + }); + + it("should return null for a number", () => { + // @ts-expect-error + expect(getTimezoneByState(35)).toBeNull(); + }); +}); diff --git a/src/get-timezone-by-state/get-timezone-by-state.ts b/src/get-timezone-by-state/get-timezone-by-state.ts new file mode 100644 index 00000000..077e5974 --- /dev/null +++ b/src/get-timezone-by-state/get-timezone-by-state.ts @@ -0,0 +1,37 @@ +import { STATE_TIMEZONES } from "./constants"; + +/** + * Retrieves the IANA time zone database name (tzdata zone) for a Brazilian state, chosen as + * the zone of the state capital. The match is case-insensitive and ignores leading/trailing + * whitespace. + * + * Some tzdata zones cover more than one state: `America/Sao_Paulo` also covers DF, GO, MG, ES, + * RJ, PR, SC and RS besides SP, and `America/Fortaleza` also covers MA, PI, RN and PB besides + * CE. Pernambuco resolves to `America/Recife`, not `America/Noronha`: Fernando de Noronha is an + * archipelago district of PE, not a state of its own. + * + * @param {string} stateCode - The two-letter state code (sigla). + * @returns {string|null} The IANA time zone name, or `null` when `stateCode` does not match + * any Brazilian state. + * + * @see Official: https://raw.githubusercontent.com/eggert/tz/main/zone1970.tab (IANA tz + * database, `BR` rows) + * @see Based on: https://en.wikipedia.org/wiki/Time_in_Brazil Used to confirm the state + * coverage of each zone. + * + * @example + * ```typescript + * getTimezoneByState("SP"); // "America/Sao_Paulo" + * getTimezoneByState("am"); // "America/Manaus" + * getTimezoneByState("AC"); // "America/Rio_Branco" + * getTimezoneByState("PE"); // "America/Recife" + * getTimezoneByState("ZZ"); // null + * ``` + */ +export const getTimezoneByState = (stateCode: string): string | null => { + if (typeof stateCode !== "string") return null; + + const normalized = stateCode.trim().toUpperCase(); + + return normalized in STATE_TIMEZONES ? STATE_TIMEZONES[normalized] : null; +}; From de09e55abf7582171c62357e8d93c71fd1e7867b Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 05/22] feat(area-code): add getAreaCodeInfo and getAreaCodesByState getAreaCodeInfo(ddd) resolves a DDD to its state and region; getAreaCodesByState(uf) does the reverse lookup. Both are backed by a new richer AREA_CODE_STATES table alongside the existing VALID_AREA_CODES. --- .../get-area-code-info.test.ts | 93 +++++++++++++++++++ src/get-area-code-info/get-area-code-info.ts | 57 ++++++++++++ .../get-area-codes-by-state.test.ts | 60 ++++++++++++ .../get-area-codes-by-state.ts | 39 ++++++++ 4 files changed, 249 insertions(+) create mode 100644 src/get-area-code-info/get-area-code-info.test.ts create mode 100644 src/get-area-code-info/get-area-code-info.ts create mode 100644 src/get-area-codes-by-state/get-area-codes-by-state.test.ts create mode 100644 src/get-area-codes-by-state/get-area-codes-by-state.ts diff --git a/src/get-area-code-info/get-area-code-info.test.ts b/src/get-area-code-info/get-area-code-info.test.ts new file mode 100644 index 00000000..6e9418b2 --- /dev/null +++ b/src/get-area-code-info/get-area-code-info.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getAreaCodeInfo } from "./get-area-code-info"; + +describe("getAreaCodeInfo", () => { + it("should resolve DDD 11 to São Paulo, Sudeste, from a string", () => { + expect(getAreaCodeInfo("11")).toEqual({ + areaCode: 11, + stateCode: "SP", + stateName: "São Paulo", + region: "Sudeste", + }); + }); + + it("should resolve DDD 11 to São Paulo, Sudeste, from a number", () => { + expect(getAreaCodeInfo(11)).toEqual({ + areaCode: 11, + stateCode: "SP", + stateName: "São Paulo", + region: "Sudeste", + }); + }); + + it("should resolve DDD 21 to Rio de Janeiro, per the Anatel Plano Geral de Numeração", () => { + expect(getAreaCodeInfo("21")?.stateCode).toBe("RJ"); + }); + + it("should resolve DDD 68 to Acre, Norte", () => { + expect(getAreaCodeInfo("68")).toEqual({ + areaCode: 68, + stateCode: "AC", + stateName: "Acre", + region: "Norte", + }); + }); + + it("should resolve DDD 61 to Distrito Federal, Centro-Oeste", () => { + expect(getAreaCodeInfo("61")).toEqual({ + areaCode: 61, + stateCode: "DF", + stateName: "Distrito Federal", + region: "Centro-Oeste", + }); + }); + + it("should resolve every one of the 67 valid DDDs to a state (Anatel Plano Geral de Numeração)", () => { + const ddds = [ + 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 24, 27, 28, 31, 32, 33, 34, 35, 37, 38, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 51, 53, 54, 55, 61, 62, 63, 64, 65, 66, 67, 68, 69, 71, 73, 74, + 75, 77, 79, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 92, 93, 94, 95, 96, 97, 98, 99, + ]; + + for (const ddd of ddds) { + expect(getAreaCodeInfo(ddd)?.areaCode).toBe(ddd); + } + + expect(ddds.length).toBe(67); + }); + + it("should distinguish DDD 41 (Paraná) from DDD 42 (Santa Catarina)", () => { + expect(getAreaCodeInfo("41")?.stateCode).toBe("PR"); + expect(getAreaCodeInfo("42")?.stateCode).toBe("SC"); + }); + + it("should ignore non-digit characters around the DDD", () => { + expect(getAreaCodeInfo(" 11 ")?.stateCode).toBe("SP"); + }); + + it("should ignore a parentheses mask around the DDD", () => { + expect(getAreaCodeInfo("(11)")?.stateCode).toBe("SP"); + }); + + it("should return null for a DDD that does not exist, such as 00", () => { + expect(getAreaCodeInfo("00")).toBeNull(); + }); + + it("should return null for a DDD that does not exist, such as 20", () => { + expect(getAreaCodeInfo("20")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getAreaCodeInfo("")).toBeNull(); + }); + + it("should return null for null", () => { + // @ts-expect-error + expect(getAreaCodeInfo(null)).toBeNull(); + }); + + it("should return null for undefined", () => { + // @ts-expect-error + expect(getAreaCodeInfo(undefined)).toBeNull(); + }); +}); diff --git a/src/get-area-code-info/get-area-code-info.ts b/src/get-area-code-info/get-area-code-info.ts new file mode 100644 index 00000000..37883a36 --- /dev/null +++ b/src/get-area-code-info/get-area-code-info.ts @@ -0,0 +1,57 @@ +import { AREA_CODE_STATES } from "../_internals/constants/area-codes"; +import { DATA, type State, type StateCode, type StateName } from "../_internals/constants/states"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +export type AreaCodeInfo = { + /** The DDD (area code) as a number, e.g. `11`. */ + areaCode: number; + /** The two-letter code of the state the DDD belongs to, e.g. `"SP"`. */ + stateCode: StateCode; + /** The full name of the state the DDD belongs to, e.g. `"São Paulo"`. */ + stateName: StateName; + /** The full name of the region the state belongs to, e.g. `"Sudeste"`. */ + region: State["regionName"]; +}; + +/** + * Retrieves the state (and its region) a Brazilian DDD (area code) belongs to. + * + * @param {string|number} areaCode - The DDD to look up. Accepts a string or a number, with any + * non-digit characters stripped before matching. + * @returns {AreaCodeInfo|null} The area code info, or `null` when `areaCode` is not one of the + * 67 DDDs in use under the Plano Geral de Numeração. + * + * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2010/167-resolucao-553 + * (Resolução Anatel 553/2010, Plano Geral de Numeração) + * @see Based on: https://brasilapi.com.br/docs#tag/DDD BrasilAPI DDD endpoint, used to verify + * the code-to-state mapping. + * + * @example + * ```typescript + * getAreaCodeInfo("11"); // { areaCode: 11, stateCode: "SP", stateName: "São Paulo", region: "Sudeste" } + * getAreaCodeInfo(21); // { areaCode: 21, stateCode: "RJ", stateName: "Rio de Janeiro", region: "Sudeste" } + * getAreaCodeInfo("68"); // { areaCode: 68, stateCode: "AC", stateName: "Acre", region: "Norte" } + * getAreaCodeInfo("00"); // null + * ``` + */ +export const getAreaCodeInfo = (areaCode: string | number): AreaCodeInfo | null => { + if (isNullish(areaCode)) return null; + + const digits = sanitizeToDigits(areaCode); + + if (digits === "") return null; + + const numericAreaCode = Number(digits); + + if (!(numericAreaCode in AREA_CODE_STATES)) return null; + + const stateCode = AREA_CODE_STATES[numericAreaCode]; + + const statesByCode: Record = {}; + for (const entry of DATA) statesByCode[entry.code] = entry; + + const state = statesByCode[stateCode]; + + return { areaCode: numericAreaCode, stateCode, stateName: state.name, region: state.regionName }; +}; diff --git a/src/get-area-codes-by-state/get-area-codes-by-state.test.ts b/src/get-area-codes-by-state/get-area-codes-by-state.test.ts new file mode 100644 index 00000000..c1a83a0c --- /dev/null +++ b/src/get-area-codes-by-state/get-area-codes-by-state.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { getAreaCodesByState } from "./get-area-codes-by-state"; + +describe("getAreaCodesByState", () => { + test("should return the ascending list of DDDs for a state with several DDDs", () => { + expect(getAreaCodesByState("SP")).toEqual([11, 12, 13, 14, 15, 16, 17, 18, 19]); + }); + + test("should return a single element list for a state with one DDD", () => { + expect(getAreaCodesByState("AC")).toEqual([68]); + }); + + test("should be case-insensitive", () => { + expect(getAreaCodesByState("sp")).toEqual([11, 12, 13, 14, 15, 16, 17, 18, 19]); + expect(getAreaCodesByState("Sp")).toEqual([11, 12, 13, 14, 15, 16, 17, 18, 19]); + }); + + test("should trim surrounding whitespace", () => { + expect(getAreaCodesByState(" SP ")).toEqual([11, 12, 13, 14, 15, 16, 17, 18, 19]); + }); + + test("should return DDDs out of numeric order in the source table sorted ascending", () => { + expect(getAreaCodesByState("PE")).toEqual([81, 87]); + }); + + test("should return a fresh array on every call", () => { + const first = getAreaCodesByState("AC"); + first.push(999); + expect(getAreaCodesByState("AC")).toEqual([68]); + }); + + describe("should return an empty array", () => { + test("when the state code does not match any Brazilian state", () => { + expect(getAreaCodesByState("XX")).toEqual([]); + }); + + test("when it is an empty string", () => { + expect(getAreaCodesByState("")).toEqual([]); + }); + + test("when it is a blank string", () => { + expect(getAreaCodesByState(" ")).toEqual([]); + }); + + test("when it is null", () => { + // @ts-expect-error + expect(getAreaCodesByState(null)).toEqual([]); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(getAreaCodesByState(undefined)).toEqual([]); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(getAreaCodesByState(11)).toEqual([]); + }); + }); +}); diff --git a/src/get-area-codes-by-state/get-area-codes-by-state.ts b/src/get-area-codes-by-state/get-area-codes-by-state.ts new file mode 100644 index 00000000..244944cd --- /dev/null +++ b/src/get-area-codes-by-state/get-area-codes-by-state.ts @@ -0,0 +1,39 @@ +import { AREA_CODE_STATES } from "../_internals/constants/area-codes"; + +/** + * Retrieves every DDD (area code) that belongs to a given Brazilian state, under the Plano + * Geral de Numeração. + * + * The match is case-insensitive, so `"sp"` and `"SP"` both resolve to the same list. The + * result is sorted in ascending order and is a fresh array on every call. + * + * @param {string} stateCode - The two-letter code (sigla) of the state. + * @returns {number[]} The DDDs of the state, sorted ascending, or an empty array when + * `stateCode` does not match any Brazilian state. + * + * @example + * ```typescript + * getAreaCodesByState("SP"); // [11, 12, 13, 14, 15, 16, 17, 18, 19] + * getAreaCodesByState("sp"); // [11, 12, 13, 14, 15, 16, 17, 18, 19] + * getAreaCodesByState("AC"); // [68] + * getAreaCodesByState("XX"); // [] + * ``` + * + * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2010/167-resolucao-553 + * (Resolução Anatel 553/2010, Plano Geral de Numeração) + */ +export const getAreaCodesByState = (stateCode: string): number[] => { + if (typeof stateCode !== "string") return []; + + const normalized = stateCode.trim().toUpperCase(); + + if (normalized === "") return []; + + const areaCodes: number[] = []; + + for (const [areaCode, code] of Object.entries(AREA_CODE_STATES)) { + if (code === normalized) areaCodes.push(Number(areaCode)); + } + + return areaCodes.sort((a, b) => a - b); +}; From 92ff25535a5db05acd5da2a900cd864c472463a9 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 06/22] feat(number-to-words): add convertNumberToWords MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spells an integer out in Portuguese, e.g. convertNumberToWords(1523) -> "mil, quinhentos e vinte e três". Backed by the new shared numberToWords and applyWordsCase internals, reused by convertCurrencyToWords/convertDateToWords. --- .../apply-words-case/apply-words-case.ts | 27 + src/_internals/constants/number-words.ts | 122 ++++ .../number-to-words/number-to-words.test.ts | 128 ++++ .../number-to-words/number-to-words.ts | 154 +++++ .../convert-number-to-words.test.ts | 627 ++++++++++++++++++ .../convert-number-to-words.ts | 60 ++ 6 files changed, 1118 insertions(+) create mode 100644 src/_internals/apply-words-case/apply-words-case.ts create mode 100644 src/_internals/constants/number-words.ts create mode 100644 src/_internals/number-to-words/number-to-words.test.ts create mode 100644 src/_internals/number-to-words/number-to-words.ts create mode 100644 src/convert-number-to-words/convert-number-to-words.test.ts create mode 100644 src/convert-number-to-words/convert-number-to-words.ts diff --git a/src/_internals/apply-words-case/apply-words-case.ts b/src/_internals/apply-words-case/apply-words-case.ts new file mode 100644 index 00000000..f17887b5 --- /dev/null +++ b/src/_internals/apply-words-case/apply-words-case.ts @@ -0,0 +1,27 @@ +import type { WordsCase } from "../number-to-words/number-to-words"; + +/** + * Applies a `WordsCase` to a "por extenso" string already written out in lowercase. + * + * `"sentence"` capitalizes only the first letter; `"upper"` uppercases the whole string with + * `toLocaleUpperCase("pt-BR")`, which keeps accents intact ("três" -> "TRÊS"). Any value other + * than `"sentence"` or `"upper"` (including `"lower"`, `undefined` or an invalid value) returns + * `text` unchanged, since it is already written in lowercase. + * + * @param {string} text - The lowercase "por extenso" string to transform. + * @param {WordsCase} [wordsCase] - The case to apply. Defaults to `"lower"` (no change). + * @returns {string} `text` with the requested case applied. + * + * @example + * ```typescript + * applyWordsCase("três reais"); // "três reais" + * applyWordsCase("três reais", "sentence"); // "Três reais" + * applyWordsCase("três reais", "upper"); // "TRÊS REAIS" + * ``` + */ +export const applyWordsCase = (text: string, wordsCase?: WordsCase): string => { + if (wordsCase === "upper") return text.toLocaleUpperCase("pt-BR"); + if (wordsCase === "sentence") return text.charAt(0).toLocaleUpperCase("pt-BR") + text.slice(1); + + return text; +}; diff --git a/src/_internals/constants/number-words.ts b/src/_internals/constants/number-words.ts new file mode 100644 index 00000000..000b4a91 --- /dev/null +++ b/src/_internals/constants/number-words.ts @@ -0,0 +1,122 @@ +/** + * Portuguese (pt-BR) number-to-words tables, shared by `numberToWords` and by every public + * "por extenso" formatter (`convertNumberToWords`, `convertCurrencyToWords`, `convertDateToWords`). + * + * @see https://github.com/brazilian-utils/python/blob/main/brutils/currency.py + * "catorze" (not "quatorze") is used for 14, matching num2words pt_BR and brutils. + */ + +export const ZERO_WORD = "zero"; + +export const UNITS: readonly string[] = [ + "zero", + "um", + "dois", + "três", + "quatro", + "cinco", + "seis", + "sete", + "oito", + "nove", + "dez", + "onze", + "doze", + "treze", + "catorze", + "quinze", + "dezesseis", + "dezessete", + "dezoito", + "dezenove", +]; + +export const UNITS_FEMININE_OVERRIDES: Record = { + 1: "uma", + 2: "duas", +}; + +export const TENS: readonly string[] = [ + "", + "", + "vinte", + "trinta", + "quarenta", + "cinquenta", + "sessenta", + "setenta", + "oitenta", + "noventa", +]; + +export const HUNDRED_EXACT = "cem"; + +export const HUNDREDS_MASCULINE: readonly string[] = [ + "", + "cento", + "duzentos", + "trezentos", + "quatrocentos", + "quinhentos", + "seiscentos", + "setecentos", + "oitocentos", + "novecentos", +]; + +export const HUNDREDS_FEMININE: readonly string[] = [ + "", + "cento", + "duzentas", + "trezentas", + "quatrocentas", + "quinhentas", + "seiscentas", + "setecentas", + "oitocentas", + "novecentas", +]; + +export type NumberScaleWord = { + /** Word used for a group whose value is exactly 1 (e.g. `"mil"`, `"milhão"`). */ + singular: string; + /** Word used for a group whose value is 0 or 2-999 (e.g. `"mil"`, `"milhões"`). */ + plural: string; +}; + +export const SCALE_WORDS: readonly NumberScaleWord[] = [ + { singular: "", plural: "" }, + { singular: "mil", plural: "mil" }, + { singular: "milhão", plural: "milhões" }, + { singular: "bilhão", plural: "bilhões" }, + { singular: "trilhão", plural: "trilhões" }, +]; + +export const MONTH_NAMES: readonly string[] = [ + "janeiro", + "fevereiro", + "março", + "abril", + "maio", + "junho", + "julho", + "agosto", + "setembro", + "outubro", + "novembro", + "dezembro", +]; + +/** + * Portuguese (pt-BR) weekday names, indexed like `Date#getDay`/`Date#getUTCDay` + * (0 = domingo, ..., 6 = sábado), used by `convertDateToWords`'s `weekday` option. + */ +export const WEEKDAY_NAMES: readonly string[] = [ + "domingo", + "segunda-feira", + "terça-feira", + "quarta-feira", + "quinta-feira", + "sexta-feira", + "sábado", +]; diff --git a/src/_internals/number-to-words/number-to-words.test.ts b/src/_internals/number-to-words/number-to-words.test.ts new file mode 100644 index 00000000..1337bdf4 --- /dev/null +++ b/src/_internals/number-to-words/number-to-words.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, test } from "../test/runtime"; +import { NUMBER_TO_WORDS_MAX_VALUE, numberToWords } from "./number-to-words"; + +describe("numberToWords", () => { + test("should return 'zero' for 0", () => { + expect(numberToWords(0)).toBe("zero"); + }); + + test("should return 'um' for 1", () => { + expect(numberToWords(1)).toBe("um"); + }); + + test("should convert every teen number (10-19)", () => { + expect(numberToWords(10)).toBe("dez"); + expect(numberToWords(11)).toBe("onze"); + expect(numberToWords(12)).toBe("doze"); + expect(numberToWords(13)).toBe("treze"); + expect(numberToWords(14)).toBe("catorze"); + expect(numberToWords(15)).toBe("quinze"); + expect(numberToWords(16)).toBe("dezesseis"); + expect(numberToWords(17)).toBe("dezessete"); + expect(numberToWords(18)).toBe("dezoito"); + expect(numberToWords(19)).toBe("dezenove"); + }); + + test("should join tens and units with 'e' (21 -> num2words pt_BR 'vinte e um')", () => { + expect(numberToWords(21)).toBe("vinte e um"); + }); + + test("should return 'cem' for the exact hundred (100)", () => { + expect(numberToWords(100)).toBe("cem"); + }); + + test("should return 'cento e um' for 101 (num2words pt_BR)", () => { + expect(numberToWords(101)).toBe("cento e um"); + }); + + test("should return 'duzentos' for the exact round hundred (200)", () => { + expect(numberToWords(200)).toBe("duzentos"); + }); + + test("should return 'mil' alone for 1000, never 'um mil'", () => { + expect(numberToWords(1000)).toBe("mil"); + }); + + test("should join 'mil' and a unit with 'e' (1001 -> 'mil e um')", () => { + expect(numberToWords(1001)).toBe("mil e um"); + }); + + test("should join 'mil' and a round hundred with 'e' (1100 -> 'mil e cem')", () => { + expect(numberToWords(1100)).toBe("mil e cem"); + }); + + test("should separate 'mil' from a non round last group with a comma (1235 -> num2words pt_BR 'mil, duzentos e trinta e cinco')", () => { + expect(numberToWords(1235)).toBe("mil, duzentos e trinta e cinco"); + }); + + test("should return 'dois mil' for 2000 (masculine default)", () => { + expect(numberToWords(2000)).toBe("dois mil"); + }); + + test("should return 'um milhão' for 1000000, never 'um milhão e zero'", () => { + expect(numberToWords(1_000_000)).toBe("um milhão"); + }); + + test("should pluralize to 'milhões' for 2000000", () => { + expect(numberToWords(2_000_000)).toBe("dois milhões"); + }); + + test("should join 'um milhão' and a trailing unit with 'e' (1000001)", () => { + expect(numberToWords(1_000_001)).toBe("um milhão e um"); + }); + + test("should convert the maximum supported value (999 trillion, num2words pt_BR)", () => { + expect(numberToWords(NUMBER_TO_WORDS_MAX_VALUE)).toBe( + "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, " + + "novecentos e noventa e nove milhões, novecentos e noventa e nove mil, " + + "novecentos e noventa e nove", + ); + }); + + test("should convert a value spanning billions, millions and thousands (999999999999, num2words pt_BR)", () => { + expect(numberToWords(999_999_999_999)).toBe( + "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, " + + "novecentos e noventa e nove mil, novecentos e noventa e nove", + ); + }); + + test("should skip a zero intermediate group (1000230 -> no 'zero mil')", () => { + expect(numberToWords(1_000_230)).toBe("um milhão, duzentos e trinta"); + }); + + test("should separate an intermediate group below 100 with a comma, reserving 'e' for the last group (1045678; num2words pt_BR differs here only because its post-processing rewrites ' e ' before a hundreds word)", () => { + expect(numberToWords(1_045_678)).toBe( + "um milhão, quarenta e cinco mil, seiscentos e setenta e oito", + ); + }); + + describe("gender agreement", () => { + test("should return 'uma' and 'duas' for 1 and 2 when feminine", () => { + expect(numberToWords(1, { gender: "feminine" })).toBe("uma"); + expect(numberToWords(2, { gender: "feminine" })).toBe("duas"); + }); + + test("should return the '-entas' hundreds form when feminine", () => { + expect(numberToWords(200, { gender: "feminine" })).toBe("duzentas"); + expect(numberToWords(202, { gender: "feminine" })).toBe("duzentas e duas"); + }); + + test("should keep 'cem'/'cento' invariant regardless of gender", () => { + expect(numberToWords(100, { gender: "feminine" })).toBe("cem"); + expect(numberToWords(101, { gender: "feminine" })).toBe("cento e uma"); + }); + + test("should agree the thousands multiplier with the feminine gender (2000 -> 'duas mil')", () => { + expect(numberToWords(2000, { gender: "feminine" })).toBe("duas mil"); + }); + + test("should agree the hundreds of the thousands group with the feminine gender (200000 -> 'duzentas mil')", () => { + expect(numberToWords(200_000, { gender: "feminine" })).toBe("duzentas mil"); + expect(numberToWords(100_000, { gender: "feminine" })).toBe("cem mil"); + }); + + test("should keep the million multiplier masculine regardless of gender (it agrees with 'milhão')", () => { + expect(numberToWords(2_000_000, { gender: "feminine" })).toBe("dois milhões"); + }); + }); +}); diff --git a/src/_internals/number-to-words/number-to-words.ts b/src/_internals/number-to-words/number-to-words.ts new file mode 100644 index 00000000..9ecb74a6 --- /dev/null +++ b/src/_internals/number-to-words/number-to-words.ts @@ -0,0 +1,154 @@ +import { + HUNDRED_EXACT, + HUNDREDS_FEMININE, + HUNDREDS_MASCULINE, + SCALE_WORDS, + TENS, + UNITS, + UNITS_FEMININE_OVERRIDES, + ZERO_WORD, +} from "../constants/number-words"; + +export type NumberToWordsGender = "masculine" | "feminine"; + +/** + * Letter case applied to the final "por extenso" string of `convertNumberToWords`, + * `convertCurrencyToWords` and `convertDateToWords`. `"lower"` leaves the string as produced + * (every word already lowercase); `"sentence"` capitalizes only its first letter; `"upper"` + * uppercases the whole string with the "pt-BR" locale, which keeps accents intact + * ("três" -> "TRÊS", "março" -> "MARÇO"). Defaults to `"lower"`; any other value is ignored and + * `"lower"` is used instead. + */ +export type WordsCase = "lower" | "sentence" | "upper"; + +export type NumberToWordsOptions = { + /** Grammatical gender used to agree "um/dois" and the 100-999 group ("duzentos/duzentas", etc.) with the noun the number qualifies. Only the thousands group and the final 0-999 group are affected: the multiplier of "milhão/bilhão/trilhão" always agrees with those (masculine) nouns. Defaults to `"masculine"`. */ + gender?: NumberToWordsGender; +}; + +/** + * The largest absolute value `numberToWords` converts: 999 trillion, 999 billion, 999 million, + * 999 thousand and 999 (999999999999999), the highest value expressible with the "trilhão" + * scale word before a new scale word would be required. + */ +export const NUMBER_TO_WORDS_MAX_VALUE = 999_999_999_999_999; + +const unitWord = (digit: number, gender?: NumberToWordsGender): string => + gender === "feminine" && digit in UNITS_FEMININE_OVERRIDES + ? UNITS_FEMININE_OVERRIDES[digit] + : UNITS[digit]; + +const groupToWords = (value: number, gender?: NumberToWordsGender): string => { + const hundredsDigit = Math.floor(value / 100); + const remainder = value % 100; + const segments: string[] = []; + + if (hundredsDigit > 0) { + segments.push( + value === 100 + ? HUNDRED_EXACT + : (gender === "feminine" ? HUNDREDS_FEMININE : HUNDREDS_MASCULINE)[hundredsDigit], + ); + } + + if (remainder > 0) { + if (remainder < 20) { + segments.push(unitWord(remainder, gender)); + } else { + const tensDigit = Math.floor(remainder / 10); + const unitsDigit = remainder % 10; + segments.push( + unitsDigit > 0 ? `${TENS[tensDigit]} e ${unitWord(unitsDigit, gender)}` : TENS[tensDigit], + ); + } + } + + return segments.join(" e "); +}; + +const isRoundHundred = (value: number): boolean => value % 100 === 0; + +/** + * Converts a non-negative integer into its Brazilian Portuguese cardinal number words + * ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. + * + * This is the shared engine behind every "por extenso" formatter of this library + * (`convertNumberToWords`, `convertCurrencyToWords`, `convertDateToWords`): it only converts, it + * never validates or sanitizes its input, so callers must pass a finite, non-negative integer + * within `[0, NUMBER_TO_WORDS_MAX_VALUE]`. Grouping uses commas between groups and "e" is used + * instead of a comma right before the last group when that group is below 100 or is a round + * hundred (100, 200, ..., 900), matching how the value would be written by hand + * (e.g. `1200` -> `"mil e duzentos"`, `1235` -> `"mil, duzentos e trinta e cinco"`). The "e" + * connector is therefore reserved for the last group: an intermediate group below 100 still takes + * a comma (`1045678` -> `"um milhão, quarenta e cinco mil, seiscentos e setenta e oito"`). This is + * the one place where the output deviates from `num2words`' pt_BR locale, which writes + * `"um milhão e quarenta e cinco mil, ..."` there because its post-processing only rewrites " e " + * into "," when the next word is a hundreds word, making an intermediate group's punctuation + * depend on the group that follows it. Every published `brutils` example is reproduced exactly. + * + * @param {number} value - A non-negative integer in `[0, NUMBER_TO_WORDS_MAX_VALUE]`. + * @param {NumberToWordsOptions} [options] - Optional conversion options. + * @param {NumberToWordsGender} [options.gender] - Grammatical gender for "um/dois" and the hundreds group. Defaults to `"masculine"`. + * @returns {string} The cardinal number written out in Portuguese. + * + * @example + * ```typescript + * numberToWords(0); // "zero" + * numberToWords(21); // "vinte e um" + * numberToWords(100); // "cem" + * numberToWords(1100); // "mil e cem" + * numberToWords(1235); // "mil, duzentos e trinta e cinco" + * numberToWords(2000000); // "dois milhões" + * numberToWords(2, { gender: "feminine" }); // "duas" + * numberToWords(2000, { gender: "feminine" }); // "duas mil" + * ``` + * + * @see https://github.com/brazilian-utils/python/blob/main/brutils/currency.py + */ +export const numberToWords = (value: number, options?: NumberToWordsOptions): string => { + if (value === 0) return ZERO_WORD; + + const gender = options?.gender; + const groups: number[] = []; + let remaining = value; + + while (remaining > 0) { + groups.unshift(remaining % 1000); + remaining = Math.floor(remaining / 1000); + } + + const highestScale = groups.length - 1; + let lastNonZeroIndex = -1; + for (let i = 0; i < groups.length; i++) { + if (groups[i] > 0) lastNonZeroIndex = i; + } + + let result = ""; + + groups.forEach((groupValue, index) => { + if (groupValue === 0) return; + + const scale = highestScale - index; + const scaleWord = SCALE_WORDS[scale]; + const groupGender = scale >= 2 ? undefined : gender; + + const groupText = + scale === 1 && groupValue === 1 + ? scaleWord.singular + : scale === 0 + ? groupToWords(groupValue, groupGender) + : `${groupToWords(groupValue, groupGender)} ${groupValue === 1 ? scaleWord.singular : scaleWord.plural}`; + + if (result === "") { + result = groupText; + return; + } + + const connector = + index === lastNonZeroIndex && (groupValue < 100 || isRoundHundred(groupValue)) ? " e " : ", "; + + result += connector + groupText; + }); + + return result; +}; diff --git a/src/convert-number-to-words/convert-number-to-words.test.ts b/src/convert-number-to-words/convert-number-to-words.test.ts new file mode 100644 index 00000000..88e19fc1 --- /dev/null +++ b/src/convert-number-to-words/convert-number-to-words.test.ts @@ -0,0 +1,627 @@ +import { NUMBER_TO_WORDS_MAX_VALUE } from "../_internals/number-to-words/number-to-words"; +import { describe, expect, test } from "../_internals/test/runtime"; +import { convertNumberToWords } from "./convert-number-to-words"; + +describe("convertNumberToWords", () => { + test("should return 'zero' for 0", () => { + expect(convertNumberToWords(0)).toBe("zero"); + }); + + test("should return 'um' for 1", () => { + expect(convertNumberToWords(1)).toBe("um"); + }); + + test("should return 'cem' for 100 and 'cento e um' for 101", () => { + expect(convertNumberToWords(100)).toBe("cem"); + expect(convertNumberToWords(101)).toBe("cento e um"); + }); + + test("should return 'mil' alone for 1000, never 'um mil'", () => { + expect(convertNumberToWords(1000)).toBe("mil"); + }); + + test("should return 'um milhão' for 1000000, never 'um milhão e zero'", () => { + expect(convertNumberToWords(1_000_000)).toBe("um milhão"); + }); + + test("should convert the maximum supported value (999999999999999, 999 trillion)", () => { + expect(convertNumberToWords(NUMBER_TO_WORDS_MAX_VALUE)).toBe( + "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, " + + "novecentos e noventa e nove milhões, novecentos e noventa e nove mil, " + + "novecentos e noventa e nove", + ); + }); + + test("should return '' above the maximum supported value", () => { + expect(convertNumberToWords(NUMBER_TO_WORDS_MAX_VALUE + 1)).toBe(""); + }); + + test("should return '' below the negative of the maximum supported value", () => { + expect(convertNumberToWords(-NUMBER_TO_WORDS_MAX_VALUE - 1)).toBe(""); + }); + + test("should not prefix 'menos' for negative zero", () => { + expect(convertNumberToWords(-0)).toBe("zero"); + }); + + describe("invalid input", () => { + test("should return '' for NaN", () => { + expect(convertNumberToWords(Number.NaN)).toBe(""); + }); + + test("should return '' for Infinity and -Infinity", () => { + expect(convertNumberToWords(Number.POSITIVE_INFINITY)).toBe(""); + expect(convertNumberToWords(Number.NEGATIVE_INFINITY)).toBe(""); + }); + + test("should return '' for a non-number value", () => { + // @ts-expect-error + expect(convertNumberToWords("123")).toBe(""); + // @ts-expect-error + expect(convertNumberToWords(null)).toBe(""); + // @ts-expect-error + expect(convertNumberToWords(undefined)).toBe(""); + }); + }); + + describe("non-integer values", () => { + test("should truncate toward zero before converting", () => { + expect(convertNumberToWords(12.9)).toBe("doze"); + expect(convertNumberToWords(-12.9)).toBe("menos doze"); + }); + }); + + describe("case option", () => { + test("should keep the result lowercase by default", () => { + expect(convertNumberToWords(123)).toBe("cento e vinte e três"); + }); + + test("should keep the result lowercase for 'lower'", () => { + expect(convertNumberToWords(123, { case: "lower" })).toBe("cento e vinte e três"); + }); + + test("should capitalize only the first letter for 'sentence'", () => { + expect(convertNumberToWords(123, { case: "sentence" })).toBe("Cento e vinte e três"); + expect(convertNumberToWords(3, { case: "sentence" })).toBe("Três"); + }); + + test("should uppercase everything for 'upper', keeping accents", () => { + expect(convertNumberToWords(3, { case: "upper" })).toBe("TRÊS"); + expect(convertNumberToWords(50, { case: "upper" })).toBe("CINQUENTA"); + expect(convertNumberToWords(-3, { case: "upper" })).toBe("MENOS TRÊS"); + }); + + test("should ignore an invalid case value and fall back to 'lower'", () => { + // @ts-expect-error + expect(convertNumberToWords(123, { case: "invalid" })).toBe("cento e vinte e três"); + }); + }); + + describe("literal case tables", () => { + test("should match a hand-written word for every integer from 0 to 200 (masculine)", () => { + const cases: Array<[number, string]> = [ + [0, "zero"], + [1, "um"], + [2, "dois"], + [3, "três"], + [4, "quatro"], + [5, "cinco"], + [6, "seis"], + [7, "sete"], + [8, "oito"], + [9, "nove"], + [10, "dez"], + [11, "onze"], + [12, "doze"], + [13, "treze"], + [14, "catorze"], + [15, "quinze"], + [16, "dezesseis"], + [17, "dezessete"], + [18, "dezoito"], + [19, "dezenove"], + [20, "vinte"], + [21, "vinte e um"], + [22, "vinte e dois"], + [23, "vinte e três"], + [24, "vinte e quatro"], + [25, "vinte e cinco"], + [26, "vinte e seis"], + [27, "vinte e sete"], + [28, "vinte e oito"], + [29, "vinte e nove"], + [30, "trinta"], + [31, "trinta e um"], + [32, "trinta e dois"], + [33, "trinta e três"], + [34, "trinta e quatro"], + [35, "trinta e cinco"], + [36, "trinta e seis"], + [37, "trinta e sete"], + [38, "trinta e oito"], + [39, "trinta e nove"], + [40, "quarenta"], + [41, "quarenta e um"], + [42, "quarenta e dois"], + [43, "quarenta e três"], + [44, "quarenta e quatro"], + [45, "quarenta e cinco"], + [46, "quarenta e seis"], + [47, "quarenta e sete"], + [48, "quarenta e oito"], + [49, "quarenta e nove"], + [50, "cinquenta"], + [51, "cinquenta e um"], + [52, "cinquenta e dois"], + [53, "cinquenta e três"], + [54, "cinquenta e quatro"], + [55, "cinquenta e cinco"], + [56, "cinquenta e seis"], + [57, "cinquenta e sete"], + [58, "cinquenta e oito"], + [59, "cinquenta e nove"], + [60, "sessenta"], + [61, "sessenta e um"], + [62, "sessenta e dois"], + [63, "sessenta e três"], + [64, "sessenta e quatro"], + [65, "sessenta e cinco"], + [66, "sessenta e seis"], + [67, "sessenta e sete"], + [68, "sessenta e oito"], + [69, "sessenta e nove"], + [70, "setenta"], + [71, "setenta e um"], + [72, "setenta e dois"], + [73, "setenta e três"], + [74, "setenta e quatro"], + [75, "setenta e cinco"], + [76, "setenta e seis"], + [77, "setenta e sete"], + [78, "setenta e oito"], + [79, "setenta e nove"], + [80, "oitenta"], + [81, "oitenta e um"], + [82, "oitenta e dois"], + [83, "oitenta e três"], + [84, "oitenta e quatro"], + [85, "oitenta e cinco"], + [86, "oitenta e seis"], + [87, "oitenta e sete"], + [88, "oitenta e oito"], + [89, "oitenta e nove"], + [90, "noventa"], + [91, "noventa e um"], + [92, "noventa e dois"], + [93, "noventa e três"], + [94, "noventa e quatro"], + [95, "noventa e cinco"], + [96, "noventa e seis"], + [97, "noventa e sete"], + [98, "noventa e oito"], + [99, "noventa e nove"], + [100, "cem"], + [101, "cento e um"], + [102, "cento e dois"], + [103, "cento e três"], + [104, "cento e quatro"], + [105, "cento e cinco"], + [106, "cento e seis"], + [107, "cento e sete"], + [108, "cento e oito"], + [109, "cento e nove"], + [110, "cento e dez"], + [111, "cento e onze"], + [112, "cento e doze"], + [113, "cento e treze"], + [114, "cento e catorze"], + [115, "cento e quinze"], + [116, "cento e dezesseis"], + [117, "cento e dezessete"], + [118, "cento e dezoito"], + [119, "cento e dezenove"], + [120, "cento e vinte"], + [121, "cento e vinte e um"], + [122, "cento e vinte e dois"], + [123, "cento e vinte e três"], + [124, "cento e vinte e quatro"], + [125, "cento e vinte e cinco"], + [126, "cento e vinte e seis"], + [127, "cento e vinte e sete"], + [128, "cento e vinte e oito"], + [129, "cento e vinte e nove"], + [130, "cento e trinta"], + [131, "cento e trinta e um"], + [132, "cento e trinta e dois"], + [133, "cento e trinta e três"], + [134, "cento e trinta e quatro"], + [135, "cento e trinta e cinco"], + [136, "cento e trinta e seis"], + [137, "cento e trinta e sete"], + [138, "cento e trinta e oito"], + [139, "cento e trinta e nove"], + [140, "cento e quarenta"], + [141, "cento e quarenta e um"], + [142, "cento e quarenta e dois"], + [143, "cento e quarenta e três"], + [144, "cento e quarenta e quatro"], + [145, "cento e quarenta e cinco"], + [146, "cento e quarenta e seis"], + [147, "cento e quarenta e sete"], + [148, "cento e quarenta e oito"], + [149, "cento e quarenta e nove"], + [150, "cento e cinquenta"], + [151, "cento e cinquenta e um"], + [152, "cento e cinquenta e dois"], + [153, "cento e cinquenta e três"], + [154, "cento e cinquenta e quatro"], + [155, "cento e cinquenta e cinco"], + [156, "cento e cinquenta e seis"], + [157, "cento e cinquenta e sete"], + [158, "cento e cinquenta e oito"], + [159, "cento e cinquenta e nove"], + [160, "cento e sessenta"], + [161, "cento e sessenta e um"], + [162, "cento e sessenta e dois"], + [163, "cento e sessenta e três"], + [164, "cento e sessenta e quatro"], + [165, "cento e sessenta e cinco"], + [166, "cento e sessenta e seis"], + [167, "cento e sessenta e sete"], + [168, "cento e sessenta e oito"], + [169, "cento e sessenta e nove"], + [170, "cento e setenta"], + [171, "cento e setenta e um"], + [172, "cento e setenta e dois"], + [173, "cento e setenta e três"], + [174, "cento e setenta e quatro"], + [175, "cento e setenta e cinco"], + [176, "cento e setenta e seis"], + [177, "cento e setenta e sete"], + [178, "cento e setenta e oito"], + [179, "cento e setenta e nove"], + [180, "cento e oitenta"], + [181, "cento e oitenta e um"], + [182, "cento e oitenta e dois"], + [183, "cento e oitenta e três"], + [184, "cento e oitenta e quatro"], + [185, "cento e oitenta e cinco"], + [186, "cento e oitenta e seis"], + [187, "cento e oitenta e sete"], + [188, "cento e oitenta e oito"], + [189, "cento e oitenta e nove"], + [190, "cento e noventa"], + [191, "cento e noventa e um"], + [192, "cento e noventa e dois"], + [193, "cento e noventa e três"], + [194, "cento e noventa e quatro"], + [195, "cento e noventa e cinco"], + [196, "cento e noventa e seis"], + [197, "cento e noventa e sete"], + [198, "cento e noventa e oito"], + [199, "cento e noventa e nove"], + [200, "duzentos"], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertNumberToWords(value); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should match a hand-written word for every round hundred and the hundred that follows it", () => { + const cases: Array<[number, string]> = [ + [100, "cem"], + [101, "cento e um"], + [200, "duzentos"], + [201, "duzentos e um"], + [300, "trezentos"], + [301, "trezentos e um"], + [400, "quatrocentos"], + [401, "quatrocentos e um"], + [500, "quinhentos"], + [501, "quinhentos e um"], + [600, "seiscentos"], + [601, "seiscentos e um"], + [700, "setecentos"], + [701, "setecentos e um"], + [800, "oitocentos"], + [801, "oitocentos e um"], + [900, "novecentos"], + [901, "novecentos e um"], + [999, "novecentos e noventa e nove"], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertNumberToWords(value); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should match a hand-written word at every ten/hundred/thousand/scale boundary", () => { + const cases: Array<[number, string]> = [ + [999, "novecentos e noventa e nove"], + [1000, "mil"], + [1001, "mil e um"], + [1021, "mil e vinte e um"], + [1100, "mil e cem"], + [1101, "mil, cento e um"], + [1200, "mil e duzentos"], + [1235, "mil, duzentos e trinta e cinco"], + [1999, "mil, novecentos e noventa e nove"], + [2000, "dois mil"], + [2001, "dois mil e um"], + [5000, "cinco mil"], + [9999, "nove mil, novecentos e noventa e nove"], + [10000, "dez mil"], + [21000, "vinte e um mil"], + [100000, "cem mil"], + [101000, "cento e um mil"], + [200000, "duzentos mil"], + [300000, "trezentos mil"], + [999999, "novecentos e noventa e nove mil, novecentos e noventa e nove"], + [1000000, "um milhão"], + [1000001, "um milhão e um"], + [1000100, "um milhão e cem"], + [1000230, "um milhão, duzentos e trinta"], + [1045678, "um milhão, quarenta e cinco mil, seiscentos e setenta e oito"], + [1100000, "um milhão e cem mil"], + [1200000, "um milhão e duzentos mil"], + [1230000, "um milhão, duzentos e trinta mil"], + [1230045, "um milhão, duzentos e trinta mil e quarenta e cinco"], + [1230456, "um milhão, duzentos e trinta mil, quatrocentos e cinquenta e seis"], + [2000000, "dois milhões"], + [1000000000, "um bilhão"], + [1000000001, "um bilhão e um"], + [2000000000, "dois bilhões"], + [ + 1234567890, + "um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa", + ], + [ + 999999999999, + "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove", + ], + [1000000000000, "um trilhão"], + [2000000000000, "dois trilhões"], + [ + 999999999999999, + "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove", + ], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertNumberToWords(value); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should prefix 'menos' to a hand-written word for every integer from -1 to -100", () => { + const cases: Array<[number, string]> = [ + [-1, "menos um"], + [-2, "menos dois"], + [-3, "menos três"], + [-4, "menos quatro"], + [-5, "menos cinco"], + [-6, "menos seis"], + [-7, "menos sete"], + [-8, "menos oito"], + [-9, "menos nove"], + [-10, "menos dez"], + [-11, "menos onze"], + [-12, "menos doze"], + [-13, "menos treze"], + [-14, "menos catorze"], + [-15, "menos quinze"], + [-16, "menos dezesseis"], + [-17, "menos dezessete"], + [-18, "menos dezoito"], + [-19, "menos dezenove"], + [-20, "menos vinte"], + [-21, "menos vinte e um"], + [-22, "menos vinte e dois"], + [-23, "menos vinte e três"], + [-24, "menos vinte e quatro"], + [-25, "menos vinte e cinco"], + [-26, "menos vinte e seis"], + [-27, "menos vinte e sete"], + [-28, "menos vinte e oito"], + [-29, "menos vinte e nove"], + [-30, "menos trinta"], + [-31, "menos trinta e um"], + [-32, "menos trinta e dois"], + [-33, "menos trinta e três"], + [-34, "menos trinta e quatro"], + [-35, "menos trinta e cinco"], + [-36, "menos trinta e seis"], + [-37, "menos trinta e sete"], + [-38, "menos trinta e oito"], + [-39, "menos trinta e nove"], + [-40, "menos quarenta"], + [-41, "menos quarenta e um"], + [-42, "menos quarenta e dois"], + [-43, "menos quarenta e três"], + [-44, "menos quarenta e quatro"], + [-45, "menos quarenta e cinco"], + [-46, "menos quarenta e seis"], + [-47, "menos quarenta e sete"], + [-48, "menos quarenta e oito"], + [-49, "menos quarenta e nove"], + [-50, "menos cinquenta"], + [-51, "menos cinquenta e um"], + [-52, "menos cinquenta e dois"], + [-53, "menos cinquenta e três"], + [-54, "menos cinquenta e quatro"], + [-55, "menos cinquenta e cinco"], + [-56, "menos cinquenta e seis"], + [-57, "menos cinquenta e sete"], + [-58, "menos cinquenta e oito"], + [-59, "menos cinquenta e nove"], + [-60, "menos sessenta"], + [-61, "menos sessenta e um"], + [-62, "menos sessenta e dois"], + [-63, "menos sessenta e três"], + [-64, "menos sessenta e quatro"], + [-65, "menos sessenta e cinco"], + [-66, "menos sessenta e seis"], + [-67, "menos sessenta e sete"], + [-68, "menos sessenta e oito"], + [-69, "menos sessenta e nove"], + [-70, "menos setenta"], + [-71, "menos setenta e um"], + [-72, "menos setenta e dois"], + [-73, "menos setenta e três"], + [-74, "menos setenta e quatro"], + [-75, "menos setenta e cinco"], + [-76, "menos setenta e seis"], + [-77, "menos setenta e sete"], + [-78, "menos setenta e oito"], + [-79, "menos setenta e nove"], + [-80, "menos oitenta"], + [-81, "menos oitenta e um"], + [-82, "menos oitenta e dois"], + [-83, "menos oitenta e três"], + [-84, "menos oitenta e quatro"], + [-85, "menos oitenta e cinco"], + [-86, "menos oitenta e seis"], + [-87, "menos oitenta e sete"], + [-88, "menos oitenta e oito"], + [-89, "menos oitenta e nove"], + [-90, "menos noventa"], + [-91, "menos noventa e um"], + [-92, "menos noventa e dois"], + [-93, "menos noventa e três"], + [-94, "menos noventa e quatro"], + [-95, "menos noventa e cinco"], + [-96, "menos noventa e seis"], + [-97, "menos noventa e sete"], + [-98, "menos noventa e oito"], + [-99, "menos noventa e nove"], + [-100, "menos cem"], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertNumberToWords(value); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should prefix 'menos' to a hand-written word at negative scale boundaries", () => { + const cases: Array<[number, string]> = [ + [-200, "menos duzentos"], + [-999, "menos novecentos e noventa e nove"], + [-1000, "menos mil"], + [-1001, "menos mil e um"], + [-2000, "menos dois mil"], + [-1000000, "menos um milhão"], + [ + -999999999999999, + "menos novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove", + ], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertNumberToWords(value); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should match a hand-written feminine word for every integer from 0 to 30", () => { + const cases: Array<[number, string]> = [ + [0, "zero"], + [1, "uma"], + [2, "duas"], + [3, "três"], + [4, "quatro"], + [5, "cinco"], + [6, "seis"], + [7, "sete"], + [8, "oito"], + [9, "nove"], + [10, "dez"], + [11, "onze"], + [12, "doze"], + [13, "treze"], + [14, "catorze"], + [15, "quinze"], + [16, "dezesseis"], + [17, "dezessete"], + [18, "dezoito"], + [19, "dezenove"], + [20, "vinte"], + [21, "vinte e uma"], + [22, "vinte e duas"], + [23, "vinte e três"], + [24, "vinte e quatro"], + [25, "vinte e cinco"], + [26, "vinte e seis"], + [27, "vinte e sete"], + [28, "vinte e oito"], + [29, "vinte e nove"], + [30, "trinta"], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertNumberToWords(value, { gender: "feminine" }); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should match a hand-written feminine word at hundred/thousand/million boundaries", () => { + const cases: Array<[number, string]> = [ + [100, "cem"], + [101, "cento e uma"], + [200, "duzentas"], + [201, "duzentas e uma"], + [300, "trezentas"], + [400, "quatrocentas"], + [500, "quinhentas"], + [600, "seiscentas"], + [700, "setecentas"], + [800, "oitocentas"], + [900, "novecentas"], + [1000, "mil"], + [1001, "mil e uma"], + [1100, "mil e cem"], + [1101, "mil, cento e uma"], + [2000, "duas mil"], + [2002, "duas mil e duas"], + [3000, "três mil"], + [21000, "vinte e uma mil"], + [100000, "cem mil"], + [200000, "duzentas mil"], + [300000, "trezentas mil"], + [1000000, "um milhão"], + [1000001, "um milhão e uma"], + [2000000, "dois milhões"], + [2000002, "dois milhões e duas"], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertNumberToWords(value, { gender: "feminine" }); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + }); +}); diff --git a/src/convert-number-to-words/convert-number-to-words.ts b/src/convert-number-to-words/convert-number-to-words.ts new file mode 100644 index 00000000..3117772c --- /dev/null +++ b/src/convert-number-to-words/convert-number-to-words.ts @@ -0,0 +1,60 @@ +import { applyWordsCase } from "../_internals/apply-words-case/apply-words-case"; +import { + NUMBER_TO_WORDS_MAX_VALUE, + type NumberToWordsGender, + numberToWords, + type WordsCase, +} from "../_internals/number-to-words/number-to-words"; + +export type ConvertNumberToWordsOptions = { + /** Grammatical gender used to agree "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies. Defaults to `"masculine"`. */ + gender?: NumberToWordsGender; + /** Letter case applied to the result: `"lower"` (unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Defaults to `"lower"`; an invalid value is ignored and `"lower"` is used instead. */ + case?: WordsCase; +}; + +/** + * Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), + * e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. + * + * Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value, + * the highest value expressible with the "trilhão" scale word) are supported; anything outside + * that range, `NaN` or a non-finite value (`Infinity`/`-Infinity`) returns `""`. A non-integer + * `value` is truncated toward zero before conversion (`12.9` behaves like `12`); this function + * only writes out whole numbers, it never spells out a decimal part (use + * `convertCurrencyToWords` for a monetary amount with cents). + * + * @param {number} value - The integer to convert. + * @param {ConvertNumberToWordsOptions} [options] - Optional formatting options. + * @param {NumberToWordsGender} [options.gender] - Grammatical gender for "um/dois" and the hundreds group. Defaults to `"masculine"`. + * @param {WordsCase} [options.case] - Letter case applied to the result. Defaults to `"lower"`. + * @returns {string} The cardinal number written out in Portuguese, or `""` for invalid input. + * + * @example + * ```typescript + * convertNumberToWords(123); // "cento e vinte e três" + * convertNumberToWords(1001); // "mil e um" + * convertNumberToWords(2000000); // "dois milhões" + * convertNumberToWords(-42); // "menos quarenta e dois" + * convertNumberToWords(2, { gender: "feminine" }); // "duas" + * convertNumberToWords(3, { case: "upper" }); // "TRÊS" + * convertNumberToWords(NaN); // "" + * ``` + * + * @see https://github.com/brazilian-utils/python/blob/main/brutils/currency.py + */ +export const convertNumberToWords = ( + value: number, + options?: ConvertNumberToWordsOptions, +): string => { + if (typeof value !== "number" || !Number.isFinite(value)) return ""; + + const truncated = Math.trunc(value); + + if (Math.abs(truncated) > NUMBER_TO_WORDS_MAX_VALUE) return ""; + + const words = numberToWords(Math.abs(truncated), { gender: options?.gender }); + const result = truncated < 0 ? `menos ${words}` : words; + + return applyWordsCase(result, options?.case); +}; From c5fbdb5d6f1804f360f7afd6fc1e2f805888416b Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 07/22] feat(currency-to-words): add convertCurrencyToWords MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spells a BRL amount out in Portuguese, e.g. convertCurrencyToWords(1523.45) -> "mil, quinhentos e vinte e três reais e quarenta e cinco centavos". --- .../convert-currency-to-words.test.ts | 404 ++++++++++++++++++ .../convert-currency-to-words.ts | 86 ++++ 2 files changed, 490 insertions(+) create mode 100644 src/convert-currency-to-words/convert-currency-to-words.test.ts create mode 100644 src/convert-currency-to-words/convert-currency-to-words.ts diff --git a/src/convert-currency-to-words/convert-currency-to-words.test.ts b/src/convert-currency-to-words/convert-currency-to-words.test.ts new file mode 100644 index 00000000..d05aa89a --- /dev/null +++ b/src/convert-currency-to-words/convert-currency-to-words.test.ts @@ -0,0 +1,404 @@ +import { NUMBER_TO_WORDS_MAX_VALUE } from "../_internals/number-to-words/number-to-words"; +import { describe, expect, test } from "../_internals/test/runtime"; +import { convertCurrencyToWords } from "./convert-currency-to-words"; + +describe("convertCurrencyToWords", () => { + test("should return 'zero reais' for 0", () => { + expect(convertCurrencyToWords(0)).toBe("zero reais"); + }); + + test("should return 'um centavo' for 0.01", () => { + expect(convertCurrencyToWords(0.01)).toBe("um centavo"); + }); + + test("should return 'um real' for 1.00", () => { + expect(convertCurrencyToWords(1.0)).toBe("um real"); + }); + + test("should return 'um real e um centavo' for 1.01", () => { + expect(convertCurrencyToWords(1.01)).toBe("um real e um centavo"); + }); + + test("should insert 'de' before 'reais' for a round million (1000000.00, brutils 'convert_real_to_text')", () => { + expect(convertCurrencyToWords(1000000.0)).toBe("um milhão de reais"); + }); + + test("should pluralize the 'de' connector for two round million (2000000.00)", () => { + expect(convertCurrencyToWords(2000000.0)).toBe("dois milhões de reais"); + }); + + test("should join reais and centavos with 'e' (1523.45, brutils 'convert_real_to_text' example)", () => { + expect(convertCurrencyToWords(1523.45)).toBe( + "mil, quinhentos e vinte e três reais e quarenta e cinco centavos", + ); + }); + + test("should not insert 'de' when a mil/hundred group follows the million group", () => { + expect(convertCurrencyToWords(1000230.0)).toBe("um milhão, duzentos e trinta reais"); + }); + + test("should return only the centavos when the reais part is zero", () => { + expect(convertCurrencyToWords(0.5)).toBe("cinquenta centavos"); + }); + + test("should return only the reais when the centavos part is zero", () => { + expect(convertCurrencyToWords(100.0)).toBe("cem reais"); + }); + + test("should truncate (not round) to 2 decimal places", () => { + expect(convertCurrencyToWords(1.999)).toBe("um real e noventa e nove centavos"); + }); + + test("should prefix negative amounts with 'menos'", () => { + expect(convertCurrencyToWords(-5.5)).toBe("menos cinco reais e cinquenta centavos"); + expect(convertCurrencyToWords(-0.01)).toBe("menos um centavo"); + }); + + test("should return '' when the reais part exceeds the maximum supported value", () => { + expect(convertCurrencyToWords(NUMBER_TO_WORDS_MAX_VALUE + 1)).toBe(""); + }); + + describe("invalid input", () => { + test("should return '' for NaN", () => { + expect(convertCurrencyToWords(Number.NaN)).toBe(""); + }); + + test("should return '' for Infinity and -Infinity", () => { + expect(convertCurrencyToWords(Number.POSITIVE_INFINITY)).toBe(""); + expect(convertCurrencyToWords(Number.NEGATIVE_INFINITY)).toBe(""); + }); + + test("should return '' for a non-number value", () => { + // @ts-expect-error + expect(convertCurrencyToWords("1523.45")).toBe(""); + // @ts-expect-error + expect(convertCurrencyToWords(null)).toBe(""); + // @ts-expect-error + expect(convertCurrencyToWords(undefined)).toBe(""); + }); + }); + + describe("zero amounts", () => { + test("should not prefix 'menos' when a negative amount truncates to nothing", () => { + expect(convertCurrencyToWords(-0.001)).toBe("zero reais"); + expect(convertCurrencyToWords(-0.009)).toBe("zero reais"); + }); + + test("should return 'zero reais' for negative zero", () => { + expect(convertCurrencyToWords(-0)).toBe("zero reais"); + }); + + test("should return 'zero reais' for an amount below one centavo", () => { + expect(convertCurrencyToWords(0.004)).toBe("zero reais"); + }); + }); + + describe("amounts too large to carry cents", () => { + test("should read an amount above Number.MAX_SAFE_INTEGER cents as whole reais", () => { + expect(convertCurrencyToWords(100_000_000_000_000.02)).toBe("cem trilhões de reais"); + }); + + test("should still report cents just below that limit", () => { + expect(convertCurrencyToWords(9_007_199_254_740.99)).toContain("noventa e nove centavos"); + }); + }); + + describe("case option", () => { + test("should keep the result lowercase by default", () => { + expect(convertCurrencyToWords(1000)).toBe("mil reais"); + }); + + test("should keep the result lowercase for 'lower'", () => { + expect(convertCurrencyToWords(1000, { case: "lower" })).toBe("mil reais"); + }); + + test("should capitalize only the first letter for 'sentence'", () => { + expect(convertCurrencyToWords(1000, { case: "sentence" })).toBe("Mil reais"); + expect(convertCurrencyToWords(0, { case: "sentence" })).toBe("Zero reais"); + }); + + test("should uppercase everything for 'upper', keeping accents", () => { + expect(convertCurrencyToWords(1000, { case: "upper" })).toBe("MIL REAIS"); + expect(convertCurrencyToWords(1523.45, { case: "upper" })).toBe( + "MIL, QUINHENTOS E VINTE E TRÊS REAIS E QUARENTA E CINCO CENTAVOS", + ); + expect(convertCurrencyToWords(-5.5, { case: "upper" })).toBe( + "MENOS CINCO REAIS E CINQUENTA CENTAVOS", + ); + }); + + test("should ignore an invalid case value and fall back to 'lower'", () => { + // @ts-expect-error + expect(convertCurrencyToWords(1000, { case: "invalid" })).toBe("mil reais"); + }); + }); + + describe("literal case tables", () => { + test("should match a hand-written string for every amount from R$ 0.00 to R$ 1.49, cent by cent", () => { + const cases: Array<[number, string]> = [ + [0, "zero reais"], + [1, "um centavo"], + [2, "dois centavos"], + [3, "três centavos"], + [4, "quatro centavos"], + [5, "cinco centavos"], + [6, "seis centavos"], + [7, "sete centavos"], + [8, "oito centavos"], + [9, "nove centavos"], + [10, "dez centavos"], + [11, "onze centavos"], + [12, "doze centavos"], + [13, "treze centavos"], + [14, "catorze centavos"], + [15, "quinze centavos"], + [16, "dezesseis centavos"], + [17, "dezessete centavos"], + [18, "dezoito centavos"], + [19, "dezenove centavos"], + [20, "vinte centavos"], + [21, "vinte e um centavos"], + [22, "vinte e dois centavos"], + [23, "vinte e três centavos"], + [24, "vinte e quatro centavos"], + [25, "vinte e cinco centavos"], + [26, "vinte e seis centavos"], + [27, "vinte e sete centavos"], + [28, "vinte e oito centavos"], + [29, "vinte e nove centavos"], + [30, "trinta centavos"], + [31, "trinta e um centavos"], + [32, "trinta e dois centavos"], + [33, "trinta e três centavos"], + [34, "trinta e quatro centavos"], + [35, "trinta e cinco centavos"], + [36, "trinta e seis centavos"], + [37, "trinta e sete centavos"], + [38, "trinta e oito centavos"], + [39, "trinta e nove centavos"], + [40, "quarenta centavos"], + [41, "quarenta e um centavos"], + [42, "quarenta e dois centavos"], + [43, "quarenta e três centavos"], + [44, "quarenta e quatro centavos"], + [45, "quarenta e cinco centavos"], + [46, "quarenta e seis centavos"], + [47, "quarenta e sete centavos"], + [48, "quarenta e oito centavos"], + [49, "quarenta e nove centavos"], + [50, "cinquenta centavos"], + [51, "cinquenta e um centavos"], + [52, "cinquenta e dois centavos"], + [53, "cinquenta e três centavos"], + [54, "cinquenta e quatro centavos"], + [55, "cinquenta e cinco centavos"], + [56, "cinquenta e seis centavos"], + [57, "cinquenta e sete centavos"], + [58, "cinquenta e oito centavos"], + [59, "cinquenta e nove centavos"], + [60, "sessenta centavos"], + [61, "sessenta e um centavos"], + [62, "sessenta e dois centavos"], + [63, "sessenta e três centavos"], + [64, "sessenta e quatro centavos"], + [65, "sessenta e cinco centavos"], + [66, "sessenta e seis centavos"], + [67, "sessenta e sete centavos"], + [68, "sessenta e oito centavos"], + [69, "sessenta e nove centavos"], + [70, "setenta centavos"], + [71, "setenta e um centavos"], + [72, "setenta e dois centavos"], + [73, "setenta e três centavos"], + [74, "setenta e quatro centavos"], + [75, "setenta e cinco centavos"], + [76, "setenta e seis centavos"], + [77, "setenta e sete centavos"], + [78, "setenta e oito centavos"], + [79, "setenta e nove centavos"], + [80, "oitenta centavos"], + [81, "oitenta e um centavos"], + [82, "oitenta e dois centavos"], + [83, "oitenta e três centavos"], + [84, "oitenta e quatro centavos"], + [85, "oitenta e cinco centavos"], + [86, "oitenta e seis centavos"], + [87, "oitenta e sete centavos"], + [88, "oitenta e oito centavos"], + [89, "oitenta e nove centavos"], + [90, "noventa centavos"], + [91, "noventa e um centavos"], + [92, "noventa e dois centavos"], + [93, "noventa e três centavos"], + [94, "noventa e quatro centavos"], + [95, "noventa e cinco centavos"], + [96, "noventa e seis centavos"], + [97, "noventa e sete centavos"], + [98, "noventa e oito centavos"], + [99, "noventa e nove centavos"], + [100, "um real"], + [101, "um real e um centavo"], + [102, "um real e dois centavos"], + [103, "um real e três centavos"], + [104, "um real e quatro centavos"], + [105, "um real e cinco centavos"], + [106, "um real e seis centavos"], + [107, "um real e sete centavos"], + [108, "um real e oito centavos"], + [109, "um real e nove centavos"], + [110, "um real e dez centavos"], + [111, "um real e onze centavos"], + [112, "um real e doze centavos"], + [113, "um real e treze centavos"], + [114, "um real e catorze centavos"], + [115, "um real e quinze centavos"], + [116, "um real e dezesseis centavos"], + [117, "um real e dezessete centavos"], + [118, "um real e dezoito centavos"], + [119, "um real e dezenove centavos"], + [120, "um real e vinte centavos"], + [121, "um real e vinte e um centavos"], + [122, "um real e vinte e dois centavos"], + [123, "um real e vinte e três centavos"], + [124, "um real e vinte e quatro centavos"], + [125, "um real e vinte e cinco centavos"], + [126, "um real e vinte e seis centavos"], + [127, "um real e vinte e sete centavos"], + [128, "um real e vinte e oito centavos"], + [129, "um real e vinte e nove centavos"], + [130, "um real e trinta centavos"], + [131, "um real e trinta e um centavos"], + [132, "um real e trinta e dois centavos"], + [133, "um real e trinta e três centavos"], + [134, "um real e trinta e quatro centavos"], + [135, "um real e trinta e cinco centavos"], + [136, "um real e trinta e seis centavos"], + [137, "um real e trinta e sete centavos"], + [138, "um real e trinta e oito centavos"], + [139, "um real e trinta e nove centavos"], + [140, "um real e quarenta centavos"], + [141, "um real e quarenta e um centavos"], + [142, "um real e quarenta e dois centavos"], + [143, "um real e quarenta e três centavos"], + [144, "um real e quarenta e quatro centavos"], + [145, "um real e quarenta e cinco centavos"], + [146, "um real e quarenta e seis centavos"], + [147, "um real e quarenta e sete centavos"], + [148, "um real e quarenta e oito centavos"], + [149, "um real e quarenta e nove centavos"], + ]; + const failures: Array<{ cents: number; actual: string; expected: string }> = []; + + for (const [cents, expected] of cases) { + const actual = convertCurrencyToWords(cents / 100); + if (actual !== expected) failures.push({ cents, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should match a hand-written string at reais boundaries, scale words and truncation cases", () => { + const cases: Array<[number, string]> = [ + [1000, "mil reais"], + [1000.01, "mil reais e um centavo"], + [1101, "mil, cento e um reais"], + [1101.01, "mil, cento e um reais e um centavo"], + [1523.45, "mil, quinhentos e vinte e três reais e quarenta e cinco centavos"], + [1000000, "um milhão de reais"], + [1000000.01, "um milhão de reais e um centavo"], + [2000000, "dois milhões de reais"], + [1000001, "um milhão e um reais"], + [ + 999999999999999, + "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais", + ], + [1.999, "um real e noventa e nove centavos"], + [100.5, "cem reais e cinquenta centavos"], + [2, "dois reais"], + [10.5, "dez reais e cinquenta centavos"], + [999999, "novecentos e noventa e nove mil, novecentos e noventa e nove reais"], + [100, "cem reais"], + [1000000000, "um bilhão de reais"], + [2000000000, "dois bilhões de reais"], + [1000000000000, "um trilhão de reais"], + [2000000000000, "dois trilhões de reais"], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertCurrencyToWords(value); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should reproduce every published brutils 'convert_real_to_text' example (tests/test_currency.py, lowercase here because brutils capitalizes and this library leaves casing to the caller)", () => { + const cases: Array<[number, string]> = [ + [0, "zero reais"], + [0.01, "um centavo"], + [0.5, "cinquenta centavos"], + [1, "um real"], + [-50.25, "menos cinquenta reais e vinte e cinco centavos"], + [1523.45, "mil, quinhentos e vinte e três reais e quarenta e cinco centavos"], + [1000000, "um milhão de reais"], + [2000000, "dois milhões de reais"], + [1000000000, "um bilhão de reais"], + [2000000000, "dois bilhões de reais"], + [1000000000000, "um trilhão de reais"], + [2000000000000, "dois trilhões de reais"], + [1000000.45, "um milhão de reais e quarenta e cinco centavos"], + [2000000000.99, "dois bilhões de reais e noventa e nove centavos"], + [ + 1234567890.5, + "um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa reais e cinquenta centavos", + ], + [0.001, "zero reais"], + [0.009, "zero reais"], + [-1000000, "menos um milhão de reais"], + [-2000000.5, "menos dois milhões de reais e cinquenta centavos"], + [1000000000.01, "um bilhão de reais e um centavo"], + [1000000000.99, "um bilhão de reais e noventa e nove centavos"], + [ + 999999999999.99, + "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos", + ], + [1000000000000.01, "um trilhão de reais e um centavo"], + [1000000000000.99, "um trilhão de reais e noventa e nove centavos"], + [ + 9999999999999.99, + "nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos", + ], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertCurrencyToWords(value); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should prefix 'menos' to a hand-written string for negative amounts", () => { + const cases: Array<[number, string]> = [ + [-0.01, "menos um centavo"], + [-1, "menos um real"], + [-1.5, "menos um real e cinquenta centavos"], + [-5.5, "menos cinco reais e cinquenta centavos"], + [-100, "menos cem reais"], + [-1000000, "menos um milhão de reais"], + [-0.001, "zero reais"], + [-0.009, "zero reais"], + ]; + const failures: Array<{ value: number; actual: string; expected: string }> = []; + + for (const [value, expected] of cases) { + const actual = convertCurrencyToWords(value); + if (actual !== expected) failures.push({ value, actual, expected }); + } + + expect(failures).toEqual([]); + }); + }); +}); diff --git a/src/convert-currency-to-words/convert-currency-to-words.ts b/src/convert-currency-to-words/convert-currency-to-words.ts new file mode 100644 index 00000000..ee5954d6 --- /dev/null +++ b/src/convert-currency-to-words/convert-currency-to-words.ts @@ -0,0 +1,86 @@ +import { applyWordsCase } from "../_internals/apply-words-case/apply-words-case"; +import { + NUMBER_TO_WORDS_MAX_VALUE, + numberToWords, + type WordsCase, +} from "../_internals/number-to-words/number-to-words"; + +export type ConvertCurrencyToWordsOptions = { + /** Letter case applied to the result: `"lower"` (unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Defaults to `"lower"`; an invalid value is ignored and `"lower"` is used instead. */ + case?: WordsCase; +}; + +const MILLION_SCALE_SUFFIXES = ["lhão", "lhões"]; + +const endsInMillionScale = (words: string): boolean => + MILLION_SCALE_SUFFIXES.some((suffix) => words.endsWith(suffix)); + +/** + * Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, + * the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` + * becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. + * + * `value` is truncated (not rounded) to 2 decimal places before conversion, matching + * `brutils`' `convert_real_to_text`. The singular noun is used for exactly 1 ("um real", + * "um centavo") and "de" is inserted before "reais" when the amount is a round million, + * billion or trillion of reais ("um milhão de reais", "dois milhões de reais"). An amount that + * truncates to nothing becomes `"zero reais"`, with no "menos" prefix even when `value` is + * negative (`-0.001` is not a debt of anything); any other negative amount is prefixed with + * "menos". `NaN`/non-finite values and amounts whose reais exceed `NUMBER_TO_WORDS_MAX_VALUE` + * (999 trillion) return `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a + * double cannot carry cents at all, so the amount is read as a whole number of reais instead of + * reporting cents that the input never held. + * + * @param {number} value - The monetary amount to convert, in reais (e.g. `1523.45` for R$ 1.523,45). + * @param {ConvertCurrencyToWordsOptions} [options] - Optional formatting options. + * @param {WordsCase} [options.case] - Letter case applied to the result. Defaults to `"lower"`. + * @returns {string} The amount written out in Portuguese, or `""` for invalid input. + * + * @example + * ```typescript + * convertCurrencyToWords(1523.45); // "mil, quinhentos e vinte e três reais e quarenta e cinco centavos" + * convertCurrencyToWords(1); // "um real" + * convertCurrencyToWords(0.01); // "um centavo" + * convertCurrencyToWords(1000000); // "um milhão de reais" + * convertCurrencyToWords(0); // "zero reais" + * convertCurrencyToWords(-5.5); // "menos cinco reais e cinquenta centavos" + * convertCurrencyToWords(1000, { case: "upper" }); // "MIL REAIS" + * ``` + * + * @see https://github.com/brazilian-utils/python/blob/main/brutils/currency.py + */ +export const convertCurrencyToWords = ( + value: number, + options?: ConvertCurrencyToWordsOptions, +): string => { + if (typeof value !== "number" || !Number.isFinite(value)) return ""; + + const absolute = Math.abs(value); + const hasExactCents = absolute * 100 <= Number.MAX_SAFE_INTEGER; + const totalCents = hasExactCents ? Math.trunc(Number((absolute * 100).toFixed(6))) : 0; + + const reais = hasExactCents ? Math.floor(totalCents / 100) : Math.trunc(absolute); + const centavos = hasExactCents ? totalCents % 100 : 0; + + if (reais > NUMBER_TO_WORDS_MAX_VALUE) return ""; + + const parts: string[] = []; + + if (reais > 0) { + const reaisWords = numberToWords(reais); + const connector = endsInMillionScale(reaisWords) ? "de " : ""; + parts.push(`${reaisWords} ${connector}${reais === 1 ? "real" : "reais"}`); + } + + if (centavos > 0) { + const centavosText = `${numberToWords(centavos)} ${centavos === 1 ? "centavo" : "centavos"}`; + parts.push(reais > 0 ? `e ${centavosText}` : centavosText); + } + + if (reais === 0 && centavos === 0) return applyWordsCase("zero reais", options?.case); + + const joined = parts.join(" "); + const result = value < 0 ? `menos ${joined}` : joined; + + return applyWordsCase(result, options?.case); +}; From b2fdba77ddbdf1ae87036fa64313797f9dc1395b Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 08/22] feat(date-to-words): add convertDateToWords Spells a date out in Portuguese, e.g. convertDateToWords("2024-01-01") -> "primeiro de janeiro de dois mil e vinte e quatro". --- .../convert-date-to-words.test.ts | 446 ++++++++++++++++++ .../convert-date-to-words.ts | 122 +++++ 2 files changed, 568 insertions(+) create mode 100644 src/convert-date-to-words/convert-date-to-words.test.ts create mode 100644 src/convert-date-to-words/convert-date-to-words.ts diff --git a/src/convert-date-to-words/convert-date-to-words.test.ts b/src/convert-date-to-words/convert-date-to-words.test.ts new file mode 100644 index 00000000..c1e522f5 --- /dev/null +++ b/src/convert-date-to-words/convert-date-to-words.test.ts @@ -0,0 +1,446 @@ +import { MONTH_NAMES, WEEKDAY_NAMES } from "../_internals/constants/number-words"; +import { describe, expect, test } from "../_internals/test/runtime"; +import { convertDateToWords } from "./convert-date-to-words"; + +describe("convertDateToWords", () => { + test("should return 'primeiro' for day 1", () => { + expect(convertDateToWords("01/01/2024")).toBe( + "primeiro de janeiro de dois mil e vinte e quatro", + ); + }); + + test("should return the cardinal number for day 2", () => { + expect(convertDateToWords("02/01/2024")).toBe("dois de janeiro de dois mil e vinte e quatro"); + }); + + test("should accept a 'dd/mm/yyyy' string", () => { + expect(convertDateToWords("25/12/2024")).toBe( + "vinte e cinco de dezembro de dois mil e vinte e quatro", + ); + }); + + test("should accept an ISO 'yyyy-mm-dd' string", () => { + expect(convertDateToWords("2024-12-25")).toBe( + "vinte e cinco de dezembro de dois mil e vinte e quatro", + ); + }); + + test("should accept a Date read by its local calendar date", () => { + expect(convertDateToWords(new Date(2024, 0, 1))).toBe( + "primeiro de janeiro de dois mil e vinte e quatro", + ); + expect(convertDateToWords(new Date(2024, 11, 25))).toBe( + "vinte e cinco de dezembro de dois mil e vinte e quatro", + ); + }); + + test("should reject February 29th on a non-leap year", () => { + expect(convertDateToWords("29/02/2023")).toBe(""); + }); + + test("should reject a day that does not exist in the given month", () => { + expect(convertDateToWords("31/04/2024")).toBe(""); + }); + + test("should reject an out of range month", () => { + expect(convertDateToWords("15/13/2024")).toBe(""); + expect(convertDateToWords("15/00/2024")).toBe(""); + }); + + test("should reject an out of range day", () => { + expect(convertDateToWords("00/01/2024")).toBe(""); + expect(convertDateToWords("32/01/2024")).toBe(""); + }); + + describe("case option", () => { + test("should keep the result lowercase by default", () => { + expect(convertDateToWords("01/01/2024")).toBe( + "primeiro de janeiro de dois mil e vinte e quatro", + ); + }); + + test("should keep the result lowercase for 'lower'", () => { + expect(convertDateToWords("01/01/2024", { case: "lower" })).toBe( + "primeiro de janeiro de dois mil e vinte e quatro", + ); + }); + + test("should capitalize only the first letter for 'sentence'", () => { + expect(convertDateToWords("01/01/2024", { case: "sentence" })).toBe( + "Primeiro de janeiro de dois mil e vinte e quatro", + ); + expect(convertDateToWords("10/05/1999", { case: "sentence" })).toBe( + "Dez de maio de mil novecentos e noventa e nove", + ); + }); + + test("should uppercase everything for 'upper', keeping accents", () => { + expect(convertDateToWords("02/03/2024", { case: "upper" })).toBe( + "DOIS DE MARÇO DE DOIS MIL E VINTE E QUATRO", + ); + }); + + test("should ignore an invalid case value and fall back to 'lower'", () => { + expect( + // @ts-expect-error + convertDateToWords("01/01/2024", { case: "invalid" }), + ).toBe("primeiro de janeiro de dois mil e vinte e quatro"); + }); + + test("should no longer accept the removed 'capitalize' option", () => { + expect( + // @ts-expect-error + convertDateToWords("01/01/2024", { capitalize: true }), + ).toBe("primeiro de janeiro de dois mil e vinte e quatro"); + }); + }); + + describe("style option", () => { + test("should default to 'full', spelling out day, month and year", () => { + expect(convertDateToWords("02/03/2024")).toBe("dois de março de dois mil e vinte e quatro"); + }); + + test("should write only the month name and leave day/year as digits for 'month'", () => { + expect(convertDateToWords("02/03/2024", { style: "month" })).toBe("2 de março de 2024"); + }); + + test("should write day 1 as 'primeiro' in 'full' style and as '1º' in 'month' style", () => { + expect(convertDateToWords("01/01/2024", { style: "full" })).toBe( + "primeiro de janeiro de dois mil e vinte e quatro", + ); + expect(convertDateToWords("01/01/2024", { style: "month" })).toBe("1º de janeiro de 2024"); + }); + + test("should ignore an invalid style value and fall back to 'full'", () => { + expect( + // @ts-expect-error + convertDateToWords("02/03/2024", { style: "invalid" }), + ).toBe("dois de março de dois mil e vinte e quatro"); + }); + + test("should match a hand-written string for every month in both styles", () => { + const cases: Array<[string, string, string]> = [ + ["02/01/2024", "dois de janeiro de dois mil e vinte e quatro", "2 de janeiro de 2024"], + ["02/02/2024", "dois de fevereiro de dois mil e vinte e quatro", "2 de fevereiro de 2024"], + ["02/03/2024", "dois de março de dois mil e vinte e quatro", "2 de março de 2024"], + ["02/04/2024", "dois de abril de dois mil e vinte e quatro", "2 de abril de 2024"], + ["02/05/2024", "dois de maio de dois mil e vinte e quatro", "2 de maio de 2024"], + ["02/06/2024", "dois de junho de dois mil e vinte e quatro", "2 de junho de 2024"], + ["02/07/2024", "dois de julho de dois mil e vinte e quatro", "2 de julho de 2024"], + ["02/08/2024", "dois de agosto de dois mil e vinte e quatro", "2 de agosto de 2024"], + ["02/09/2024", "dois de setembro de dois mil e vinte e quatro", "2 de setembro de 2024"], + ["02/10/2024", "dois de outubro de dois mil e vinte e quatro", "2 de outubro de 2024"], + ["02/11/2024", "dois de novembro de dois mil e vinte e quatro", "2 de novembro de 2024"], + ["02/12/2024", "dois de dezembro de dois mil e vinte e quatro", "2 de dezembro de 2024"], + ]; + const failures: Array<{ + input: string; + actualFull: string; + expectedFull: string; + actualMonth: string; + expectedMonth: string; + }> = []; + + for (const [input, expectedFull, expectedMonth] of cases) { + const actualFull = convertDateToWords(input, { style: "full" }); + const actualMonth = convertDateToWords(input, { style: "month" }); + if (actualFull !== expectedFull || actualMonth !== expectedMonth) { + failures.push({ input, actualFull, expectedFull, actualMonth, expectedMonth }); + } + } + + expect(failures).toEqual([]); + }); + }); + + describe("weekday option", () => { + test("should not prefix a weekday by default", () => { + expect(convertDateToWords("02/03/2024")).toBe("dois de março de dois mil e vinte e quatro"); + }); + + test("should list every weekday name in order (Date#getDay indexing)", () => { + expect(WEEKDAY_NAMES).toEqual([ + "domingo", + "segunda-feira", + "terça-feira", + "quarta-feira", + "quinta-feira", + "sexta-feira", + "sábado", + ]); + }); + + test("should prefix the pt-BR weekday and a comma for 7 consecutive known dates", () => { + const cases: Array<[string, string]> = [ + ["03/03/2024", "domingo, três de março de dois mil e vinte e quatro"], + ["04/03/2024", "segunda-feira, quatro de março de dois mil e vinte e quatro"], + ["05/03/2024", "terça-feira, cinco de março de dois mil e vinte e quatro"], + ["06/03/2024", "quarta-feira, seis de março de dois mil e vinte e quatro"], + ["07/03/2024", "quinta-feira, sete de março de dois mil e vinte e quatro"], + ["08/03/2024", "sexta-feira, oito de março de dois mil e vinte e quatro"], + ["02/03/2024", "sábado, dois de março de dois mil e vinte e quatro"], + ]; + const failures: Array<{ input: string; actual: string; expected: string }> = []; + + for (const [input, expected] of cases) { + const actual = convertDateToWords(input, { weekday: true }); + if (actual !== expected) failures.push({ input, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should compute the weekday from a Date's local calendar date", () => { + expect(convertDateToWords(new Date(2024, 2, 2), { weekday: true })).toBe( + "sábado, dois de março de dois mil e vinte e quatro", + ); + }); + + test("should combine with 'month' style", () => { + expect(convertDateToWords("01/01/2024", { weekday: true, style: "month" })).toBe( + "segunda-feira, 1º de janeiro de 2024", + ); + }); + + test("should combine with the 'case' option", () => { + expect(convertDateToWords("02/03/2024", { weekday: true, case: "sentence" })).toBe( + "Sábado, dois de março de dois mil e vinte e quatro", + ); + expect(convertDateToWords("02/03/2024", { weekday: true, case: "upper" })).toBe( + "SÁBADO, DOIS DE MARÇO DE DOIS MIL E VINTE E QUATRO", + ); + }); + }); + + describe("invalid input", () => { + test("should return '' for an invalid Date", () => { + expect(convertDateToWords(new Date("invalid"))).toBe(""); + }); + + test("should return '' for a malformed string", () => { + expect(convertDateToWords("2024/01/01")).toBe(""); + expect(convertDateToWords("01-01-2024")).toBe(""); + expect(convertDateToWords("not a date")).toBe(""); + expect(convertDateToWords("")).toBe(""); + }); + + test("should return '' for a non-Date/non-string value", () => { + // @ts-expect-error + expect(convertDateToWords(null)).toBe(""); + // @ts-expect-error + expect(convertDateToWords(undefined)).toBe(""); + // @ts-expect-error + expect(convertDateToWords(20240101)).toBe(""); + }); + }); + + describe("years outside the calendar", () => { + test("should return '' for year zero, which has no year to write out", () => { + expect(convertDateToWords("01/01/0000")).toBe(""); + expect(convertDateToWords("0000-01-01")).toBe(""); + }); + + test("should return '' for a Date with a year before year 1 instead of a truncated string", () => { + const beforeYearOne = new Date(2000, 0, 1); + beforeYearOne.setFullYear(-500); + + expect(convertDateToWords(beforeYearOne)).toBe(""); + }); + }); + + describe("leap years of the proleptic Gregorian calendar", () => { + test("should accept February 29th on a year divisible by 400", () => { + expect(convertDateToWords("29/02/2000")).toBe("vinte e nove de fevereiro de dois mil"); + expect(convertDateToWords("29/02/1600")).toBe( + "vinte e nove de fevereiro de mil e seiscentos", + ); + }); + + test("should reject February 29th on a century that is not divisible by 400", () => { + expect(convertDateToWords("29/02/1900")).toBe(""); + expect(convertDateToWords("29/02/2100")).toBe(""); + expect(convertDateToWords("29/02/1800")).toBe(""); + }); + + test("should accept February 29th on a year of the first century divisible by 4", () => { + expect(convertDateToWords("29/02/0004")).toBe("vinte e nove de fevereiro de quatro"); + expect(convertDateToWords("29/02/0096")).toBe("vinte e nove de fevereiro de noventa e seis"); + }); + + test("should reject February 29th on a year of the first century not divisible by 4", () => { + expect(convertDateToWords("29/02/0003")).toBe(""); + expect(convertDateToWords("29/02/0100")).toBe(""); + }); + }); + + test("should list every month name in order", () => { + expect(MONTH_NAMES).toEqual([ + "janeiro", + "fevereiro", + "março", + "abril", + "maio", + "junho", + "julho", + "agosto", + "setembro", + "outubro", + "novembro", + "dezembro", + ]); + }); + + describe("literal case tables", () => { + test("should match a hand-written string for the 1st and the 15th of every month", () => { + const cases: Array<[string, string]> = [ + ["01/01/2024", "primeiro de janeiro de dois mil e vinte e quatro"], + ["15/01/2024", "quinze de janeiro de dois mil e vinte e quatro"], + ["01/02/2024", "primeiro de fevereiro de dois mil e vinte e quatro"], + ["15/02/2024", "quinze de fevereiro de dois mil e vinte e quatro"], + ["01/03/2024", "primeiro de março de dois mil e vinte e quatro"], + ["15/03/2024", "quinze de março de dois mil e vinte e quatro"], + ["01/04/2024", "primeiro de abril de dois mil e vinte e quatro"], + ["15/04/2024", "quinze de abril de dois mil e vinte e quatro"], + ["01/05/2024", "primeiro de maio de dois mil e vinte e quatro"], + ["15/05/2024", "quinze de maio de dois mil e vinte e quatro"], + ["01/06/2024", "primeiro de junho de dois mil e vinte e quatro"], + ["15/06/2024", "quinze de junho de dois mil e vinte e quatro"], + ["01/07/2024", "primeiro de julho de dois mil e vinte e quatro"], + ["15/07/2024", "quinze de julho de dois mil e vinte e quatro"], + ["01/08/2024", "primeiro de agosto de dois mil e vinte e quatro"], + ["15/08/2024", "quinze de agosto de dois mil e vinte e quatro"], + ["01/09/2024", "primeiro de setembro de dois mil e vinte e quatro"], + ["15/09/2024", "quinze de setembro de dois mil e vinte e quatro"], + ["01/10/2024", "primeiro de outubro de dois mil e vinte e quatro"], + ["15/10/2024", "quinze de outubro de dois mil e vinte e quatro"], + ["01/11/2024", "primeiro de novembro de dois mil e vinte e quatro"], + ["15/11/2024", "quinze de novembro de dois mil e vinte e quatro"], + ["01/12/2024", "primeiro de dezembro de dois mil e vinte e quatro"], + ["15/12/2024", "quinze de dezembro de dois mil e vinte e quatro"], + ]; + const failures: Array<{ input: string; actual: string; expected: string }> = []; + + for (const [input, expected] of cases) { + const actual = convertDateToWords(input); + if (actual !== expected) failures.push({ input, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should match a hand-written string for every day of a 31 day month", () => { + const cases: Array<[string, string]> = [ + ["01/03/2024", "primeiro de março de dois mil e vinte e quatro"], + ["02/03/2024", "dois de março de dois mil e vinte e quatro"], + ["03/03/2024", "três de março de dois mil e vinte e quatro"], + ["04/03/2024", "quatro de março de dois mil e vinte e quatro"], + ["05/03/2024", "cinco de março de dois mil e vinte e quatro"], + ["06/03/2024", "seis de março de dois mil e vinte e quatro"], + ["07/03/2024", "sete de março de dois mil e vinte e quatro"], + ["08/03/2024", "oito de março de dois mil e vinte e quatro"], + ["09/03/2024", "nove de março de dois mil e vinte e quatro"], + ["10/03/2024", "dez de março de dois mil e vinte e quatro"], + ["11/03/2024", "onze de março de dois mil e vinte e quatro"], + ["12/03/2024", "doze de março de dois mil e vinte e quatro"], + ["13/03/2024", "treze de março de dois mil e vinte e quatro"], + ["14/03/2024", "catorze de março de dois mil e vinte e quatro"], + ["15/03/2024", "quinze de março de dois mil e vinte e quatro"], + ["16/03/2024", "dezesseis de março de dois mil e vinte e quatro"], + ["17/03/2024", "dezessete de março de dois mil e vinte e quatro"], + ["18/03/2024", "dezoito de março de dois mil e vinte e quatro"], + ["19/03/2024", "dezenove de março de dois mil e vinte e quatro"], + ["20/03/2024", "vinte de março de dois mil e vinte e quatro"], + ["21/03/2024", "vinte e um de março de dois mil e vinte e quatro"], + ["22/03/2024", "vinte e dois de março de dois mil e vinte e quatro"], + ["23/03/2024", "vinte e três de março de dois mil e vinte e quatro"], + ["24/03/2024", "vinte e quatro de março de dois mil e vinte e quatro"], + ["25/03/2024", "vinte e cinco de março de dois mil e vinte e quatro"], + ["26/03/2024", "vinte e seis de março de dois mil e vinte e quatro"], + ["27/03/2024", "vinte e sete de março de dois mil e vinte e quatro"], + ["28/03/2024", "vinte e oito de março de dois mil e vinte e quatro"], + ["29/03/2024", "vinte e nove de março de dois mil e vinte e quatro"], + ["30/03/2024", "trinta de março de dois mil e vinte e quatro"], + ["31/03/2024", "trinta e um de março de dois mil e vinte e quatro"], + ]; + const failures: Array<{ input: string; actual: string; expected: string }> = []; + + for (const [input, expected] of cases) { + const actual = convertDateToWords(input); + if (actual !== expected) failures.push({ input, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should reproduce every published brutils 'convert_date_to_text' example (tests/test_date_utils.py, lowercase here because brutils always capitalizes and this library exposes that as case: 'sentence')", () => { + const cases: Array<[string, string]> = [ + ["15/08/2024", "quinze de agosto de dois mil e vinte e quatro"], + ["01/01/2000", "primeiro de janeiro de dois mil"], + ["31/12/1999", "trinta e um de dezembro de mil novecentos e noventa e nove"], + ["29/02/2020", "vinte e nove de fevereiro de dois mil e vinte"], + ["01/01/1900", "primeiro de janeiro de mil e novecentos"], + ]; + const failures: Array<{ input: string; actual: string; expected: string }> = []; + + for (const [input, expected] of cases) { + const actual = convertDateToWords(input); + if (actual !== expected) failures.push({ input, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should match a hand-written string for day 31 and for the leap day", () => { + const cases: Array<[string, string]> = [ + ["31/01/2024", "trinta e um de janeiro de dois mil e vinte e quatro"], + ["29/02/2024", "vinte e nove de fevereiro de dois mil e vinte e quatro"], + ]; + const failures: Array<{ input: string; actual: string; expected: string }> = []; + + for (const [input, expected] of cases) { + const actual = convertDateToWords(input); + if (actual !== expected) failures.push({ input, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should render the year without the thousands comma for 1900, 1999, 2000, 2001, 2024 and 2100", () => { + const cases: Array<[string, string]> = [ + ["01/01/1101", "primeiro de janeiro de mil cento e um"], + ["01/01/1200", "primeiro de janeiro de mil e duzentos"], + ["01/01/1500", "primeiro de janeiro de mil e quinhentos"], + ["01/01/1900", "primeiro de janeiro de mil e novecentos"], + ["01/01/1999", "primeiro de janeiro de mil novecentos e noventa e nove"], + ["01/01/2000", "primeiro de janeiro de dois mil"], + ["01/01/2001", "primeiro de janeiro de dois mil e um"], + ["01/01/2024", "primeiro de janeiro de dois mil e vinte e quatro"], + ["01/01/2100", "primeiro de janeiro de dois mil e cem"], + ["10/05/1999", "dez de maio de mil novecentos e noventa e nove"], + ]; + const failures: Array<{ input: string; actual: string; expected: string }> = []; + + for (const [input, expected] of cases) { + const actual = convertDateToWords(input); + if (actual !== expected) failures.push({ input, actual, expected }); + } + + expect(failures).toEqual([]); + }); + + test("should give the same hand-written result for the 'dd/mm/yyyy' and the ISO form", () => { + const cases: Array<[string, string]> = [ + ["2024-01-02", "dois de janeiro de dois mil e vinte e quatro"], + ["1999-05-10", "dez de maio de mil novecentos e noventa e nove"], + ]; + const failures: Array<{ input: string; actual: string; expected: string }> = []; + + for (const [input, expected] of cases) { + const actual = convertDateToWords(input); + if (actual !== expected) failures.push({ input, actual, expected }); + } + + expect(failures).toEqual([]); + }); + }); +}); diff --git a/src/convert-date-to-words/convert-date-to-words.ts b/src/convert-date-to-words/convert-date-to-words.ts new file mode 100644 index 00000000..97193edf --- /dev/null +++ b/src/convert-date-to-words/convert-date-to-words.ts @@ -0,0 +1,122 @@ +import { applyWordsCase } from "../_internals/apply-words-case/apply-words-case"; +import { MONTH_NAMES, WEEKDAY_NAMES } from "../_internals/constants/number-words"; +import { numberToWords, type WordsCase } from "../_internals/number-to-words/number-to-words"; + +export type ConvertDateToWordsOptions = { + /** Letter case applied to the result: `"lower"` (unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Defaults to `"lower"`; an invalid value is ignored and `"lower"` is used instead. */ + case?: WordsCase; + /** Output style: `"full"` spells out the day, month and year (`"dois de março de dois mil e vinte e quatro"`); `"month"` spells out only the month name and leaves the day and year as digits (`"2 de março de 2024"`, day 1 as `"1º"`). Defaults to `"full"`; an invalid value is ignored and `"full"` is used instead. */ + style?: "full" | "month"; + /** Prefixes the pt-BR weekday name (lowercase) followed by a comma, e.g. `"sábado, dois de março de dois mil e vinte e quatro"`. The weekday is derived from the resolved calendar date (the `Date`'s local calendar date, or the parsed civil date for a string). Defaults to `false`. */ + weekday?: boolean; +}; + +const BR_DATE_REGEX = /^(\d{2})\/(\d{2})\/(\d{4})$/; +const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/; + +const MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +const isLeapYear = (year: number): boolean => + year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + +const daysInMonth = (year: number, month: number): number => + month === 2 && isLeapYear(year) ? 29 : MONTH_LENGTHS[month - 1]; + +const getWeekdayIndex = (year: number, month: number, day: number): number => { + const date = new Date(0); + date.setUTCFullYear(year, month - 1, day); + return date.getUTCDay(); +}; + +/** + * Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. + * `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. + * + * `value` can be a `Date` (read by its **local calendar date**, i.e. `getFullYear`/`getMonth`/ + * `getDate`, not its underlying UTC instant, the same convention used by `isHoliday`) or a + * string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format, both parsed as plain calendar dates + * with no timezone conversion. With the default `"full"` `options.style`, day 1 is written as + * "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name + * is spelled out and the day/year are written as digits (day 1 as `"1º"`). Month names are + * lowercase. In `"full"` style the year is written out as a cardinal number without the + * thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as + * `"mil novecentos e noventa e nove"`, not `"mil, novecentos e noventa e nove"`), matching how a + * date is read aloud. `options.weekday` prefixes the pt-BR weekday name (lowercase) followed by + * a comma. February 29th is accepted on the leap years of the proleptic Gregorian calendar + * (divisible by 4, except centuries that are not divisible by 400). Returns `""` when `value` is + * not one of those forms, is an invalid `Date`, names a day/month that does not exist (e.g. + * `"31/04/2024"` or `"29/02/2023"`), or falls before year 1, which has no year to write out. + * + * @param {Date|string} value - The date to convert: a `Date`, `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"`. + * @param {ConvertDateToWordsOptions} [options] - Optional formatting options. + * @param {WordsCase} [options.case] - Letter case applied to the result. Defaults to `"lower"`. + * @param {"full"|"month"} [options.style] - Output style. Defaults to `"full"`. + * @param {boolean} [options.weekday] - Prefixes the pt-BR weekday name and a comma. Defaults to `false`. + * @returns {string} The date written out in Portuguese, or `""` for invalid input. + * + * @example + * ```typescript + * convertDateToWords("01/01/2024"); // "primeiro de janeiro de dois mil e vinte e quatro" + * convertDateToWords("2024-01-02"); // "dois de janeiro de dois mil e vinte e quatro" + * convertDateToWords(new Date(2024, 0, 1)); // "primeiro de janeiro de dois mil e vinte e quatro" + * convertDateToWords("01/01/2024", { case: "sentence" }); // "Primeiro de janeiro de dois mil e vinte e quatro" + * convertDateToWords("02/03/2024", { style: "month" }); // "2 de março de 2024" + * convertDateToWords("01/01/2024", { style: "month" }); // "1º de janeiro de 2024" + * convertDateToWords("02/03/2024", { weekday: true }); // "sábado, dois de março de dois mil e vinte e quatro" + * convertDateToWords("10/05/1999"); // "dez de maio de mil novecentos e noventa e nove" + * convertDateToWords("31/04/2024"); // "" (April has 30 days) + * convertDateToWords("invalid"); // "" + * ``` + * + * @see https://github.com/brazilian-utils/python/blob/main/brutils/date_utils.py + */ +export const convertDateToWords = ( + value: Date | string, + options?: ConvertDateToWordsOptions, +): string => { + let year: number; + let month: number; + let day: number; + + if (value instanceof Date) { + if (Number.isNaN(value.getTime())) return ""; + + year = value.getFullYear(); + month = value.getMonth() + 1; + day = value.getDate(); + } else if (typeof value === "string") { + const brMatch = BR_DATE_REGEX.exec(value); + const isoMatch = ISO_DATE_REGEX.exec(value); + + if (brMatch) { + day = Number(brMatch[1]); + month = Number(brMatch[2]); + year = Number(brMatch[3]); + } else if (isoMatch) { + year = Number(isoMatch[1]); + month = Number(isoMatch[2]); + day = Number(isoMatch[3]); + } else { + return ""; + } + } else { + return ""; + } + + if (year < 1) return ""; + if (month < 1 || month > 12) return ""; + if (day < 1 || day > daysInMonth(year, month)) return ""; + + const monthName = MONTH_NAMES[month - 1]; + const isMonthStyle = options?.style === "month"; + + const dateWords = isMonthStyle + ? `${day === 1 ? "1º" : day} de ${monthName} de ${year}` + : `${day === 1 ? "primeiro" : numberToWords(day)} de ${monthName} de ${numberToWords(year).replaceAll(", ", " ")}`; + + const result = options?.weekday + ? `${WEEKDAY_NAMES[getWeekdayIndex(year, month, day)]}, ${dateWords}` + : dateWords; + + return applyWordsCase(result, options?.case); +}; From af86b51876bbb20f3b57906a93b7ef4fee80c31a Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 09/22] feat(cns): add isValidCns and formatCns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validates the Cartão Nacional de Saúde (15 digits): definitive numbers (starting 1/2) use a mod11 check shared with PIS; provisional numbers (starting 7/8/9) use a weighted sum that must be a multiple of 11. --- src/_internals/constants/cns.ts | 14 +++++ src/format-cns/format-cns.test.ts | 37 +++++++++++ src/format-cns/format-cns.ts | 32 ++++++++++ src/is-valid-cns/is-valid-cns.test.ts | 91 +++++++++++++++++++++++++++ src/is-valid-cns/is-valid-cns.ts | 73 +++++++++++++++++++++ 5 files changed, 247 insertions(+) create mode 100644 src/_internals/constants/cns.ts create mode 100644 src/format-cns/format-cns.test.ts create mode 100644 src/format-cns/format-cns.ts create mode 100644 src/is-valid-cns/is-valid-cns.test.ts create mode 100644 src/is-valid-cns/is-valid-cns.ts diff --git a/src/_internals/constants/cns.ts b/src/_internals/constants/cns.ts new file mode 100644 index 00000000..e95020aa --- /dev/null +++ b/src/_internals/constants/cns.ts @@ -0,0 +1,14 @@ +/** + * CNS (Cartão Nacional de Saúde) structural constants, shared by `isValidCns` and `formatCns`. + * + * @see Official: https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/ + */ + +/** Total digits of a CNS number. */ +export const CNS_LENGTH = 15; + +/** Digits of the PIS/PASEP/NIS derived base embedded in a definitive CNS (starts with 1 or 2). */ +export const CNS_DEFINITIVE_BASE_LENGTH = 11; + +/** Fixed 3 digit suffix appended after the check digit of a definitive CNS. */ +export const CNS_DEFINITIVE_SUFFIX = "001"; diff --git a/src/format-cns/format-cns.test.ts b/src/format-cns/format-cns.test.ts new file mode 100644 index 00000000..f155599b --- /dev/null +++ b/src/format-cns/format-cns.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { formatCns } from "./format-cns"; + +describe("formatCns", () => { + it("should format a CNS with the 3-4-4-4 space mask", () => { + expect(formatCns("")).toBe(""); + expect(formatCns("1")).toBe("1"); + expect(formatCns("12")).toBe("12"); + expect(formatCns("123")).toBe("123"); + expect(formatCns("1234")).toBe("123 4"); + expect(formatCns("123456789010001")).toBe("123 4567 8901 0001"); + }); + + it("should format a number CNS with the space mask", () => { + expect(formatCns(123456789010001)).toBe("123 4567 8901 0001"); + }); + + it("should pad the value with leading zeros when pad is true", () => { + expect(formatCns("", { pad: true })).toBe("000 0000 0000 0000"); + expect(formatCns("89010001", { pad: true })).toBe("000 0000 8901 0001"); + }); + + it("should not add digits after the CNS length (15)", () => { + expect(formatCns("123456789010001999")).toBe("123 4567 8901 0001"); + }); + + it("should remove all non numeric characters", () => { + expect(formatCns("123.456.789-01/0001")).toBe("123 4567 8901 0001"); + }); + + it("should return an empty string when the value is null or undefined", () => { + // @ts-expect-error + expect(formatCns(null)).toBe(""); + // @ts-expect-error + expect(formatCns(undefined)).toBe(""); + }); +}); diff --git a/src/format-cns/format-cns.ts b/src/format-cns/format-cns.ts new file mode 100644 index 00000000..0c3d224a --- /dev/null +++ b/src/format-cns/format-cns.ts @@ -0,0 +1,32 @@ +import { type FormatParams, format } from "../_internals/format/format"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +export type FormatCnsOptions = Pick; + +/** + * Formats a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 + * digits separated by spaces. + * + * @param {string|number} value - The CNS value to be formatted. It can be a string or a number. + * @param {FormatCnsOptions} [options] - Optional formatting options. + * @param {boolean} options.pad - If true, pads the value with leading zeros if necessary. + * @returns {string} The formatted CNS string in the pattern "000 0000 0000 0000". + * + * @example + * ```typescript + * formatCns("123456789010001"); // "123 4567 8901 0001" + * formatCns(123456789010001); // "123 4567 8901 0001" + * formatCns("89010001", { pad: true }); // "000 0000 8901 0001" + * ``` + * + * @see Official: https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/ + */ +export const formatCns = (value: string | number, options?: FormatCnsOptions): string => + isNullish(value) + ? "" + : format({ + pad: options?.pad, + value: sanitizeToDigits(value), + pattern: "000 0000 0000 0000", + }); diff --git a/src/is-valid-cns/is-valid-cns.test.ts b/src/is-valid-cns/is-valid-cns.test.ts new file mode 100644 index 00000000..180c00a6 --- /dev/null +++ b/src/is-valid-cns/is-valid-cns.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidCns } from "./is-valid-cns"; + +describe("isValidCns", () => { + describe("should return false", () => { + test("when it is null", () => { + // @ts-expect-error + expect(isValidCns(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidCns(undefined)).toBe(false); + }); + + test("when it is a boolean", () => { + // @ts-expect-error + expect(isValidCns(true)).toBe(false); + }); + + test("when it is an object", () => { + // @ts-expect-error + expect(isValidCns({})).toBe(false); + }); + + test("when it is an array", () => { + // @ts-expect-error + expect(isValidCns([])).toBe(false); + }); + + test("when it is an empty string", () => { + expect(isValidCns("")).toBe(false); + }); + + test("when it does not have 15 digits", () => { + expect(isValidCns("12345678901")).toBe(false); + }); + + test("when the first digit is not 1, 2, 7, 8 or 9", () => { + expect(isValidCns("312345678901234")).toBe(false); + expect(isValidCns("012345678901234")).toBe(false); + expect(isValidCns("612345678901234")).toBe(false); + }); + + test("when a definitive card has the wrong check digit", () => { + expect(isValidCns("123456789011001")).toBe(false); + }); + + test("when a definitive card does not carry the fixed 001 suffix", () => { + expect(isValidCns("123456789010002")).toBe(false); + }); + + test("when a definitive card base needed the +2 adjustment (raw check digit 10) and was not adjusted", () => { + expect(isValidCns("100000000060001")).toBe(false); + }); + + test("when a provisional card's weighted sum is not a multiple of 11", () => { + expect(isValidCns("700000000000001")).toBe(false); + }); + }); + + describe("should return true", () => { + test("for a definitive CNS whose raw check digit does not need the +2 adjustment", () => { + expect(isValidCns("123456789010001")).toBe(true); + }); + + test("for a definitive CNS as a number", () => { + expect(isValidCns(123456789010001)).toBe(true); + }); + + test("for a definitive CNS with a whitespace mask", () => { + expect(isValidCns("123 4567 8901 0001")).toBe(true); + }); + + test("for a definitive CNS whose base needed the +2 adjustment (raw check digit 10)", () => { + expect(isValidCns("100000000080001")).toBe(true); + }); + + test("for a provisional CNS starting with 7", () => { + expect(isValidCns("700000000000005")).toBe(true); + }); + + test("for a provisional CNS starting with 8", () => { + expect(isValidCns("800000000000001")).toBe(true); + }); + + test("for a provisional CNS starting with 9", () => { + expect(isValidCns("900000000000008")).toBe(true); + }); + }); +}); diff --git a/src/is-valid-cns/is-valid-cns.ts b/src/is-valid-cns/is-valid-cns.ts new file mode 100644 index 00000000..00e21c48 --- /dev/null +++ b/src/is-valid-cns/is-valid-cns.ts @@ -0,0 +1,73 @@ +import { + CNS_DEFINITIVE_BASE_LENGTH, + CNS_DEFINITIVE_SUFFIX, + CNS_LENGTH, +} from "../_internals/constants/cns"; +import { generateChecksum } from "../_internals/generate-checksum/generate-checksum"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +const DEFINITIVE_FIRST_DIGIT_REGEX = /^[12]/; +const PROVISIONAL_FIRST_DIGIT_REGEX = /^[789]/; + +const getDefinitiveCheckDigit = (base: string): number => { + const remainder = generateChecksum({ base, weight: 15 }) % 11; + const checkDigit = 11 - remainder; + + return checkDigit === 11 ? 0 : checkDigit; +}; + +const isValidDefinitive = (digits: string): boolean => { + const base = digits.slice(0, CNS_DEFINITIVE_BASE_LENGTH); + + let adjustedBase = base; + let checkDigit = getDefinitiveCheckDigit(base); + + if (checkDigit === 10) { + adjustedBase = String(Number(base) + 2).padStart(CNS_DEFINITIVE_BASE_LENGTH, "0"); + checkDigit = getDefinitiveCheckDigit(adjustedBase); + } + + return digits === `${adjustedBase}${checkDigit}${CNS_DEFINITIVE_SUFFIX}`; +}; + +const isValidProvisional = (digits: string): boolean => + generateChecksum({ base: digits, weight: 15 }) % 11 === 0; + +/** + * Validates a CNS (Cartão Nacional de Saúde) number, the unique identifier of a SUS + * (Sistema Único de Saúde) user, health professional or health facility. + * + * Definitive cards (starting with 1 or 2) embed an 11 digit PIS/PASEP/NIS derived base + * followed by a check digit calculated with the same mod 11 weighting used for PIS numbers + * (weights 15 down to 5) and the fixed suffix `"001"`. When the raw check digit computes to + * 10, that base is not a legitimate one: it is adjusted by adding 2 and the check digit is + * recalculated from the adjusted base, so only the adjusted base and its own check digit are + * accepted. Provisional cards (starting with 7, 8 or 9) are validated instead by a single + * weighted sum (weights 15 down to 1 over all 15 digits) that must be a multiple of 11. + * + * @param {string|number} value - The CNS value to be validated. + * @returns {boolean} True if the CNS is valid, false otherwise. + * + * @example + * ```typescript + * isValidCns("123456789010001"); // true (definitive) + * isValidCns("700000000000005"); // true (provisional) + * isValidCns("100000000060001"); // false (base needed the +2 adjustment, see tests) + * isValidCns("12345678901"); // false (wrong length) + * ``` + * + * @see Official: https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/ + */ +export const isValidCns = (value: string | number): boolean => { + if (typeof value !== "string" && typeof value !== "number") return false; + + const digits = sanitizeToDigits(value); + + if (digits.length !== CNS_LENGTH) return false; + + if (DEFINITIVE_FIRST_DIGIT_REGEX.test(digits)) return isValidDefinitive(digits); + + if (PROVISIONAL_FIRST_DIGIT_REGEX.test(digits)) return isValidProvisional(digits); + + return false; +}; From b7eb21ced88c5b20b5b70f8ee9ad4987884af77a Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 10/22] feat(certidao): add formatCertidao, isValidCertidao and parseCertidao MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validates the 32-digit matrícula of a birth/marriage/death certidão, a 2-stage mod11 checksum per Provimento CNJ 46/2015. --- src/_internals/constants/certidao.ts | 22 +++ src/format-certidao/format-certidao.test.ts | 66 ++++++++ src/format-certidao/format-certidao.ts | 44 ++++++ .../is-valid-certidao.test.ts | 149 ++++++++++++++++++ src/is-valid-certidao/is-valid-certidao.ts | 96 +++++++++++ src/parse-certidao/constants.ts | 23 +++ src/parse-certidao/parse-certidao.test.ts | 111 +++++++++++++ src/parse-certidao/parse-certidao.ts | 78 +++++++++ 8 files changed, 589 insertions(+) create mode 100644 src/_internals/constants/certidao.ts create mode 100644 src/format-certidao/format-certidao.test.ts create mode 100644 src/format-certidao/format-certidao.ts create mode 100644 src/is-valid-certidao/is-valid-certidao.test.ts create mode 100644 src/is-valid-certidao/is-valid-certidao.ts create mode 100644 src/parse-certidao/constants.ts create mode 100644 src/parse-certidao/parse-certidao.test.ts create mode 100644 src/parse-certidao/parse-certidao.ts diff --git a/src/_internals/constants/certidao.ts b/src/_internals/constants/certidao.ts new file mode 100644 index 00000000..9236c671 --- /dev/null +++ b/src/_internals/constants/certidao.ts @@ -0,0 +1,22 @@ +/** + * Layout of the matrícula of a certidão de registro civil, 32 digits grouped as + * 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + + * 3 (folha) + 7 (termo) + 2 (dígitos verificadores). + * + * @see Official: Provimento CNJ 46/2015, art. 1º and Anexo (Código Nacional de Serventias). + * @see Based on: http://ghiorzi.org/DVnew.htm Worked example of the two check digits + * (sums 288 and 309). + * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts + * Reference implementation, and the source of the matrículas used as test vectors. + * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php + * Third reference implementation agreeing on the weights and on the remainder of 10 read as 1. + */ + +export const CERTIDAO_LENGTH = 32; + +export const CERTIDAO_BASE_LENGTH = 30; + +export const CERTIDAO_PATTERN = "000000 00 00 0000 0 00000 000 0000000 00"; + +export const CERTIDAO_FORMAT_REGEX = + /^\d{6}[\s.\-/]*\d{2}[\s.\-/]*\d{2}[\s.\-/]*\d{4}[\s.\-/]*\d[\s.\-/]*\d{5}[\s.\-/]*\d{3}[\s.\-/]*\d{7}[\s.\-/]*\d{2}$/; diff --git a/src/format-certidao/format-certidao.test.ts b/src/format-certidao/format-certidao.test.ts new file mode 100644 index 00000000..7185a9f8 --- /dev/null +++ b/src/format-certidao/format-certidao.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { formatCertidao } from "./format-certidao"; + +describe("formatCertidao", () => { + describe("should return an empty string", () => { + test("when it is null", () => { + // @ts-expect-error + expect(formatCertidao(null)).toBe(""); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(formatCertidao(undefined)).toBe(""); + }); + + test("when it is an empty string", () => { + expect(formatCertidao("")).toBe(""); + }); + }); + + describe("should return the matrícula in the printed mask", () => { + test("for the 32 digits of the ghiorzi.org/DVnew.htm worked example", () => { + expect(formatCertidao("10453901552013100012021000012321")).toBe( + "104539 01 55 2013 1 00012 021 0000123 21", + ); + }); + + test("for a value already carrying the dotted mask of the Provimento", () => { + expect(formatCertidao("104539.01.55.2013.1.00012.021.0000123-21")).toBe( + "104539 01 55 2013 1 00012 021 0000123 21", + ); + }); + + test("for 094300 01 55 2010 1 00020 112 0000120-87 (klawdyo/validation-br certidao.spec.ts)", () => { + expect(formatCertidao("09430001552010100020112000012087")).toBe( + "094300 01 55 2010 1 00020 112 0000120 87", + ); + }); + }); + + describe("should return a partial mask", () => { + test("when the value has fewer than 32 digits", () => { + expect(formatCertidao("10453901")).toBe("104539 01"); + }); + + test("when the value has more than 32 digits, dropping the excess", () => { + expect(formatCertidao("1045390155201310001202100001232199")).toBe( + "104539 01 55 2013 1 00012 021 0000123 21", + ); + }); + }); + + describe("should left pad the value", () => { + test("when options.pad is true", () => { + expect(formatCertidao("1552010100020112000012087", { pad: true })).toBe( + "000000 01 55 2010 1 00020 112 0000120 87", + ); + }); + }); + + describe("should accept a number", () => { + test("for a value short enough to be an exact integer", () => { + expect(formatCertidao(104539015520)).toBe("104539 01 55 20"); + }); + }); +}); diff --git a/src/format-certidao/format-certidao.ts b/src/format-certidao/format-certidao.ts new file mode 100644 index 00000000..63501d41 --- /dev/null +++ b/src/format-certidao/format-certidao.ts @@ -0,0 +1,44 @@ +import { CERTIDAO_PATTERN } from "../_internals/constants/certidao"; +import { type FormatParams, format } from "../_internals/format/format"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +export type FormatCertidaoOptions = Pick; + +/** + * Formats the matrícula of a certidão de registro civil into the printed mask of the + * Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. + * + * @param {string|number} value - The matrícula value to be formatted. It can be a string or a number. + * @param {FormatCertidaoOptions} [options] - Optional formatting options. + * @param {boolean} options.pad - If true, pads the value with leading zeros if necessary. + * @returns {string} The formatted matrícula in the pattern "000000 00 00 0000 0 00000 000 0000000 00". + * + * @example + * ```typescript + * formatCertidao("10453901552013100012021000012321"); + * // "104539 01 55 2013 1 00012 021 0000123 21" + * + * formatCertidao("104539.01.55.2013.1.00012.021.0000123-21"); + * // "104539 01 55 2013 1 00012 021 0000123 21" + * + * formatCertidao("1552010100020112000012087", { pad: true }); + * // "000000 01 55 2010 1 00020 112 0000120 87" + * ``` + * + * @see Official: Provimento CNJ 46/2015, art. 1º and Anexo (Código Nacional de Serventias). + * @see Based on: http://ghiorzi.org/DVnew.htm Worked example of the two check digits + * (sums 288 and 309). + * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts + * Reference implementation, and the source of the matrículas used as test vectors. + * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php + * Third reference implementation agreeing on the weights and on the remainder of 10 read as 1. + */ +export const formatCertidao = (value: string | number, options?: FormatCertidaoOptions): string => + isNullish(value) + ? "" + : format({ + pad: options?.pad, + value: sanitizeToDigits(value), + pattern: CERTIDAO_PATTERN, + }); diff --git a/src/is-valid-certidao/is-valid-certidao.test.ts b/src/is-valid-certidao/is-valid-certidao.test.ts new file mode 100644 index 00000000..d6dd0f94 --- /dev/null +++ b/src/is-valid-certidao/is-valid-certidao.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidCertidao } from "./is-valid-certidao"; + +describe("isValidCertidao", () => { + describe("should return false", () => { + test("when it is null", () => { + // @ts-expect-error + expect(isValidCertidao(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidCertidao(undefined)).toBe(false); + }); + + test("when it is an array", () => { + // @ts-expect-error + expect(isValidCertidao([])).toBe(false); + }); + + test("when it is an empty string", () => { + expect(isValidCertidao("")).toBe(false); + }); + + test("when it does not have 32 digits", () => { + expect(isValidCertidao("104539015520131000120210000123")).toBe(false); + expect(isValidCertidao("104539015520131000120210000123210")).toBe(false); + }); + + test("when it has 32 digits but an unsupported separator", () => { + expect(isValidCertidao("104539#01#55#2013#1#00012#021#0000123#21")).toBe(false); + }); + + test("when it has 32 digits grouped outside the 6-2-2-4-1-5-3-7-2 mask", () => { + expect(isValidCertidao("1045.3901.5520.1310.0012.0210.0001.2321")).toBe(false); + }); + + test("when it contains letters", () => { + expect(isValidCertidao("A04539 01 55 2013 1 00012 021 0000123 21")).toBe(false); + }); + + test("when the check digits do not match (the ghiorzi.org example with 22)", () => { + expect(isValidCertidao("10453901552013100012021000012322")).toBe(false); + }); + + test("when only the second check digit is wrong (the ghiorzi.org example with 20)", () => { + expect(isValidCertidao("10453901552013100012021000012320")).toBe(false); + }); + + test("when the check digits are 99 (klawdyo/validation-br certidao.spec.ts invalid case)", () => { + expect(isValidCertidao("12345601552023100001001000000199")).toBe(false); + }); + + test("when it is a number, which cannot carry the 32 significant digits of a matrícula", () => { + expect(isValidCertidao(1045390155)).toBe(false); + }); + }); + + describe("should return true", () => { + test("for 104539.01.55.2013.1.00012.021.0000123-21, the worked example of ghiorzi.org/DVnew.htm", () => { + expect(isValidCertidao("104539 01 55 2013 1 00012 021 0000123 21")).toBe(true); + expect(isValidCertidao("10453901552013100012021000012321")).toBe(true); + }); + + test("for 131128 01 55 2010 1 00014 192 0006001 00 (klawdyo/validation-br certidao.spec.ts)", () => { + expect(isValidCertidao("131128 01 55 2010 1 00014 192 0006001 00")).toBe(true); + }); + + test("for 094003 01 55 2011 1 00110 002 0051917 43 (klawdyo/validation-br certidao.spec.ts)", () => { + expect(isValidCertidao("094003 01 55 2011 1 00110 002 0051917 43")).toBe(true); + }); + + test("for 094003 01 55 2010 1 00109 151 0051816 26 (klawdyo/validation-br certidao.spec.ts)", () => { + expect(isValidCertidao("094003 01 55 2010 1 00109 151 0051816 26")).toBe(true); + }); + + test("for 094300 01 55 2010 1 00020 112 0000120-87 with a dash before the check digits (klawdyo/validation-br certidao.spec.ts)", () => { + expect(isValidCertidao("094300 01 55 2010 1 00020 112 0000120-87")).toBe(true); + }); + + test("for 094946 01 55 2011 1 00241 196 0099147 54 (klawdyo/validation-br certidao.spec.ts)", () => { + expect(isValidCertidao("094946 01 55 2011 1 00241 196 0099147 54")).toBe(true); + }); + + test("for 001234 01 55 2026 1 00567 078 0099999 92 (klawdyo/validation-br certidao.spec.ts)", () => { + expect(isValidCertidao("001234 01 55 2026 1 00567 078 0099999 92")).toBe(true); + }); + + test("for a matrícula whose first modulus 11 remainder is 10 and is read as 1", () => { + expect(isValidCertidao("82668301552015209245842999011418")).toBe(true); + }); + + test("for a matrícula whose second modulus 11 remainder is 10 and is read as 1", () => { + expect(isValidCertidao("79975401552015772710866666109571")).toBe(true); + }); + + test("for the dotted mask of the Provimento", () => { + expect(isValidCertidao("104539.01.55.2013.1.00012.021.0000123-21")).toBe(true); + }); + }); + + describe("options.accept", () => { + test("should return true when the book type is in the accepted list", () => { + expect( + isValidCertidao("104539 01 55 2013 1 00012 021 0000123 21", { accept: ["birth"] }), + ).toBe(true); + }); + + test("should return true when the book type is one of several accepted types", () => { + expect( + isValidCertidao("104539 01 55 2013 1 00012 021 0000123 21", { + accept: ["death", "birth"], + }), + ).toBe(true); + }); + + test("should return false when the book type is not in the accepted list", () => { + expect( + isValidCertidao("104539 01 55 2013 1 00012 021 0000123 21", { accept: ["death"] }), + ).toBe(false); + }); + + test("should return false when the accepted list is empty", () => { + expect(isValidCertidao("104539 01 55 2013 1 00012 021 0000123 21", { accept: [] })).toBe( + false, + ); + }); + + test("should return true for an interdiction act (book code 9) when accepted", () => { + expect( + isValidCertidao("10453901552013900012021000012398", { accept: ["interdiction"] }), + ).toBe(true); + }); + + test("should return true when the check digits match and accept is not given, book code 0", () => { + expect(isValidCertidao("10453901552013000012021000012387")).toBe(true); + }); + + test("should return false when the book code is 0, outside the nine books of the Provimento, and accept is given", () => { + expect(isValidCertidao("10453901552013000012021000012387", { accept: ["birth"] })).toBe( + false, + ); + }); + + test("should return false when the matrícula itself is invalid, regardless of accept", () => { + expect(isValidCertidao("123456", { accept: ["birth"] })).toBe(false); + }); + }); +}); diff --git a/src/is-valid-certidao/is-valid-certidao.ts b/src/is-valid-certidao/is-valid-certidao.ts new file mode 100644 index 00000000..abeb098f --- /dev/null +++ b/src/is-valid-certidao/is-valid-certidao.ts @@ -0,0 +1,96 @@ +import { + CERTIDAO_BASE_LENGTH, + CERTIDAO_FORMAT_REGEX, + CERTIDAO_LENGTH, +} from "../_internals/constants/certidao"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { CERTIDAO_TYPES } from "../parse-certidao/constants"; +import type { CertidaoType } from "../parse-certidao/parse-certidao"; + +export type IsValidCertidaoOptions = { + /** Kinds of certidão (book types) that count as valid (default: all of them). */ + accept?: CertidaoType[]; +}; + +const getCheckDigit = (value: string): number => { + let weight = CERTIDAO_LENGTH - value.length; + let sum = 0; + + for (let i = 0; i < value.length; i++) { + sum += (value.charCodeAt(i) - 48) * weight; + weight = weight < 10 ? weight + 1 : 0; + } + + const remainder = sum % 11; + + return remainder === 10 ? 1 : remainder; +}; + +/** + * Validates the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the + * other acts kept by a serventia de registro civil das pessoas naturais). + * + * The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + + * 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), + * printed as "000000 00 00 0000 0 00000 000 0000000 00". Both check digits are modulus 11: the + * first weights the 30 base digits by 2, 3, ... 10, 0, 1, 2, ... restarting the cycle every 11 + * digits, the second weights the 31 digits that include the first check digit by 1, 2, ... 10, + * 0, 1, ... In both passes the check digit is the remainder itself, with a remainder of 10 read + * as 1. + * + * `options.accept` restricts which of the nine books (see `CertidaoType`, reused from + * `parseCertidao`) count as valid: when given, the book-type digit (fifteenth position of the + * matrícula) must map to one of the listed types, so a matrícula whose digit is `0` or greater + * than `9` (not one of the nine defined books) is also rejected. When omitted, every book type + * is accepted and the digit is not otherwise checked, matching the previous behavior. + * + * @param {string|number} value - The matrícula value to be validated. + * @param {IsValidCertidaoOptions} [options] - Optional validation options. + * @param {CertidaoType[]} [options.accept] - The book types to accept. Defaults to all of them. + * @returns {boolean} True if the matrícula is valid, false otherwise. + * + * @example + * ```typescript + * isValidCertidao("104539 01 55 2013 1 00012 021 0000123 21"); // true + * isValidCertidao("09430001552010100020112000012087"); // true + * isValidCertidao("104539 01 55 2013 1 00012 021 0000123 22"); // false (invalid check digits) + * isValidCertidao("123456"); // false (wrong length) + * isValidCertidao("104539 01 55 2013 1 00012 021 0000123 21", { accept: ["birth"] }); // true + * isValidCertidao("104539 01 55 2013 1 00012 021 0000123 21", { accept: ["death"] }); // false + * ``` + * + * @see Official: Provimento CNJ 46/2015, art. 1º and Anexo (Código Nacional de Serventias). + * @see Based on: http://ghiorzi.org/DVnew.htm Worked example of the two check digits + * (sums 288 and 309). + * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts + * Reference implementation, and the source of the matrículas used as test vectors. + * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php + * Third reference implementation agreeing on the weights and on the remainder of 10 read as 1. + */ +export const isValidCertidao = ( + value: string | number, + options?: IsValidCertidaoOptions, +): boolean => { + if (typeof value !== "string" && typeof value !== "number") return false; + + const digits = sanitizeToDigits(value); + + if (digits.length !== CERTIDAO_LENGTH) return false; + + if (!CERTIDAO_FORMAT_REGEX.test(String(value).trim())) return false; + + const base = digits.slice(0, CERTIDAO_BASE_LENGTH); + const first = getCheckDigit(base); + const second = getCheckDigit(`${base}${first}`); + + if (digits.slice(CERTIDAO_BASE_LENGTH) !== `${first}${second}`) return false; + + const accept = options?.accept; + + if (!Array.isArray(accept)) return true; + + const typeCode = digits.charCodeAt(14) - 48; + const type: CertidaoType | undefined = CERTIDAO_TYPES[typeCode - 1]; + + return type !== undefined && accept.includes(type); +}; diff --git a/src/parse-certidao/constants.ts b/src/parse-certidao/constants.ts new file mode 100644 index 00000000..02b77f21 --- /dev/null +++ b/src/parse-certidao/constants.ts @@ -0,0 +1,23 @@ +/** + * The nine books (tipo do livro) a matrícula de registro civil can point to, in the order of + * the codes 1 to 9: Livro A (nascimento), Livro B (casamento), Livro B Auxiliar (casamento + * religioso com efeito civil), Livro C (óbito), Livro C Auxiliar (natimorto), Livro D + * (proclamas), Livro E (demais atos), Livro E desdobrado para emancipações and Livro E + * desdobrado para interdições. + * + * @see Official: Provimento CNJ 46/2015, art. 1º and Anexo (Código Nacional de Serventias). + * @see Based on: http://ghiorzi.org/DVnew.htm Description of the nine books and their codes. + * @see Based on: https://github.com/Casilhero/brazilian-validators/blob/main/src/Support/CertidaoInfo.php + * Reference implementation agreeing on the same nine books, in the same order. + */ +export const CERTIDAO_TYPES = [ + "birth", + "marriage", + "religious-marriage", + "death", + "stillbirth", + "banns", + "other", + "emancipation", + "interdiction", +] as const; diff --git a/src/parse-certidao/parse-certidao.test.ts b/src/parse-certidao/parse-certidao.test.ts new file mode 100644 index 00000000..5d57f518 --- /dev/null +++ b/src/parse-certidao/parse-certidao.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { parseCertidao } from "./parse-certidao"; + +describe("parseCertidao", () => { + describe("should return null", () => { + test("when it is null", () => { + // @ts-expect-error + expect(parseCertidao(null)).toBeNull(); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(parseCertidao(undefined)).toBeNull(); + }); + + test("when it is an empty string", () => { + expect(parseCertidao("")).toBeNull(); + }); + + test("when the check digits do not match", () => { + expect(parseCertidao("10453901552013100012021000012322")).toBeNull(); + }); + + test("when the matrícula is otherwise invalid", () => { + expect(parseCertidao("not-a-matricula")).toBeNull(); + }); + + test("when the book code is 0, outside the nine books of the Provimento", () => { + expect(parseCertidao("10453901552013000012021000012387")).toBeNull(); + }); + }); + + describe("should return the parsed matrícula", () => { + test("for 104539.01.55.2013.1.00012.021.0000123-21, the worked example of ghiorzi.org/DVnew.htm", () => { + expect(parseCertidao("104539 01 55 2013 1 00012 021 0000123 21")).toEqual({ + registryCns: "104539", + acervo: "01", + service: "55", + year: 2013, + type: "birth", + typeCode: 1, + book: "00012", + page: "021", + term: "0000123", + checkDigits: "21", + }); + }); + + test("for 094300 01 55 2010 1 00020 112 0000120-87 (klawdyo/validation-br certidao.spec.ts)", () => { + expect(parseCertidao("094300 01 55 2010 1 00020 112 0000120-87")).toEqual({ + registryCns: "094300", + acervo: "01", + service: "55", + year: 2010, + type: "birth", + typeCode: 1, + book: "00020", + page: "112", + term: "0000120", + checkDigits: "87", + }); + }); + + test("for a marriage act, book code 2", () => { + expect(parseCertidao("10453901552013200012021000012376")?.type).toBe("marriage"); + }); + + test("for a religious marriage with civil effect, book code 3", () => { + expect(parseCertidao("10453901552013300012021000012310")?.type).toBe("religious-marriage"); + }); + + test("for a death act, book code 4", () => { + expect(parseCertidao("10453901552013400012021000012365")?.type).toBe("death"); + }); + + test("for a stillbirth act, book code 5", () => { + expect(parseCertidao("10453901552013500012021000012301")?.type).toBe("stillbirth"); + }); + + test("for a proclamas act, book code 6", () => { + expect(parseCertidao("10453901552013600012021000012354")?.type).toBe("banns"); + }); + + test("for the other acts of Livro E, book code 7", () => { + expect(parseCertidao("10453901552013700012021000012315")?.type).toBe("other"); + }); + + test("for an emancipation act, book code 8", () => { + expect(parseCertidao("10453901552013800012021000012343")?.type).toBe("emancipation"); + }); + + test("for an interdiction act, book code 9", () => { + expect(parseCertidao("10453901552013900012021000012398")?.type).toBe("interdiction"); + }); + + test("for a matrícula whose first modulus 11 remainder is 10 (826683 01 55 2015 2 09245 842 9990114 18)", () => { + expect(parseCertidao("82668301552015209245842999011418")).toEqual({ + registryCns: "826683", + acervo: "01", + service: "55", + year: 2015, + type: "marriage", + typeCode: 2, + book: "09245", + page: "842", + term: "9990114", + checkDigits: "18", + }); + }); + }); +}); diff --git a/src/parse-certidao/parse-certidao.ts b/src/parse-certidao/parse-certidao.ts new file mode 100644 index 00000000..88b9b181 --- /dev/null +++ b/src/parse-certidao/parse-certidao.ts @@ -0,0 +1,78 @@ +import { CERTIDAO_BASE_LENGTH } from "../_internals/constants/certidao"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { isValidCertidao } from "../is-valid-certidao/is-valid-certidao"; +import { CERTIDAO_TYPES } from "./constants"; + +export type CertidaoType = (typeof CERTIDAO_TYPES)[number]; + +export type Certidao = { + /** The 6 digit CNS (Código Nacional de Serventia) of the serventia that issued the act. */ + registryCns: string; + /** Acervo the book belongs to: "01" the serventia's own, "02" a collection it absorbed. */ + acervo: string; + /** Service rendered by the serventia, "55" for registro civil das pessoas naturais. */ + service: string; + /** Four digit year the act was recorded. */ + year: number; + /** The book the act belongs to, as an English name. */ + type: CertidaoType; + /** Raw book code, 1 to 9, as printed in the fifteenth position of the matrícula. */ + typeCode: number; + /** The 5 digit book (livro) number, zero padded. */ + book: string; + /** The 3 digit page (folha) number, zero padded. */ + page: string; + /** The 7 digit term (termo) number, zero padded. */ + term: string; + /** The 2 modulus 11 check digits of the matrícula. */ + checkDigits: string; +}; + +/** + * Parses the matrícula of a certidão de registro civil into its fields. + * + * Accepts the same input forms as `isValidCertidao` and returns `null` when the matrícula is + * not valid or when its book code is not one of the nine books defined by the Provimento, since + * an unknown book cannot be named. + * + * @param {string|number} value - The matrícula value to be parsed. + * @returns {Certidao | null} The parsed matrícula, or `null` when it is not valid. + * + * @example + * ```typescript + * parseCertidao("104539 01 55 2013 1 00012 021 0000123 21"); + * // { registryCns: "104539", acervo: "01", service: "55", year: 2013, type: "birth", + * // typeCode: 1, book: "00012", page: "021", term: "0000123", checkDigits: "21" } + * + * parseCertidao("invalid"); // null + * ``` + * + * @see Official: Provimento CNJ 46/2015, art. 1º and Anexo (Código Nacional de Serventias). + * @see Based on: http://ghiorzi.org/DVnew.htm Worked example of the two check digits + * (sums 288 and 309). + * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts + * Reference implementation, and the source of the matrículas used as test vectors. + * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php + * Third reference implementation agreeing on the weights and on the remainder of 10 read as 1. + */ +export const parseCertidao = (value: string | number): Certidao | null => { + if (!isValidCertidao(value)) return null; + + const digits = sanitizeToDigits(value); + const typeCode = digits.charCodeAt(14) - 48; + + if (typeCode < 1 || typeCode > CERTIDAO_TYPES.length) return null; + + return { + registryCns: digits.slice(0, 6), + acervo: digits.slice(6, 8), + service: digits.slice(8, 10), + year: Number(digits.slice(10, 14)), + type: CERTIDAO_TYPES[typeCode - 1], + typeCode, + book: digits.slice(15, 20), + page: digits.slice(20, 23), + term: digits.slice(23, CERTIDAO_BASE_LENGTH), + checkDigits: digits.slice(CERTIDAO_BASE_LENGTH), + }; +}; From 660a4fc60a66807e37f9550f15607e3dbf11a40b Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 11/22] feat(cei-cno-caepf): add isValidCei, formatCei, isValidCno, formatCno, isValidCaepf and formatCaepf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CEI (Cadastro Específico do INSS), CNO and CAEPF are the RFB registrations for a construction site or rural producer; CEI'"'"'s 12th digit is a mod11 check digit, computed by the new shared calculateCeiCheckDigit internal. --- .../calculate-cei-check-digit.test.ts | 24 +++++ .../calculate-cei-check-digit.ts | 35 ++++++++ src/_internals/constants/cei.ts | 18 ++++ src/format-caepf/constants.ts | 3 + src/format-caepf/format-caepf.test.ts | 51 +++++++++++ src/format-caepf/format-caepf.ts | 39 ++++++++ src/format-cei/constants.ts | 1 + src/format-cei/format-cei.test.ts | 47 ++++++++++ src/format-cei/format-cei.ts | 39 ++++++++ src/format-cno/constants.ts | 1 + src/format-cno/format-cno.test.ts | 47 ++++++++++ src/format-cno/format-cno.ts | 42 +++++++++ src/is-valid-caepf/constants.ts | 23 +++++ src/is-valid-caepf/is-valid-caepf.test.ts | 84 ++++++++++++++++++ src/is-valid-caepf/is-valid-caepf.ts | 63 +++++++++++++ src/is-valid-cei/is-valid-cei.test.ts | 88 +++++++++++++++++++ src/is-valid-cei/is-valid-cei.ts | 49 +++++++++++ src/is-valid-cno/is-valid-cno.test.ts | 83 +++++++++++++++++ src/is-valid-cno/is-valid-cno.ts | 49 +++++++++++ 19 files changed, 786 insertions(+) create mode 100644 src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.test.ts create mode 100644 src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.ts create mode 100644 src/_internals/constants/cei.ts create mode 100644 src/format-caepf/constants.ts create mode 100644 src/format-caepf/format-caepf.test.ts create mode 100644 src/format-caepf/format-caepf.ts create mode 100644 src/format-cei/constants.ts create mode 100644 src/format-cei/format-cei.test.ts create mode 100644 src/format-cei/format-cei.ts create mode 100644 src/format-cno/constants.ts create mode 100644 src/format-cno/format-cno.test.ts create mode 100644 src/format-cno/format-cno.ts create mode 100644 src/is-valid-caepf/constants.ts create mode 100644 src/is-valid-caepf/is-valid-caepf.test.ts create mode 100644 src/is-valid-caepf/is-valid-caepf.ts create mode 100644 src/is-valid-cei/is-valid-cei.test.ts create mode 100644 src/is-valid-cei/is-valid-cei.ts create mode 100644 src/is-valid-cno/is-valid-cno.test.ts create mode 100644 src/is-valid-cno/is-valid-cno.ts diff --git a/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.test.ts b/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.test.ts new file mode 100644 index 00000000..cd394d90 --- /dev/null +++ b/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "../test/runtime"; +import { calculateCeiCheckDigit } from "./calculate-cei-check-digit"; + +describe("calculateCeiCheckDigit", () => { + test("should return 5 for the base of 11.583.00249/85 (yiibr/yii2-br-validator CeiValidatorTest)", () => { + expect(calculateCeiCheckDigit("11583002498")).toBe(5); + }); + + test("should return 7 for the base of 27.729.71181/87 (yiibr/yii2-br-validator CeiValidatorTest)", () => { + expect(calculateCeiCheckDigit("27729711818")).toBe(7); + }); + + test("should return 6 for the base of 24.985.96743/86 (marcos-cruz/Documento CeiTest)", () => { + expect(calculateCeiCheckDigit("24985967438")).toBe(6); + }); + + test("should return 0 when the folded sum ends in 0 (CNO 401800097960 of the Receita Federal CNO dataset)", () => { + expect(calculateCeiCheckDigit("40180009796")).toBe(0); + }); + + test("should return 0 for a base of only zeros", () => { + expect(calculateCeiCheckDigit("00000000000")).toBe(0); + }); +}); diff --git a/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.ts b/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.ts new file mode 100644 index 00000000..d81fd9ae --- /dev/null +++ b/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.ts @@ -0,0 +1,35 @@ +import { CEI_WEIGHTS } from "../constants/cei"; +import { generateChecksum } from "../generate-checksum/generate-checksum"; + +/** + * Calculates the check digit of a CEI (Cadastro Específico do INSS) base, the same digit the + * CNO (Cadastro Nacional de Obras) kept when it replaced the CEI numbering. + * + * The 11 base digits are weighted by 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4 from left to right. + * The tens part and the units part of that sum are added together and the check digit is the + * complement of the units digit of the result to 10, with 10 mapped back to 0. + * + * @param {string} base - The 11 digits that precede the check digit. + * @returns {number} The check digit, 0 to 9. + * + * @example + * ```typescript + * calculateCeiCheckDigit("11583002498"); // 5 + * calculateCeiCheckDigit("40180009796"); // 0 + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno + * @see Official: Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: the + * 38432 works registered in Minas Gerais confirm the rule, and their check digits of 0 are + * what shows that a computed 10 maps back to 0, which neither reference implementation does. + * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php + * PHP reference implementation of the CEI check digit. + * @see Based on: https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs + * Second, independent reference implementation agreeing with the first. + */ +export const calculateCeiCheckDigit = (base: string): number => { + const sum = generateChecksum({ base, weight: CEI_WEIGHTS }); + const folded = Math.floor(sum / 10) + (sum % 10); + + return (10 - (folded % 10)) % 10; +}; diff --git a/src/_internals/constants/cei.ts b/src/_internals/constants/cei.ts new file mode 100644 index 00000000..a165ffa5 --- /dev/null +++ b/src/_internals/constants/cei.ts @@ -0,0 +1,18 @@ +/** + * Numbering shared by the CEI (Cadastro Específico do INSS) and by the CNO (Cadastro Nacional + * de Obras) that replaced it: 12 digits printed as "00.000.00000/00". + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno + * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php + * PHP reference implementation of the CEI check digit. + * @see Based on: https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs + * Second, independent reference implementation agreeing with the first. + */ + +export const CEI_LENGTH = 12; + +export const CEI_BASE_LENGTH = 11; + +export const CEI_WEIGHTS = [7, 4, 1, 8, 5, 2, 1, 6, 3, 7, 4]; + +export const CEI_FORMAT_REGEX = /^\d{2}[\s.\-/]*\d{3}[\s.\-/]*\d{5}[\s.\-/]*\d{2}$/; diff --git a/src/format-caepf/constants.ts b/src/format-caepf/constants.ts new file mode 100644 index 00000000..6e86247a --- /dev/null +++ b/src/format-caepf/constants.ts @@ -0,0 +1,3 @@ +export const PATTERN = "000.000.000/000-00"; + +export const CAEPF_LENGTH = 14; diff --git a/src/format-caepf/format-caepf.test.ts b/src/format-caepf/format-caepf.test.ts new file mode 100644 index 00000000..9d788a5d --- /dev/null +++ b/src/format-caepf/format-caepf.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { formatCaepf } from "./format-caepf"; + +describe("formatCaepf", () => { + test("should format a full 14 digit value", () => { + expect(formatCaepf("29311861000184")).toBe("293.118.610/001-84"); + }); + + test("should format a number input", () => { + expect(formatCaepf(41142260000101)).toBe("411.422.600/001-01"); + }); + + test("should format progressively as digits are typed", () => { + expect(formatCaepf("2")).toBe("2"); + expect(formatCaepf("29")).toBe("29"); + expect(formatCaepf("293")).toBe("293"); + expect(formatCaepf("2931")).toBe("293.1"); + expect(formatCaepf("29311")).toBe("293.11"); + expect(formatCaepf("293118")).toBe("293.118"); + }); + + test("should remove mask characters before formatting", () => { + expect(formatCaepf("293.118.610/001-84")).toBe("293.118.610/001-84"); + }); + + test("should truncate values longer than 14 digits", () => { + expect(formatCaepf("293118610001840000")).toBe("293.118.610/001-84"); + }); + + test("should pad the value with leading zeros when options.pad is true", () => { + expect(formatCaepf("184", { pad: true })).toBe("000.000.000/001-84"); + }); + + test("should not pad the value when options.pad is not given", () => { + expect(formatCaepf("184")).toBe("184"); + }); + + test("should return an empty string for an empty string", () => { + expect(formatCaepf("")).toBe(""); + }); + + test("should return an empty string for null", () => { + // @ts-expect-error + expect(formatCaepf(null)).toBe(""); + }); + + test("should return an empty string for undefined", () => { + // @ts-expect-error + expect(formatCaepf(undefined)).toBe(""); + }); +}); diff --git a/src/format-caepf/format-caepf.ts b/src/format-caepf/format-caepf.ts new file mode 100644 index 00000000..aefd98a3 --- /dev/null +++ b/src/format-caepf/format-caepf.ts @@ -0,0 +1,39 @@ +import { type FormatParams, format } from "../_internals/format/format"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { CAEPF_LENGTH, PATTERN } from "./constants"; + +export type FormatCaepfOptions = Pick; + +/** + * Formats a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number according to the + * official mask. + * + * Formats progressively, as far as the digits given go, so it can also be used as an input + * mask while the user is still typing. + * + * @param {string|number} value - The CAEPF value to be formatted. + * @param {FormatCaepfOptions} [options] - Optional formatting options. + * @param {boolean} [options.pad] - Whether to pad the value with leading zeros up to 14 digits. + * @returns {string} The formatted CAEPF string in the pattern "000.000.000/000-00", or an + * empty string when there is nothing to format. + * + * @example + * ```typescript + * formatCaepf("29311861000184"); // "293.118.610/001-84" + * formatCaepf(41142260000101); // "411.422.600/001-01" + * formatCaepf("184", { pad: true }); // "000.000.000/001-84" + * formatCaepf("184"); // "184" + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/caepf + */ +export const formatCaepf = (value: string | number, options?: FormatCaepfOptions): string => { + if (isNullish(value)) return ""; + + return format({ + pad: options?.pad, + value: sanitizeToDigits(value).slice(0, CAEPF_LENGTH), + pattern: PATTERN, + }); +}; diff --git a/src/format-cei/constants.ts b/src/format-cei/constants.ts new file mode 100644 index 00000000..74ec1d3b --- /dev/null +++ b/src/format-cei/constants.ts @@ -0,0 +1 @@ +export const PATTERN = "00.000.00000/00"; diff --git a/src/format-cei/format-cei.test.ts b/src/format-cei/format-cei.test.ts new file mode 100644 index 00000000..7ae8d28e --- /dev/null +++ b/src/format-cei/format-cei.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { formatCei } from "./format-cei"; + +describe("formatCei", () => { + test("should format a full 12 digit value", () => { + expect(formatCei("277297118187")).toBe("27.729.71181/87"); + }); + + test("should format a number input", () => { + expect(formatCei(249859674386)).toBe("24.985.96743/86"); + }); + + test("should format progressively as digits are typed", () => { + expect(formatCei("2")).toBe("2"); + expect(formatCei("27")).toBe("27"); + expect(formatCei("277")).toBe("27.7"); + expect(formatCei("2772")).toBe("27.72"); + expect(formatCei("27729")).toBe("27.729"); + expect(formatCei("277297")).toBe("27.729.7"); + }); + + test("should remove mask characters before formatting", () => { + expect(formatCei("11.583.00249/85")).toBe("11.583.00249/85"); + }); + + test("should truncate values longer than 12 digits", () => { + expect(formatCei("2772971181870000")).toBe("27.729.71181/87"); + }); + + test("should pad the value with leading zeros when options.pad is true", () => { + expect(formatCei("249", { pad: true })).toBe("00.000.00002/49"); + }); + + test("should return an empty string for an empty string", () => { + expect(formatCei("")).toBe(""); + }); + + test("should return an empty string for null", () => { + // @ts-expect-error + expect(formatCei(null)).toBe(""); + }); + + test("should return an empty string for undefined", () => { + // @ts-expect-error + expect(formatCei(undefined)).toBe(""); + }); +}); diff --git a/src/format-cei/format-cei.ts b/src/format-cei/format-cei.ts new file mode 100644 index 00000000..14f8496b --- /dev/null +++ b/src/format-cei/format-cei.ts @@ -0,0 +1,39 @@ +import { CEI_LENGTH } from "../_internals/constants/cei"; +import { type FormatParams, format } from "../_internals/format/format"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { PATTERN } from "./constants"; + +export type FormatCeiOptions = Pick; + +/** + * Formats a CEI (Cadastro Específico do INSS) number according to the official mask. + * + * Formats progressively, as far as the digits given go, so it can also be used as an input + * mask while the user is still typing. + * + * @param {string|number} value - The CEI value to be formatted. + * @param {FormatCeiOptions} [options] - Optional formatting options. + * @param {boolean} [options.pad] - Whether to pad the value with leading zeros up to 12 digits. + * @returns {string} The formatted CEI string in the pattern "00.000.00000/00", or an empty + * string when there is nothing to format. + * + * @example + * ```typescript + * formatCei("277297118187"); // "27.729.71181/87" + * formatCei(249859674386); // "24.985.96743/86" + * formatCei("249", { pad: true }); // "00.000.00002/49" + * formatCei("249"); // "24.9" + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno + */ +export const formatCei = (value: string | number, options?: FormatCeiOptions): string => { + if (isNullish(value)) return ""; + + return format({ + pad: options?.pad, + value: sanitizeToDigits(value).slice(0, CEI_LENGTH), + pattern: PATTERN, + }); +}; diff --git a/src/format-cno/constants.ts b/src/format-cno/constants.ts new file mode 100644 index 00000000..74ec1d3b --- /dev/null +++ b/src/format-cno/constants.ts @@ -0,0 +1 @@ +export const PATTERN = "00.000.00000/00"; diff --git a/src/format-cno/format-cno.test.ts b/src/format-cno/format-cno.test.ts new file mode 100644 index 00000000..a701adb3 --- /dev/null +++ b/src/format-cno/format-cno.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { formatCno } from "./format-cno"; + +describe("formatCno", () => { + test("should format a full 12 digit value", () => { + expect(formatCno("111130137368")).toBe("11.113.01373/68"); + }); + + test("should format a number input", () => { + expect(formatCno(401800097960)).toBe("40.180.00979/60"); + }); + + test("should format progressively as digits are typed", () => { + expect(formatCno("1")).toBe("1"); + expect(formatCno("11")).toBe("11"); + expect(formatCno("111")).toBe("11.1"); + expect(formatCno("1111")).toBe("11.11"); + expect(formatCno("11113")).toBe("11.113"); + expect(formatCno("111130")).toBe("11.113.0"); + }); + + test("should remove mask characters before formatting", () => { + expect(formatCno("11.084.01680/62")).toBe("11.084.01680/62"); + }); + + test("should truncate values longer than 12 digits", () => { + expect(formatCno("1111301373680000")).toBe("11.113.01373/68"); + }); + + test("should pad the value with leading zeros when options.pad is true", () => { + expect(formatCno("979", { pad: true })).toBe("00.000.00009/79"); + }); + + test("should return an empty string for an empty string", () => { + expect(formatCno("")).toBe(""); + }); + + test("should return an empty string for null", () => { + // @ts-expect-error + expect(formatCno(null)).toBe(""); + }); + + test("should return an empty string for undefined", () => { + // @ts-expect-error + expect(formatCno(undefined)).toBe(""); + }); +}); diff --git a/src/format-cno/format-cno.ts b/src/format-cno/format-cno.ts new file mode 100644 index 00000000..39b02818 --- /dev/null +++ b/src/format-cno/format-cno.ts @@ -0,0 +1,42 @@ +import { CEI_LENGTH } from "../_internals/constants/cei"; +import { type FormatParams, format } from "../_internals/format/format"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { PATTERN } from "./constants"; + +export type FormatCnoOptions = Pick; + +/** + * Formats a CNO (Cadastro Nacional de Obras) number according to the official mask. + * + * The CNO replaced the CEI for construction works and kept its numbering, so both share the + * same 12 digit, "00.000.00000/00" mask. + * + * Formats progressively, as far as the digits given go, so it can also be used as an input + * mask while the user is still typing. + * + * @param {string|number} value - The CNO value to be formatted. + * @param {FormatCnoOptions} [options] - Optional formatting options. + * @param {boolean} [options.pad] - Whether to pad the value with leading zeros up to 12 digits. + * @returns {string} The formatted CNO string in the pattern "00.000.00000/00", or an empty + * string when there is nothing to format. + * + * @example + * ```typescript + * formatCno("111130137368"); // "11.113.01373/68" + * formatCno(401800097960); // "40.180.00979/60" + * formatCno("979", { pad: true }); // "00.000.00009/79" + * formatCno("979"); // "97.9" + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno + */ +export const formatCno = (value: string | number, options?: FormatCnoOptions): string => { + if (isNullish(value)) return ""; + + return format({ + pad: options?.pad, + value: sanitizeToDigits(value).slice(0, CEI_LENGTH), + pattern: PATTERN, + }); +}; diff --git a/src/is-valid-caepf/constants.ts b/src/is-valid-caepf/constants.ts new file mode 100644 index 00000000..8aec3824 --- /dev/null +++ b/src/is-valid-caepf/constants.ts @@ -0,0 +1,23 @@ +/** + * Layout of the CAEPF (Cadastro de Atividade Econômica da Pessoa Física): 14 digits printed as + * "000.000.000/000-00", the first 9 being the CPF base of the holder, the next 3 the sequence + * of the holder's registrations and the last 2 the check digits. + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/caepf + * @see Based on: http://ghiorzi.org/DVnew.htm Description of the CAEPF layout and of the + * shift of 12 applied to the check digit pair. + * @see Based on: https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts + * Reference implementation agreeing on the weights and on the shift. + */ + +export const CAEPF_LENGTH = 14; + +export const CAEPF_BASE_LENGTH = 12; + +export const CAEPF_FIRST_WEIGHTS = [6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9]; + +export const CAEPF_SECOND_WEIGHTS = [5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9]; + +export const CAEPF_CHECK_DIGITS_OFFSET = 12; + +export const CAEPF_FORMAT_REGEX = /^\d{3}[\s.\-/]*\d{3}[\s.\-/]*\d{3}[\s.\-/]*\d{3}[\s.\-/]*\d{2}$/; diff --git a/src/is-valid-caepf/is-valid-caepf.test.ts b/src/is-valid-caepf/is-valid-caepf.test.ts new file mode 100644 index 00000000..f3f9d9c8 --- /dev/null +++ b/src/is-valid-caepf/is-valid-caepf.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidCaepf } from "./is-valid-caepf"; + +describe("isValidCaepf", () => { + describe("should return false", () => { + test("when it is null", () => { + // @ts-expect-error + expect(isValidCaepf(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidCaepf(undefined)).toBe(false); + }); + + test("when it is an array", () => { + // @ts-expect-error + expect(isValidCaepf([])).toBe(false); + }); + + test("when it is an empty string", () => { + expect(isValidCaepf("")).toBe(false); + }); + + test("when it does not have 14 digits", () => { + expect(isValidCaepf("1234567890")).toBe(false); + expect(isValidCaepf("293118610001840")).toBe(false); + }); + + test("when it contains letters", () => { + expect(isValidCaepf("abc.118.610/001-84")).toBe(false); + }); + + test("when it has 14 digits but an unsupported separator", () => { + expect(isValidCaepf("293#118#610#001#84")).toBe(false); + }); + + test("when every digit is the same", () => { + expect(isValidCaepf("00000000000000")).toBe(false); + expect(isValidCaepf("11111111111111")).toBe(false); + }); + + test("when the check digits do not match (29311861000185, Casilhero/brazilian-validators CaepfTest)", () => { + expect(isValidCaepf("29311861000185")).toBe(false); + }); + + test("when the check digits are zeroed (293.118.610/001-00, Casilhero/brazilian-validators CaepfTest)", () => { + expect(isValidCaepf("29311861000100")).toBe(false); + expect(isValidCaepf("293.118.610/001-00")).toBe(false); + }); + + test("when the shift of 12 is not applied (29311861000172 instead of 29311861000184)", () => { + expect(isValidCaepf("29311861000172")).toBe(false); + }); + }); + + describe("should return true", () => { + test("for 293.118.610/001-84 (Casilhero/brazilian-validators CaepfTest, from ghiorzi.org)", () => { + expect(isValidCaepf("293.118.610/001-84")).toBe(true); + expect(isValidCaepf("29311861000184")).toBe(true); + }); + + test("for 411.422.600/001-01 (VitorLuizC/brazilian-values isCAEPF doc example)", () => { + expect(isValidCaepf("411.422.600/001-01")).toBe(true); + expect(isValidCaepf("41142260000101")).toBe(true); + }); + + test("for 826.200.352/001-15, whose first modulus 11 remainder is 10", () => { + expect(isValidCaepf("82620035200115")).toBe(true); + }); + + test("for 701.801.963/001-02, whose second modulus 11 remainder is 10", () => { + expect(isValidCaepf("70180196300102")).toBe(true); + }); + + test("for a number input", () => { + expect(isValidCaepf(29311861000184)).toBe(true); + }); + + test("for a whitespace mask and surrounding whitespace", () => { + expect(isValidCaepf(" 293 118 610 001 84 ")).toBe(true); + }); + }); +}); diff --git a/src/is-valid-caepf/is-valid-caepf.ts b/src/is-valid-caepf/is-valid-caepf.ts new file mode 100644 index 00000000..a4c64b0d --- /dev/null +++ b/src/is-valid-caepf/is-valid-caepf.ts @@ -0,0 +1,63 @@ +import { generateChecksum } from "../_internals/generate-checksum/generate-checksum"; +import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { + CAEPF_BASE_LENGTH, + CAEPF_CHECK_DIGITS_OFFSET, + CAEPF_FIRST_WEIGHTS, + CAEPF_FORMAT_REGEX, + CAEPF_LENGTH, + CAEPF_SECOND_WEIGHTS, +} from "./constants"; + +const getCheckDigit = (base: string, weights: number[]): number => + (generateChecksum({ base, weight: weights }) % 11) % 10; + +/** + * Validates a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number. + * + * The CAEPF replaced the CEI for individuals who hire employees, such as rural producers and + * notary officials. It has 14 digits printed as "000.000.000/000-00": the 9 digit CPF base of + * the holder, a 3 digit sequence for the holder's several registrations and 2 check digits. + * Both check digits use the modulus 11 of the CNPJ, weights cycling from 2 to 9 from the right, + * with a remainder of 10 read as 0. The pair is then shifted by 12, wrapping around 100, so a + * CAEPF whose plain modulus 11 digits would be 72 is printed with 84. + * + * @param {string|number} value - The CAEPF value to be validated. + * @returns {boolean} True if the CAEPF is valid, false otherwise. + * + * @example + * ```typescript + * isValidCaepf("293.118.610/001-84"); // true + * isValidCaepf("41142260000101"); // true + * isValidCaepf(29311861000184); // true + * isValidCaepf("29311861000185"); // false (invalid check digits) + * isValidCaepf("00000000000000"); // false (repeated digits) + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/caepf + * @see Based on: http://ghiorzi.org/DVnew.htm Description of the CAEPF layout and of the + * shift of 12 applied to the check digit pair. + * @see Based on: https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts + * Reference implementation agreeing on the weights and on the shift. + * @see Based on: https://github.com/Casilhero/brazilian-validators/blob/main/src/Validators/Caepf.php + * Third reference implementation. + */ +export const isValidCaepf = (value: string | number): boolean => { + if (typeof value !== "string" && typeof value !== "number") return false; + + const digits = sanitizeToDigits(value); + + if (digits.length !== CAEPF_LENGTH) return false; + + if (!CAEPF_FORMAT_REGEX.test(String(value).trim())) return false; + + if (isRepeatedDigits(digits)) return false; + + const base = digits.slice(0, CAEPF_BASE_LENGTH); + const first = getCheckDigit(base, CAEPF_FIRST_WEIGHTS); + const second = getCheckDigit(`${base}${first}`, CAEPF_SECOND_WEIGHTS); + const expected = (first * 10 + second + CAEPF_CHECK_DIGITS_OFFSET) % 100; + + return Number(digits.slice(CAEPF_BASE_LENGTH)) === expected; +}; diff --git a/src/is-valid-cei/is-valid-cei.test.ts b/src/is-valid-cei/is-valid-cei.test.ts new file mode 100644 index 00000000..28616e8e --- /dev/null +++ b/src/is-valid-cei/is-valid-cei.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidCei } from "./is-valid-cei"; + +describe("isValidCei", () => { + describe("should return false", () => { + test("when it is null", () => { + // @ts-expect-error + expect(isValidCei(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidCei(undefined)).toBe(false); + }); + + test("when it is a boolean", () => { + // @ts-expect-error + expect(isValidCei(true)).toBe(false); + }); + + test("when it is an empty string", () => { + expect(isValidCei("")).toBe(false); + }); + + test("when it does not have 12 digits", () => { + expect(isValidCei("1234567890")).toBe(false); + expect(isValidCei("1234567890123")).toBe(false); + }); + + test("when it has 12 digits but an unsupported separator", () => { + expect(isValidCei("11#583#00249#85")).toBe(false); + }); + + test("when it has 12 digits grouped outside the 2-3-5-2 mask", () => { + expect(isValidCei("115.830.02498/5")).toBe(false); + }); + + test("when it contains letters", () => { + expect(isValidCei("aa.583.00249/85")).toBe(false); + }); + + test("when every digit is the same", () => { + expect(isValidCei("000000000000")).toBe(false); + expect(isValidCei("111111111111")).toBe(false); + }); + + test("when the check digit does not match (24.985.96743/68, yiibr/yii2-br-validator and marcos-cruz/Documento invalid case)", () => { + expect(isValidCei("24.985.96743/68")).toBe(false); + expect(isValidCei("249859674368")).toBe(false); + }); + + test("when only the check digit is wrong (11.583.00249/85 with a 4)", () => { + expect(isValidCei("115830024984")).toBe(false); + }); + }); + + describe("should return true", () => { + test("for 11.583.00249/85 (yiibr/yii2-br-validator CeiValidatorTest)", () => { + expect(isValidCei("11.583.00249/85")).toBe(true); + expect(isValidCei("115830024985")).toBe(true); + }); + + test("for 27.729.71181/87 (yiibr/yii2-br-validator CeiValidatorTest)", () => { + expect(isValidCei("27.729.71181/87")).toBe(true); + expect(isValidCei("277297118187")).toBe(true); + }); + + test("for 24.985.96743/86 (marcos-cruz/Documento CeiTest)", () => { + expect(isValidCei("24.985.96743/86")).toBe(true); + }); + + test("for 20.381.44217/87 (marcos-cruz/Documento CeiTest)", () => { + expect(isValidCei("20.381.44217/87")).toBe(true); + }); + + test("for 27.247.25187/86 (marcos-cruz/Documento CeiTest)", () => { + expect(isValidCei("27.247.25187/86")).toBe(true); + }); + + test("for a number input", () => { + expect(isValidCei(249859674386)).toBe(true); + }); + + test("for a whitespace mask and surrounding whitespace", () => { + expect(isValidCei(" 11 583 00249 85 ")).toBe(true); + }); + }); +}); diff --git a/src/is-valid-cei/is-valid-cei.ts b/src/is-valid-cei/is-valid-cei.ts new file mode 100644 index 00000000..74d4b81c --- /dev/null +++ b/src/is-valid-cei/is-valid-cei.ts @@ -0,0 +1,49 @@ +import { calculateCeiCheckDigit } from "../_internals/calculate-cei-check-digit/calculate-cei-check-digit"; +import { CEI_BASE_LENGTH, CEI_FORMAT_REGEX, CEI_LENGTH } from "../_internals/constants/cei"; +import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Validates a CEI (Cadastro Específico do INSS) number. + * + * The CEI identifies an employer that has no CNPJ, such as a construction work or a rural + * producer. It has 12 digits printed as "00.000.00000/00": 11 base digits and one check digit. + * The check digit weights the base by 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4, adds the tens part of + * that sum to its units part and takes the complement of the units digit of the result to 10, + * mapping 10 back to 0. The CEI was replaced by the CNO for construction works and by the CAEPF + * for individuals, but numbers already issued keep their meaning and their check digit. + * + * @param {string|number} value - The CEI value to be validated. + * @returns {boolean} True if the CEI is valid, false otherwise. + * + * @example + * ```typescript + * isValidCei("11.583.00249/85"); // true + * isValidCei("277297118187"); // true + * isValidCei(249859674386); // true + * isValidCei("24.985.96743/68"); // false (invalid check digit) + * isValidCei("000000000000"); // false (repeated digits) + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno + * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php + * PHP reference implementation of the CEI check digit. + * @see Based on: https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs + * Second, independent reference implementation agreeing with the first. + */ +export const isValidCei = (value: string | number): boolean => { + if (typeof value !== "string" && typeof value !== "number") return false; + + const digits = sanitizeToDigits(value); + + if (digits.length !== CEI_LENGTH) return false; + + if (!CEI_FORMAT_REGEX.test(String(value).trim())) return false; + + if (isRepeatedDigits(digits)) return false; + + return ( + calculateCeiCheckDigit(digits.slice(0, CEI_BASE_LENGTH)) === + digits.charCodeAt(CEI_BASE_LENGTH) - 48 + ); +}; diff --git a/src/is-valid-cno/is-valid-cno.test.ts b/src/is-valid-cno/is-valid-cno.test.ts new file mode 100644 index 00000000..4ce6d5d6 --- /dev/null +++ b/src/is-valid-cno/is-valid-cno.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidCno } from "./is-valid-cno"; + +describe("isValidCno", () => { + describe("should return false", () => { + test("when it is null", () => { + // @ts-expect-error + expect(isValidCno(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidCno(undefined)).toBe(false); + }); + + test("when it is an array", () => { + // @ts-expect-error + expect(isValidCno([])).toBe(false); + }); + + test("when it is an empty string", () => { + expect(isValidCno("")).toBe(false); + }); + + test("when it does not have 12 digits", () => { + expect(isValidCno("1234567890")).toBe(false); + expect(isValidCno("1234567890123")).toBe(false); + }); + + test("when it has 12 digits but an unsupported separator", () => { + expect(isValidCno("11#084#01680#62")).toBe(false); + }); + + test("when it has 12 digits grouped outside the 2-3-5-2 mask", () => { + expect(isValidCno("110.840.16806/2")).toBe(false); + }); + + test("when every digit is the same", () => { + expect(isValidCno("000000000000")).toBe(false); + }); + + test("when the check digit does not match (110840168062 with a 3)", () => { + expect(isValidCno("110840168063")).toBe(false); + }); + + test("when a check digit of 0 is replaced by another digit (401800097960 of the Receita Federal CNO dataset)", () => { + expect(isValidCno("401800097961")).toBe(false); + }); + }); + + describe("should return true", () => { + test("for 110840168062, an obra in Botelhos/MG of the Receita Federal CNO open dataset", () => { + expect(isValidCno("110840168062")).toBe(true); + expect(isValidCno("11.084.01680/62")).toBe(true); + }); + + test("for 111130137368, an obra in Campo do Meio/MG of the Receita Federal CNO open dataset", () => { + expect(isValidCno("111130137368")).toBe(true); + }); + + test("for 112772388267 and 113381018769 of the Receita Federal CNO open dataset", () => { + expect(isValidCno("112772388267")).toBe(true); + expect(isValidCno("113381018769")).toBe(true); + }); + + test("for 401800097960, whose check digit is 0 (Receita Federal CNO open dataset, Frutal/MG)", () => { + expect(isValidCno("401800097960")).toBe(true); + expect(isValidCno(401800097960)).toBe(true); + }); + + test("for 512070915160, whose check digit is 0 (Receita Federal CNO open dataset, Capitólio/MG)", () => { + expect(isValidCno("512070915160")).toBe(true); + }); + + test("for a legacy CEI number kept by the CNO (11.583.00249/85, yiibr/yii2-br-validator)", () => { + expect(isValidCno("11.583.00249/85")).toBe(true); + }); + + test("for a whitespace mask and surrounding whitespace", () => { + expect(isValidCno(" 11 084 01680 62 ")).toBe(true); + }); + }); +}); diff --git a/src/is-valid-cno/is-valid-cno.ts b/src/is-valid-cno/is-valid-cno.ts new file mode 100644 index 00000000..ec1abb88 --- /dev/null +++ b/src/is-valid-cno/is-valid-cno.ts @@ -0,0 +1,49 @@ +import { calculateCeiCheckDigit } from "../_internals/calculate-cei-check-digit/calculate-cei-check-digit"; +import { CEI_BASE_LENGTH, CEI_FORMAT_REGEX, CEI_LENGTH } from "../_internals/constants/cei"; +import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Validates a CNO (Cadastro Nacional de Obras) number, the registration of a construction work + * with the Receita Federal. + * + * The CNO replaced the CEI for construction works and kept its numbering: 12 digits printed as + * "00.000.00000/00", the last one being a check digit calculated over the 11 base digits with + * the weights 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4. A work registered under a legacy CEI keeps + * the same number in the CNO, so both registries validate identically. + * + * @param {string|number} value - The CNO value to be validated. + * @returns {boolean} True if the CNO is valid, false otherwise. + * + * @example + * ```typescript + * isValidCno("11.084.01680/62"); // true + * isValidCno("111130137368"); // true + * isValidCno(401800097960); // true + * isValidCno("110840168063"); // false (invalid check digit) + * isValidCno("000000000000"); // false (repeated digits) + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno + * @see Official: Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: every + * one of the 38432 works registered in Minas Gerais passes this check, which is what ties + * the CNO to the CEI rule and where the test vectors come from. + * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php + * PHP reference implementation of the CEI check digit. + */ +export const isValidCno = (value: string | number): boolean => { + if (typeof value !== "string" && typeof value !== "number") return false; + + const digits = sanitizeToDigits(value); + + if (digits.length !== CEI_LENGTH) return false; + + if (!CEI_FORMAT_REGEX.test(String(value).trim())) return false; + + if (isRepeatedDigits(digits)) return false; + + return ( + calculateCeiCheckDigit(digits.slice(0, CEI_BASE_LENGTH)) === + digits.charCodeAt(CEI_BASE_LENGTH) - 48 + ); +}; From 1a5b6b3d41f73e0ca4f4d845dab2646902110e69 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 12/22] feat(registro-profissional): add isValidRegistroProfissional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structural validation only (OAB/CRM/CREA, per council/state) — none of these councils publish a public check-digit algorithm. --- .../constants.ts | 21 +++++ .../is-valid-registro-profissional.test.ts | 89 ++++++++++++++++++ .../is-valid-registro-profissional.ts | 93 +++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 src/is-valid-registro-profissional/constants.ts create mode 100644 src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts create mode 100644 src/is-valid-registro-profissional/is-valid-registro-profissional.ts diff --git a/src/is-valid-registro-profissional/constants.ts b/src/is-valid-registro-profissional/constants.ts new file mode 100644 index 00000000..77d54280 --- /dev/null +++ b/src/is-valid-registro-profissional/constants.ts @@ -0,0 +1,21 @@ +/** + * Structural format of each supported professional council registration number. + * + * @see Official: https://www.oab.org.br/ Ordem dos Advogados do Brasil (OAB): "número de inscrição" + "seccional" (UF). + * @see Official: https://portal.cfm.org.br/ Conselho Federal de Medicina (CRM): registration number + UF. + * @see Official: https://cfo.org.br/ Conselho Federal de Odontologia (CRO): registration number + UF. + * @see Official: https://cfp.org.br/ Conselho Federal de Psicologia (CRP): 2 digit regional code + registration number. + * @see Official: https://cfc.org.br/ Conselho Federal de Contabilidade (CRC): UF + registration number + category (O/T) + check digit. + */ + +export type RegistroProfissionalCouncil = "OAB" | "CRM" | "CRO" | "CRP" | "CRC"; + +export const OAB_REGEX = /^(?\d{4,6})(?[A-Z]{2})$/; + +export const CRM_REGEX = /^(?\d{4,6})(?[A-Z]{2})$/; + +export const CRO_REGEX = /^(?\d{3,6})(?[A-Z]{2})$/; + +export const CRP_REGEX = /^(?\d{2})(?\d{4,6})$/; + +export const CRC_REGEX = /^(?[A-Z]{2})(?\d{4,6})(?[OT])(?\d)$/; diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts new file mode 100644 index 00000000..ebfba1ca --- /dev/null +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidRegistroProfissional } from "./is-valid-registro-profissional"; + +describe("isValidRegistroProfissional", () => { + describe("should return false", () => { + test("when value is null", () => { + // @ts-expect-error + expect(isValidRegistroProfissional(null, { council: "OAB" })).toBe(false); + }); + + test("when value is an empty string", () => { + expect(isValidRegistroProfissional("", { council: "OAB" })).toBe(false); + }); + + test("when options is null", () => { + // @ts-expect-error + expect(isValidRegistroProfissional("123456/SP", null)).toBe(false); + }); + + test("when the council is not supported (e.g. CREA)", () => { + // @ts-expect-error + expect(isValidRegistroProfissional("1234567890", { council: "CREA" })).toBe(false); + }); + + test("when an OAB number has no UF", () => { + expect(isValidRegistroProfissional("123456", { council: "OAB" })).toBe(false); + }); + + test("when an OAB number has too many digits", () => { + expect(isValidRegistroProfissional("1234567/SP", { council: "OAB" })).toBe(false); + }); + + test("when the UF is not a real Brazilian state code", () => { + expect(isValidRegistroProfissional("123456/ZZ", { council: "OAB" })).toBe(false); + }); + + test("when the UF does not match options.stateCode", () => { + expect(isValidRegistroProfissional("123456-RJ", { council: "OAB", stateCode: "SP" })).toBe( + false, + ); + }); + + test("when a CRP number has letters instead of the regional code", () => { + expect(isValidRegistroProfissional("SP/12345", { council: "CRP" })).toBe(false); + }); + + test("when a CRC number is missing the category letter", () => { + expect(isValidRegistroProfissional("SP-123456-3", { council: "CRC" })).toBe(false); + }); + + test("when a CRC number is missing the check digit", () => { + expect(isValidRegistroProfissional("SP-123456/O", { council: "CRC" })).toBe(false); + }); + }); + + describe("should return true", () => { + test("for a valid OAB number", () => { + expect(isValidRegistroProfissional("123456/SP", { council: "OAB" })).toBe(true); + }); + + test("for a valid OAB number matching options.stateCode", () => { + expect(isValidRegistroProfissional("123456-SP", { council: "OAB", stateCode: "SP" })).toBe( + true, + ); + }); + + test("for a valid CRM number", () => { + expect(isValidRegistroProfissional("54321/RJ", { council: "CRM" })).toBe(true); + }); + + test("for a valid CRO number", () => { + expect(isValidRegistroProfissional("12345/MG", { council: "CRO" })).toBe(true); + }); + + test("for a valid CRP number, ignoring options.stateCode", () => { + expect(isValidRegistroProfissional("06/12345", { council: "CRP", stateCode: "SP" })).toBe( + true, + ); + }); + + test("for a valid CRC number", () => { + expect(isValidRegistroProfissional("SP-123456/O-3", { council: "CRC" })).toBe(true); + }); + + test("for a valid CRC number of a técnico em contabilidade", () => { + expect(isValidRegistroProfissional("RJ-654321/T-9", { council: "CRC" })).toBe(true); + }); + }); +}); diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts new file mode 100644 index 00000000..657b99d8 --- /dev/null +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts @@ -0,0 +1,93 @@ +import { DATA, type StateCode } from "../_internals/constants/states"; +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; +import { + CRC_REGEX, + CRM_REGEX, + CRO_REGEX, + CRP_REGEX, + OAB_REGEX, + type RegistroProfissionalCouncil, +} from "./constants"; + +export type IsValidRegistroProfissionalOptions = { + /** The professional council that issued the registration number. */ + council: RegistroProfissionalCouncil; + /** The UF the registration is expected to belong to. Ignored for `"CRP"` (see below). */ + stateCode?: StateCode; +}; + +const REGEX_BY_COUNCIL: Record = { + OAB: OAB_REGEX, + CRM: CRM_REGEX, + CRO: CRO_REGEX, + CRP: CRP_REGEX, + CRC: CRC_REGEX, +}; + +const isKnownStateCode = (value: string): boolean => DATA.some((state) => state.code === value); + +/** + * Checks the structure of a professional council registration number (registro/inscrição + * profissional). + * + * This is a structural check only: it validates the digit count and, for the councils whose + * number embeds the UF, that the UF is a real Brazilian state code, optionally matching + * `options.stateCode`. It never computes or asserts a check digit, even for CRC, whose format + * includes one (the digit is only checked for presence and shape). + * + * Supported councils and what is validated: + * - `"OAB"` (Ordem dos Advogados do Brasil): 4 to 6 digits + UF, e.g. `"123456/SP"`. + * - `"CRM"` (Conselho Regional de Medicina): 4 to 6 digits + UF, e.g. `"123456-SP"`. + * - `"CRO"` (Conselho Regional de Odontologia): 3 to 6 digits + UF, e.g. `"12345/SP"`. + * - `"CRP"` (Conselho Regional de Psicologia): 2 digit regional code + 4 to 6 digits, e.g. + * `"06/12345"`. The regional code is not a literal UF (some regions cover more than one + * state), so `options.stateCode` is ignored for this council. + * - `"CRC"` (Conselho Regional de Contabilidade): UF + 4 to 6 digits + category (`"O"` for + * Contador/Organização Contábil or `"T"` for Técnico em Contabilidade) + 1 check digit + * whose value is not verified, e.g. `"SP-123456/O-3"`. + * + * CREA (Conselho Regional de Engenharia e Agronomia) is not supported: since the 2016 national + * unification (RNP) its registration number format could not be confirmed from an official, + * publicly documented source. + * + * @param {string} value - The registration number to be validated. + * @param {IsValidRegistroProfissionalOptions} options - The validation options. + * @param {RegistroProfissionalCouncil} options.council - The issuing council. + * @param {string} [options.stateCode] - The expected UF, ignored for `"CRP"`. + * @returns {boolean} True if the value has the structure of a registration number for the + * given council, false otherwise. + * + * @example + * ```typescript + * isValidRegistroProfissional("123456/SP", { council: "OAB" }); // true + * isValidRegistroProfissional("123456-SP", { council: "OAB", stateCode: "SP" }); // true + * isValidRegistroProfissional("123456-RJ", { council: "OAB", stateCode: "SP" }); // false (UF mismatch) + * isValidRegistroProfissional("06/12345", { council: "CRP" }); // true + * isValidRegistroProfissional("SP-123456/O-3", { council: "CRC" }); // true + * isValidRegistroProfissional("123456", { council: "OAB" }); // false (no UF) + * ``` + */ +export const isValidRegistroProfissional = ( + value: string, + options: IsValidRegistroProfissionalOptions, +): boolean => { + if (typeof value !== "string" || value === "") return false; + + if (typeof options !== "object" || options === null) return false; + + const regex = REGEX_BY_COUNCIL[options.council]; + + if (!regex) return false; + + const match = regex.exec(sanitizeToAlphanumeric(value)); + + if (!match?.groups) return false; + + const { uf } = match.groups; + + if (!uf) return true; + + if (!isKnownStateCode(uf)) return false; + + return !options.stateCode || uf === options.stateCode; +}; From 43960d296024af18cd7ed9cacdd1e224d7b5a30e Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 13/22] feat(credit-card): add isValidCreditCard Luhn checksum (ISO/IEC 7812), for validating a card number at checkout alongside boleto/Pix. --- src/is-valid-credit-card/constants.ts | 5 + .../is-valid-credit-card.test.ts | 91 +++++++++++++++++++ .../is-valid-credit-card.ts | 38 ++++++++ 3 files changed, 134 insertions(+) create mode 100644 src/is-valid-credit-card/constants.ts create mode 100644 src/is-valid-credit-card/is-valid-credit-card.test.ts create mode 100644 src/is-valid-credit-card/is-valid-credit-card.ts diff --git a/src/is-valid-credit-card/constants.ts b/src/is-valid-credit-card/constants.ts new file mode 100644 index 00000000..3ed3cbef --- /dev/null +++ b/src/is-valid-credit-card/constants.ts @@ -0,0 +1,5 @@ +/** Shortest digit count accepted by ISO/IEC 7812-1 issuer identification numbers. */ +export const MIN_LENGTH = 12; + +/** Longest digit count accepted by ISO/IEC 7812-1 issuer identification numbers. */ +export const MAX_LENGTH = 19; diff --git a/src/is-valid-credit-card/is-valid-credit-card.test.ts b/src/is-valid-credit-card/is-valid-credit-card.test.ts new file mode 100644 index 00000000..28a4e309 --- /dev/null +++ b/src/is-valid-credit-card/is-valid-credit-card.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidCreditCard } from "./is-valid-credit-card"; + +describe("isValidCreditCard", () => { + describe("should return true", () => { + test("for the Visa test number 4111111111111111", () => { + expect(isValidCreditCard("4111111111111111")).toBe(true); + }); + + test("for the Mastercard test number 5555555555554444", () => { + expect(isValidCreditCard("5555555555554444")).toBe(true); + }); + + test("for the American Express test number 378282246310005 (15 digits)", () => { + expect(isValidCreditCard("378282246310005")).toBe(true); + }); + + test("for the Discover test number 6011111111111117", () => { + expect(isValidCreditCard("6011111111111117")).toBe(true); + }); + + test("for a number input", () => { + expect(isValidCreditCard(4111111111111111)).toBe(true); + }); + + test("for a value with a spaced mask", () => { + expect(isValidCreditCard("4111 1111 1111 1111")).toBe(true); + }); + + test("for a value with a hyphenated mask", () => { + expect(isValidCreditCard("4111-1111-1111-1111")).toBe(true); + }); + + test("for the shortest accepted length (12 digits)", () => { + expect(isValidCreditCard("601100000004")).toBe(true); + }); + + test("for the longest accepted length (19 digits)", () => { + expect(isValidCreditCard("1234567890123456785")).toBe(true); + }); + }); + + describe("should return false", () => { + test("when the check digit does not match", () => { + expect(isValidCreditCard("4111111111111112")).toBe(false); + }); + + test("when it has fewer than 12 digits (11 digits)", () => { + expect(isValidCreditCard("60110000000")).toBe(false); + }); + + test("when it has more than 19 digits (20 digits)", () => { + expect(isValidCreditCard("12345678901234567850")).toBe(false); + }); + + test("when it is an empty string", () => { + expect(isValidCreditCard("")).toBe(false); + }); + + test("when it contains only letters", () => { + expect(isValidCreditCard("abcdabcdabcd")).toBe(false); + }); + + test("when it is null", () => { + // @ts-expect-error + expect(isValidCreditCard(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidCreditCard(undefined)).toBe(false); + }); + + test("when it is a boolean", () => { + // @ts-expect-error + expect(isValidCreditCard(true)).toBe(false); + // @ts-expect-error + expect(isValidCreditCard(false)).toBe(false); + }); + + test("when it is an object", () => { + // @ts-expect-error + expect(isValidCreditCard({})).toBe(false); + }); + + test("when it is an array", () => { + // @ts-expect-error + expect(isValidCreditCard([])).toBe(false); + }); + }); +}); diff --git a/src/is-valid-credit-card/is-valid-credit-card.ts b/src/is-valid-credit-card/is-valid-credit-card.ts new file mode 100644 index 00000000..1e4edece --- /dev/null +++ b/src/is-valid-credit-card/is-valid-credit-card.ts @@ -0,0 +1,38 @@ +import { mod10 } from "../_internals/mod10/mod10"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { MAX_LENGTH, MIN_LENGTH } from "./constants"; + +/** + * Validates a payment card number (crédito ou débito) using the Luhn algorithm. + * + * Accepts the usual mask characters (spaces and hyphens) between digits. Only checks the + * digit count (12 to 19, the range every ISO/IEC 7812-1 issuer identification number falls + * into) and the Luhn check digit; it performs no brand detection (Visa, Mastercard, Amex...), + * issuer range lookup or expiration/CVV checks. + * + * @param {string|number} value - The card number to be validated. + * @returns {boolean} True when `value` sanitizes to 12-19 digits ending in a valid Luhn check digit. + * + * @example + * ```typescript + * isValidCreditCard("4111111111111111"); // true (Visa test number) + * isValidCreditCard("5555555555554444"); // true (Mastercard test number) + * isValidCreditCard("378282246310005"); // true (American Express test number) + * isValidCreditCard("4111 1111 1111 1111"); // true (spaced mask) + * isValidCreditCard("4111111111111112"); // false (bad check digit) + * isValidCreditCard("123456789"); // false (too short) + * ``` + * + * @see Official: https://www.iso.org/standard/70484.html ISO/IEC 7812-1 (issuer identification numbers) + */ +export const isValidCreditCard = (value: string | number): boolean => { + if (typeof value !== "string" && typeof value !== "number") return false; + + const digits = sanitizeToDigits(value); + + if (digits.length < MIN_LENGTH || digits.length > MAX_LENGTH) return false; + + const checkDigit = digits.charCodeAt(digits.length - 1) - 48; + + return mod10(digits.slice(0, -1)) === checkDigit; +}; From 34df3bc41a761a51553011384dee20bc2fe3d8bd Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 14/22] feat(iban): add formatIban, isValidIban and parseIban Validates/parses a Brazilian IBAN (29 characters) with the mod-97 ISO 13616 checksum, per Bacen'"'"'s "Diretrizes IBAN". --- src/_internals/constants/iban.ts | 11 ++ src/format-iban/constants.ts | 2 + src/format-iban/format-iban.test.ts | 53 ++++++++++ src/format-iban/format-iban.ts | 40 ++++++++ src/is-valid-iban/is-valid-iban.test.ts | 88 ++++++++++++++++ src/is-valid-iban/is-valid-iban.ts | 55 ++++++++++ src/parse-iban/parse-iban.test.ts | 131 ++++++++++++++++++++++++ src/parse-iban/parse-iban.ts | 84 +++++++++++++++ 8 files changed, 464 insertions(+) create mode 100644 src/_internals/constants/iban.ts create mode 100644 src/format-iban/constants.ts create mode 100644 src/format-iban/format-iban.test.ts create mode 100644 src/format-iban/format-iban.ts create mode 100644 src/is-valid-iban/is-valid-iban.test.ts create mode 100644 src/is-valid-iban/is-valid-iban.ts create mode 100644 src/parse-iban/parse-iban.test.ts create mode 100644 src/parse-iban/parse-iban.ts diff --git a/src/_internals/constants/iban.ts b/src/_internals/constants/iban.ts new file mode 100644 index 00000000..f22d0984 --- /dev/null +++ b/src/_internals/constants/iban.ts @@ -0,0 +1,11 @@ +/** + * Layout of a Brazilian IBAN: `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB (the + * institution's Identificador do Sistema de Pagamentos Brasileiro, not the 3 digit COMPE code) + * + 5 digit branch (agência) + 10 digit account (conta) + 1 letter account type (`C` for + * conta corrente, `P` for conta poupança) + 1 alphanumeric owner indicator = 29 characters. + * Only Brazilian IBANs follow this layout; every other ISO 13616 country has its own. + * @see Official: https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Circular&numero=3625 Circular BCB nº 3.625/2013 (Diretrizes de Implementação do IBAN no Brasil) + */ +export const BR_IBAN_LENGTH = 29; + +export const BR_IBAN_REGEX = /^BR\d{2}\d{8}\d{5}\d{10}[CP][A-Z0-9]$/; diff --git a/src/format-iban/constants.ts b/src/format-iban/constants.ts new file mode 100644 index 00000000..734d0b4e --- /dev/null +++ b/src/format-iban/constants.ts @@ -0,0 +1,2 @@ +/** Number of characters per group when presenting an IBAN (ISO 13616 print format). */ +export const GROUP_SIZE = 4; diff --git a/src/format-iban/format-iban.test.ts b/src/format-iban/format-iban.test.ts new file mode 100644 index 00000000..8c958dfd --- /dev/null +++ b/src/format-iban/format-iban.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { formatIban } from "./format-iban"; + +describe("formatIban", () => { + it("should group a full IBAN in blocks of 4", () => { + expect(formatIban("BR1500000000000010932840814P2")).toBe( + "BR15 0000 0000 0000 1093 2840 814P 2", + ); + }); + + it("should uppercase a lowercase value", () => { + expect(formatIban("br1500000000000010932840814p2")).toBe( + "BR15 0000 0000 0000 1093 2840 814P 2", + ); + }); + + it("should format a partial value as far as it goes", () => { + expect(formatIban("")).toBe(""); + expect(formatIban("B")).toBe("B"); + expect(formatIban("BR")).toBe("BR"); + expect(formatIban("BR1")).toBe("BR1"); + expect(formatIban("BR15")).toBe("BR15"); + expect(formatIban("BR150")).toBe("BR15 0"); + expect(formatIban("BR1500000000000010932840814P")).toBe("BR15 0000 0000 0000 1093 2840 814P"); + }); + + it("should remove non alphanumeric characters before grouping", () => { + expect(formatIban("BR15 0000-0000.0000/1093 2840 814P 2")).toBe( + "BR15 0000 0000 0000 1093 2840 814P 2", + ); + }); + + it("should cap the result to 29 characters", () => { + expect(formatIban("BR1500000000000010932840814P2EXTRACHARS")).toBe( + "BR15 0000 0000 0000 1093 2840 814P 2", + ); + }); + + it("should return an empty string when the value is not a string", () => { + // @ts-expect-error + expect(formatIban(null)).toBe(""); + // @ts-expect-error + expect(formatIban(undefined)).toBe(""); + // @ts-expect-error + expect(formatIban(1500000000000)).toBe(""); + // @ts-expect-error + expect(formatIban(true)).toBe(""); + // @ts-expect-error + expect(formatIban({})).toBe(""); + // @ts-expect-error + expect(formatIban([])).toBe(""); + }); +}); diff --git a/src/format-iban/format-iban.ts b/src/format-iban/format-iban.ts new file mode 100644 index 00000000..f024f611 --- /dev/null +++ b/src/format-iban/format-iban.ts @@ -0,0 +1,40 @@ +import { BR_IBAN_LENGTH } from "../_internals/constants/iban"; +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; +import { GROUP_SIZE } from "./constants"; + +/** + * Formats a Brazilian IBAN by grouping it in blocks of 4 characters, the ISO 13616 "print" + * presentation used on statements and bank forms. + * + * Does not validate the check digits or the field layout; formats whatever is given, up to + * the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be + * used as an input mask. Use `isValidIban` to check validity. + * + * @param {string} value - The IBAN to be formatted. + * @returns {string} The IBAN uppercased and grouped in blocks of 4 characters, or an empty + * string when `value` is not a string. + * + * @example + * ```typescript + * formatIban("BR1500000000000010932840814P2"); // "BR15 0000 0000 0000 1093 2840 814P 2" + * formatIban("br1500000000000010932840814p2"); // "BR15 0000 0000 0000 1093 2840 814P 2" + * formatIban("BR15"); // "BR15" + * formatIban("BR1500000000000010932840814P2EXTRA"); // "BR15 0000 0000 0000 1093 2840 814P 2" + * ``` + * + * @see Official: https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Circular&numero=3625 Circular BCB nº 3.625/2013 (Diretrizes de Implementação do IBAN no Brasil) + */ +export const formatIban = (value: string): string => { + if (typeof value !== "string" || value === "") return ""; + + const sanitized = sanitizeToAlphanumeric(value).slice(0, BR_IBAN_LENGTH); + + let formatted = ""; + + for (let i = 0; i < sanitized.length; i++) { + if (i > 0 && i % GROUP_SIZE === 0) formatted += " "; + formatted += sanitized[i]; + } + + return formatted; +}; diff --git a/src/is-valid-iban/is-valid-iban.test.ts b/src/is-valid-iban/is-valid-iban.test.ts new file mode 100644 index 00000000..12e2ace1 --- /dev/null +++ b/src/is-valid-iban/is-valid-iban.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidIban } from "./is-valid-iban"; + +describe("isValidIban", () => { + describe("should return true", () => { + test("for a known valid IBAN (iban.com Brazil example)", () => { + expect(isValidIban("BR1500000000000010932840814P2")).toBe(true); + }); + + test("for a value with grouping spaces", () => { + expect(isValidIban("BR15 0000 0000 0000 1093 2840 814P 2")).toBe(true); + }); + + test("for a lowercase value", () => { + expect(isValidIban("br1500000000000010932840814p2")).toBe(true); + }); + + test("for a valid IBAN with a poupança (P) account type", () => { + expect(isValidIban("BR1460746948000020001234567P2")).toBe(true); + }); + + test("for a valid IBAN with a corrente (C) account type", () => { + expect(isValidIban("BR3860701190000010000012345C1")).toBe(true); + }); + }); + + describe("should return false", () => { + test("when the check digits do not match", () => { + expect(isValidIban("BR1500000000000010932840814P3")).toBe(false); + }); + + test("when the country code is not BR", () => { + expect(isValidIban("DE89370400440532013000")).toBe(false); + }); + + test("when it is shorter than 29 characters", () => { + expect(isValidIban("BR15000000000000109328408")).toBe(false); + }); + + test("when it is longer than 29 characters", () => { + expect(isValidIban("BR1500000000000010932840814P2000")).toBe(false); + }); + + test("when the account type is not C or P", () => { + expect(isValidIban("BR1500000000000010932840814X2")).toBe(false); + }); + + test("when it is an empty string", () => { + expect(isValidIban("")).toBe(false); + }); + + test("when it contains only whitespace", () => { + expect(isValidIban(" ")).toBe(false); + }); + + test("when it is null", () => { + // @ts-expect-error + expect(isValidIban(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidIban(undefined)).toBe(false); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(isValidIban(1500000000000)).toBe(false); + }); + + test("when it is a boolean", () => { + // @ts-expect-error + expect(isValidIban(true)).toBe(false); + // @ts-expect-error + expect(isValidIban(false)).toBe(false); + }); + + test("when it is an object", () => { + // @ts-expect-error + expect(isValidIban({})).toBe(false); + }); + + test("when it is an array", () => { + // @ts-expect-error + expect(isValidIban([])).toBe(false); + }); + }); +}); diff --git a/src/is-valid-iban/is-valid-iban.ts b/src/is-valid-iban/is-valid-iban.ts new file mode 100644 index 00000000..3f5f3e2e --- /dev/null +++ b/src/is-valid-iban/is-valid-iban.ts @@ -0,0 +1,55 @@ +import { BR_IBAN_LENGTH, BR_IBAN_REGEX } from "../_internals/constants/iban"; +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; + +const LETTER_CODE_A = 65; +const LETTER_CODE_Z = 90; +const LETTER_OFFSET = 55; + +const hasValidCheckDigits = (iban: string): boolean => { + const rearranged = iban.slice(4) + iban.slice(0, 4); + + let numeric = ""; + + for (let i = 0; i < rearranged.length; i++) { + const code = rearranged.charCodeAt(i); + numeric += + code >= LETTER_CODE_A && code <= LETTER_CODE_Z ? String(code - LETTER_OFFSET) : rearranged[i]; + } + + return BigInt(numeric) % 97n === 1n; +}; + +/** + * Validates a Brazilian IBAN (International Bank Account Number). + * + * Only Brazilian IBANs (country code `BR`) are recognized: the field layout of the other 90+ + * ISO 13616 countries is out of scope, so any non `BR` IBAN, however well formed, returns + * `false`. Accepts the usual grouping spaces and is case-insensitive. + * + * @param {string} value - The IBAN to be validated. + * @returns {boolean} True when `value` is a structurally valid Brazilian IBAN whose ISO 7064 + * MOD 97-10 check digits match. + * + * @example + * ```typescript + * isValidIban("BR1500000000000010932840814P2"); // true + * isValidIban("BR15 0000 0000 0000 1093 2840 814P 2"); // true (grouping spaces) + * isValidIban("br1500000000000010932840814p2"); // true (case-insensitive) + * isValidIban("BR1500000000000010932840814P3"); // false (bad check digits) + * isValidIban("DE89370400440532013000"); // false (non Brazilian IBAN) + * ``` + * + * @see Official: https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Circular&numero=3625 Circular BCB nº 3.625/2013 (Diretrizes de Implementação do IBAN no Brasil) + * @see Official: https://www.iso.org/standard/81090.html ISO/IEC 7064 (MOD 97-10 check digit algorithm) + * @see Based on: https://www.iban.com/structure Used to cross check the Brazil IBAN example. + */ +export const isValidIban = (value: string): boolean => { + if (typeof value !== "string" || value === "") return false; + + const sanitized = sanitizeToAlphanumeric(value); + + if (sanitized.length !== BR_IBAN_LENGTH) return false; + if (!BR_IBAN_REGEX.test(sanitized)) return false; + + return hasValidCheckDigits(sanitized); +}; diff --git a/src/parse-iban/parse-iban.test.ts b/src/parse-iban/parse-iban.test.ts new file mode 100644 index 00000000..aa442e80 --- /dev/null +++ b/src/parse-iban/parse-iban.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { formatIban } from "../format-iban/format-iban"; +import { isValidIban } from "../is-valid-iban/is-valid-iban"; +import { parseIban } from "./parse-iban"; + +describe("parseIban", () => { + describe("should return the parsed iban", () => { + test("for a known valid IBAN (iban.com Brazil example)", () => { + expect(parseIban("BR1500000000000010932840814P2")).toEqual({ + countryCode: "BR", + checkDigits: "15", + bankIspb: "00000000", + branch: "00001", + account: "0932840814", + accountType: "P", + owner: "2", + }); + }); + + test("for a value with grouping spaces", () => { + expect(parseIban("BR15 0000 0000 0000 1093 2840 814P 2")).toEqual({ + countryCode: "BR", + checkDigits: "15", + bankIspb: "00000000", + branch: "00001", + account: "0932840814", + accountType: "P", + owner: "2", + }); + }); + + test("for a lowercase value", () => { + expect(parseIban("br1500000000000010932840814p2")).toEqual({ + countryCode: "BR", + checkDigits: "15", + bankIspb: "00000000", + branch: "00001", + account: "0932840814", + accountType: "P", + owner: "2", + }); + }); + + test("for a valid IBAN with a corrente (C) account type", () => { + expect(parseIban("BR3860701190000010000012345C1")).toEqual({ + countryCode: "BR", + checkDigits: "38", + bankIspb: "60701190", + branch: "00001", + account: "0000012345", + accountType: "C", + owner: "1", + }); + }); + + test("for a valid IBAN with a poupança (P) account type and a non zero branch", () => { + expect(parseIban("BR1460746948000020001234567P2")).toEqual({ + countryCode: "BR", + checkDigits: "14", + bankIspb: "60746948", + branch: "00002", + account: "0001234567", + accountType: "P", + owner: "2", + }); + }); + }); + + describe("should return null", () => { + test("when the check digits do not match", () => { + expect(parseIban("BR1500000000000010932840814P3")).toBeNull(); + }); + + test("when the country code is not BR", () => { + expect(parseIban("DE89370400440532013000")).toBeNull(); + }); + + test("when it is shorter than 29 characters", () => { + expect(parseIban("BR15000000000000109328408")).toBeNull(); + }); + + test("when it is longer than 29 characters", () => { + expect(parseIban("BR1500000000000010932840814P2000")).toBeNull(); + }); + + test("when the account type is not C or P", () => { + expect(parseIban("BR1500000000000010932840814X2")).toBeNull(); + }); + + test("when it is an empty string", () => { + expect(parseIban("")).toBeNull(); + }); + + test("when it is null", () => { + // @ts-expect-error + expect(parseIban(null)).toBeNull(); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(parseIban(undefined)).toBeNull(); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(parseIban(150000000000)).toBeNull(); + }); + }); + + describe("should round-trip with formatIban and isValidIban", () => { + const IBANS = [ + "BR1500000000000010932840814P2", + "BR3860701190000010000012345C1", + "BR1460746948000020001234567P2", + ]; + + for (const iban of IBANS) { + test(`for ${iban}`, () => { + expect(isValidIban(iban)).toBe(true); + + const parsed = parseIban(iban); + + expect(parsed).not.toBeNull(); + expect( + `BR${parsed?.checkDigits}${parsed?.bankIspb}${parsed?.branch}${parsed?.account}${parsed?.accountType}${parsed?.owner}`, + ).toBe(iban); + expect(formatIban(iban)).toBe(formatIban(iban.toUpperCase())); + }); + } + }); +}); diff --git a/src/parse-iban/parse-iban.ts b/src/parse-iban/parse-iban.ts new file mode 100644 index 00000000..3970dc91 --- /dev/null +++ b/src/parse-iban/parse-iban.ts @@ -0,0 +1,84 @@ +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; +import { isValidIban } from "../is-valid-iban/is-valid-iban"; + +export type Iban = { + /** ISO 3166-1 alpha-2 country code. Always `"BR"`, the only country this parser supports. */ + countryCode: "BR"; + /** The 2 digit ISO 7064 MOD 97-10 check digits. */ + checkDigits: string; + /** The 8 digit ISPB (Identificador do Sistema de Pagamentos Brasileiro) of the institution. */ + bankIspb: string; + /** The 5 digit branch (agência) number, zero-padded. */ + branch: string; + /** The 10 digit account (conta) number, zero-padded. */ + account: string; + /** The account type: `"C"` for conta corrente, `"P"` for conta poupança. */ + accountType: "C" | "P"; + /** The 1 character alphanumeric owner indicator, distinguishing co-owners of the same account. */ + owner: string; +}; + +const COUNTRY_CODE_LENGTH = 2; +const CHECK_DIGITS_LENGTH = 2; +const ISPB_LENGTH = 8; +const BRANCH_LENGTH = 5; +const ACCOUNT_LENGTH = 10; +const ACCOUNT_TYPE_LENGTH = 1; + +const COUNTRY_CODE_END = COUNTRY_CODE_LENGTH; +const CHECK_DIGITS_END = COUNTRY_CODE_END + CHECK_DIGITS_LENGTH; +const ISPB_END = CHECK_DIGITS_END + ISPB_LENGTH; +const BRANCH_END = ISPB_END + BRANCH_LENGTH; +const ACCOUNT_END = BRANCH_END + ACCOUNT_LENGTH; +const ACCOUNT_TYPE_END = ACCOUNT_END + ACCOUNT_TYPE_LENGTH; + +/** + * Parses a Brazilian IBAN (International Bank Account Number) into its fields. + * + * The 29 character Brazilian IBAN is laid out as 2 (country code, always `BR`) + 2 (ISO 7064 + * MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, `C` or `P`) + * + 1 (owner indicator). Only Brazilian IBANs are supported: the field layout of the other ISO + * 13616 countries is out of scope, so a well-formed non `BR` IBAN also returns `null`. + * + * Accepts the same input forms as `isValidIban` (grouping spaces, lowercase) and returns `null` + * whenever `isValidIban` would return `false`. + * + * @param {string} value - The IBAN to be parsed. + * @returns {Iban|null} The parsed IBAN, or `null` when it is not a valid Brazilian IBAN. + * + * @example + * ```typescript + * parseIban("BR1500000000000010932840814P2"); + * // { + * // countryCode: "BR", + * // checkDigits: "15", + * // bankIspb: "00000000", + * // branch: "00001", + * // account: "0932840814", + * // accountType: "P", + * // owner: "2", + * // } + * + * parseIban("BR15 0000 0000 0000 1093 2840 814P 2"); // same result (grouping spaces) + * parseIban("DE89370400440532013000"); // null (non Brazilian IBAN) + * parseIban("BR1500000000000010932840814P3"); // null (bad check digits) + * ``` + * + * @see Official: https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Circular&numero=3625 Circular BCB nº 3.625/2013 (Diretrizes de Implementação do IBAN no Brasil) + * @see Official: https://www.iso.org/standard/81090.html ISO/IEC 7064 (MOD 97-10 check digit algorithm) + */ +export const parseIban = (value: string): Iban | null => { + if (!isValidIban(value)) return null; + + const sanitized = sanitizeToAlphanumeric(value); + + return { + countryCode: "BR", + checkDigits: sanitized.slice(COUNTRY_CODE_END, CHECK_DIGITS_END), + bankIspb: sanitized.slice(CHECK_DIGITS_END, ISPB_END), + branch: sanitized.slice(ISPB_END, BRANCH_END), + account: sanitized.slice(BRANCH_END, ACCOUNT_END), + accountType: sanitized.charAt(ACCOUNT_END) === "C" ? "C" : "P", + owner: sanitized.slice(ACCOUNT_TYPE_END), + }; +}; From 4e161cbaff0698c205a0c2ab6363e0477683991a Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 15/22] feat(vin): add isValidVin Validates a 17-character VIN/chassi (ISO 3779) with the NHTSA mod11 check digit at position 9. --- src/is-valid-vin/constants.ts | 51 +++++++++++++ src/is-valid-vin/is-valid-vin.test.ts | 100 ++++++++++++++++++++++++++ src/is-valid-vin/is-valid-vin.ts | 60 ++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 src/is-valid-vin/constants.ts create mode 100644 src/is-valid-vin/is-valid-vin.test.ts create mode 100644 src/is-valid-vin/is-valid-vin.ts diff --git a/src/is-valid-vin/constants.ts b/src/is-valid-vin/constants.ts new file mode 100644 index 00000000..afd1e525 --- /dev/null +++ b/src/is-valid-vin/constants.ts @@ -0,0 +1,51 @@ +/** + * ISO 3779 layout of a VIN (Vehicle Identification Number / chassi): 17 characters, excluding + * the letters `I`, `O` and `Q` (dropped to avoid confusion with `1` and `0`), with a check + * digit at the 9th position. Resolução CONTRAN nº 27/1998 requires the same transliteration + * table and weighted MOD 11 check digit algorithm used across the Americas (SAE J853 / NHTSA + * 49 CFR 565.15) for vehicles manufactured in or imported into Brazil. + * @see Official: https://www.iso.org/standard/52200.html ISO 3779:2009 (VIN content and structure) + * @see Based on: https://vpic.nhtsa.dot.gov/api/ NHTSA vPIC VIN decoding API and WMI table, used + * as a reference for the transliteration/weights across the Americas. + */ +export const VIN_LENGTH = 17; + +export const VIN_CHECK_DIGIT_POSITION = 8; + +export const VIN_TRANSLITERATION: Record = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + A: 1, + B: 2, + C: 3, + D: 4, + E: 5, + F: 6, + G: 7, + H: 8, + J: 1, + K: 2, + L: 3, + M: 4, + N: 5, + P: 7, + R: 9, + S: 2, + T: 3, + U: 4, + V: 5, + W: 6, + X: 7, + Y: 8, + Z: 9, +}; + +export const VIN_WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]; diff --git a/src/is-valid-vin/is-valid-vin.test.ts b/src/is-valid-vin/is-valid-vin.test.ts new file mode 100644 index 00000000..b6c5eeed --- /dev/null +++ b/src/is-valid-vin/is-valid-vin.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, test } from "../_internals/test/runtime"; +import { isValidVin } from "./is-valid-vin"; + +describe("isValidVin", () => { + describe("should return true", () => { + test("for a known valid VIN with a numeric check digit", () => { + expect(isValidVin("1HGCM82633A004352")).toBe(true); + }); + + test("for a known valid VIN with an X check digit", () => { + expect(isValidVin("1M8GDM9AXKP042788")).toBe(true); + }); + + test("for a second known valid VIN", () => { + expect(isValidVin("JH4TB2H26CC000000")).toBe(true); + }); + + test("for a lowercase value", () => { + expect(isValidVin("1m8gdm9axkp042788")).toBe(true); + }); + + test("for a value with leading/trailing whitespace", () => { + expect(isValidVin(" 1HGCM82633A004352 ")).toBe(true); + }); + }); + + describe("should return false", () => { + test("when the check digit does not match", () => { + expect(isValidVin("1HGCM82633A004353")).toBe(false); + }); + + test("when it contains the excluded letter I", () => { + expect(isValidVin("1HGCM8263IA004352")).toBe(false); + }); + + test("when it contains the excluded letter O", () => { + expect(isValidVin("1HGCM8263OA004352")).toBe(false); + }); + + test("when it contains the excluded letter Q", () => { + expect(isValidVin("1HGCM8263QA004352")).toBe(false); + }); + + test("when it has fewer than 17 characters", () => { + expect(isValidVin("1HGCM82633A00435")).toBe(false); + }); + + test("when it has more than 17 characters", () => { + expect(isValidVin("1HGCM82633A0043522")).toBe(false); + }); + + test("when the check digit character is a letter other than X", () => { + expect(isValidVin("1HGCM826C3A004352")).toBe(false); + }); + + test("when it contains a symbol", () => { + expect(isValidVin("1HGCM82633A00435-")).toBe(false); + }); + + test("when it is an empty string", () => { + expect(isValidVin("")).toBe(false); + }); + + test("when it is only whitespace", () => { + expect(isValidVin(" ")).toBe(false); + }); + + test("when it is null", () => { + // @ts-expect-error + expect(isValidVin(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error + expect(isValidVin(undefined)).toBe(false); + }); + + test("when it is a number", () => { + // @ts-expect-error + expect(isValidVin(12345678901234)).toBe(false); + }); + + test("when it is a boolean", () => { + // @ts-expect-error + expect(isValidVin(true)).toBe(false); + // @ts-expect-error + expect(isValidVin(false)).toBe(false); + }); + + test("when it is an object", () => { + // @ts-expect-error + expect(isValidVin({})).toBe(false); + }); + + test("when it is an array", () => { + // @ts-expect-error + expect(isValidVin([])).toBe(false); + }); + }); +}); diff --git a/src/is-valid-vin/is-valid-vin.ts b/src/is-valid-vin/is-valid-vin.ts new file mode 100644 index 00000000..deb3ea78 --- /dev/null +++ b/src/is-valid-vin/is-valid-vin.ts @@ -0,0 +1,60 @@ +import { generateChecksum } from "../_internals/generate-checksum/generate-checksum"; +import { + VIN_CHECK_DIGIT_POSITION, + VIN_LENGTH, + VIN_TRANSLITERATION, + VIN_WEIGHTS, +} from "./constants"; + +const CHECK_DIGIT_REGEX = /^[0-9X]$/; + +/** + * Validates a VIN (Vehicle Identification Number / chassi) under ISO 3779. + * + * Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid) and + * the check digit at the 9th position, calculated with the ISO 3779 transliteration table and + * a weighted MOD 11 sum, mandatory for vehicles manufactured in or imported into Brazil under + * Resolução CONTRAN nº 27/1998. Case-insensitive and trims surrounding whitespace. + * + * @param {string} value - The VIN to be validated. + * @returns {boolean} True when `value` is a 17 character VIN with a matching check digit. + * + * @example + * ```typescript + * isValidVin("1HGCM82633A004352"); // true + * isValidVin("1m8gdm9axkp042788"); // true (check digit X, lowercase) + * isValidVin("JH4TB2H26CC000000"); // true + * isValidVin("1HGCM82633A004353"); // false (bad check digit) + * isValidVin("1HGCM8263IA004352"); // false (contains the excluded letter I) + * isValidVin("1HGCM82633A00435"); // false (16 characters) + * ``` + * + * @see Official: https://www.iso.org/standard/52200.html ISO 3779:2009 (VIN content and structure) + * @see Based on: https://vpic.nhtsa.dot.gov/api/ NHTSA vPIC VIN decoding API and WMI table. + */ +export const isValidVin = (value: string): boolean => { + if (typeof value !== "string" || value === "") return false; + + const vin = value.trim().toUpperCase(); + + if (vin.length !== VIN_LENGTH) return false; + + let translitDigits = ""; + + for (let i = 0; i < VIN_LENGTH; i++) { + const char = vin[i]; + + if (!(char in VIN_TRANSLITERATION)) return false; + + translitDigits += VIN_TRANSLITERATION[char]; + } + + const checkDigit = vin[VIN_CHECK_DIGIT_POSITION]; + + if (!CHECK_DIGIT_REGEX.test(checkDigit)) return false; + + const remainder = generateChecksum({ base: translitDigits, weight: [...VIN_WEIGHTS] }) % 11; + const expected = remainder === 10 ? "X" : String(remainder); + + return expected === checkDigit; +}; From 4924b86d8417ef8bf18c2f5a25761913e1d6f691 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 16/22] feat(cbo): add getCbo and isValidCbo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6-digit CBO (Classificação Brasileira de Ocupações, MTE) lookup/validation against a table with no check digit, generated by scripts/cbo.ts. --- scripts/cbo.ts | 67 + src/_internals/constants/cbo.ts | 2577 +++++++++++++++++++++++++ src/get-cbo/get-cbo.test.ts | 55 + src/get-cbo/get-cbo.ts | 41 + src/is-valid-cbo/is-valid-cbo.test.ts | 48 + src/is-valid-cbo/is-valid-cbo.ts | 31 + 6 files changed, 2819 insertions(+) create mode 100644 scripts/cbo.ts create mode 100644 src/_internals/constants/cbo.ts create mode 100644 src/get-cbo/get-cbo.test.ts create mode 100644 src/get-cbo/get-cbo.ts create mode 100644 src/is-valid-cbo/is-valid-cbo.test.ts create mode 100644 src/is-valid-cbo/is-valid-cbo.ts diff --git a/scripts/cbo.ts b/scripts/cbo.ts new file mode 100644 index 00000000..3925d7c2 --- /dev/null +++ b/scripts/cbo.ts @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +import { writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; + +const scriptsDir = dirname(fileURLToPath(import.meta.url)); + +type CboEntry = { + cbo: string; + descricao: string; +}; + +const main = async () => { + const response = await fetchWithRetry( + "https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json", + ); + + if (!response.ok) { + throw new Error(`CBO mirror request failed with status ${response.status}`); + } + + const json: CboEntry[] = await response.json(); + + const data: Record = {}; + + for (const entry of json) { + const code = /^\d{5}$/.test(entry.cbo) ? `0${entry.cbo}` : entry.cbo; + + if (!/^\d{6}$/.test(code)) continue; + + data[code] = entry.descricao; + } + + const sorted: Record = {}; + for (const code of Object.keys(data).sort()) { + sorted[code] = data[code]; + } + + await writeFile( + resolve(scriptsDir, "..", "./src/_internals/constants/cbo.ts"), + `/** + * CBO 2002 (Classificação Brasileira de Ocupações) titles, indexed by the raw 6 digit code. + * + * The MTE download at mtecbo.gov.br requires a browser session and cannot be fetched + * programmatically, so this table is generated from a public community mirror of the + * official table. Codes that are not purely numeric with 6 digits in the source (a small + * number of law enforcement and military ranks and a few sub-occupation codes suffixed + * with a letter) are normalized by left padding a 5 digit numeric code with a zero, or + * dropped when a letter is present, since \`Cbo.code\` only accepts 6 digits. + * + * Generated by \`node ./scripts/cbo.ts\`. Do not edit by hand. + * + * @see https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json + * @see http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf + */ +export const CBO_TITLES: Record = ${JSON.stringify(sorted)}; +`, + ); +}; + +await main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +}); diff --git a/src/_internals/constants/cbo.ts b/src/_internals/constants/cbo.ts new file mode 100644 index 00000000..70e0ad39 --- /dev/null +++ b/src/_internals/constants/cbo.ts @@ -0,0 +1,2577 @@ +/** + * CBO 2002 (Classificação Brasileira de Ocupações) titles, indexed by the raw 6 digit code. + * + * The MTE download at mtecbo.gov.br requires a browser session and cannot be fetched + * programmatically, so this table is generated from a public community mirror of the + * official table. Codes that are not purely numeric with 6 digits in the source (a small + * number of law enforcement and military ranks and a few sub-occupation codes suffixed + * with a letter) are normalized by left padding a 5 digit numeric code with a zero, or + * dropped when a letter is present, since `Cbo.code` only accepts 6 digits. + * + * Generated by `node ./scripts/cbo.ts`. Do not edit by hand. + * + * @see https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json + * @see http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf + */ +export const CBO_TITLES: Record = { + "111105": "Senador", + "111110": "Deputado Federal", + "111115": "Deputado Estadual e distrital", + "111120": "Vereador", + "111205": "Presidente da República", + "111210": "Vice-Presidente da República", + "111215": "Ministro de estado", + "111220": "Secretário-Executivo", + "111225": "Membro superior do poder executivo", + "111230": "Governador de Estado", + "111235": "Governador do Distrito Federal", + "111240": "Vice-Governador de Estado", + "111245": "Vice-Governador do Distrito Federal", + "111250": "Prefeito", + "111255": "Vice-Prefeito", + "111305": "Ministro do Supremo Tribunal Federal", + "111310": "Ministro do Superior Tribunal de Justiça", + "111315": "Ministro do Superior Tribunal Militar", + "111320": "Ministro do Superior Tribunal do Trabalho", + "111325": "Juiz de Direito", + "111330": "Juiz Federal", + "111335": "Juiz Auditor Federal - Justiça Militar", + "111340": "Juiz Auditor Estadual - Justiça Militar", + "111345": "Juiz do Trabalho", + "111405": "Dirigente do serviço público federal", + "111410": "Dirigente do serviço público estadual e distrital", + "111415": "Dirigente do serviço público municipal", + "111505": "Especialista de políticas públicas e gestão governamental - EPPGG", + "111510": "Analista de planejamento e orçamento - APO", + "113005": "Cacique", + "113010": "Líder de comunidade caiçara", + "113015": "Membro de liderança quilombola", + "114105": "Dirigente de partido político", + "114205": "Dirigentes de entidades de trabalhadores", + "114210": "Dirigentes de entidades patronais", + "114305": "Dirigente e administrador de organização religiosa", + "114405": "Dirigente e administrador de organização da sociedade civil sem fins lucrativos", + "121005": "Diretor de planejamento estratégico", + "121010": "Diretor geral de empresa e organizações (exceto de interesse público)", + "122105": "Diretor de produção e operações em empresa agropecuária", + "122110": "Diretor de produção e operações em empresa aqüícola", + "122115": "Diretor de produção e operações em empresa florestal", + "122120": "Diretor de produção e operações em empresa pesqueira", + "122205": "Diretor de produção e operações da indústria de transformação", + "122305": "Diretor de operações de obras pública e civil", + "122405": "Diretor de operações comerciais (comércio atacadista e varejista)", + "122505": "Diretor de produção e operações de alimentação", + "122510": "Diretor de produção e operações de hotel", + "122515": "Diretor de produção e operações de turismo", + "122520": "Turismólogo", + "122605": "Diretor de operações de correios", + "122610": "Diretor de operações de serviços de armazenamento", + "122615": "Diretor de operações de serviços de telecomunicações", + "122620": "Diretor de operações de serviços de transporte", + "122705": "Diretor comercial em operações de intermediação financeira", + "122710": "Diretor de produtos bancários", + "122715": "Diretor de crédito rural", + "122720": "Diretor de câmbio e comércio exterior", + "122725": "Diretor de compliance", + "122730": "Diretor de crédito (exceto crédito imobiliário)", + "122735": "Diretor de crédito imobiliário", + "122740": "Diretor de leasing", + "122745": "Diretor de mercado de capitais", + "122750": "Diretor de recuperação de créditos em operações de intermediação financeira", + "122755": "Diretor de riscos de mercado", + "123105": "Diretor administrativo", + "123110": "Diretor administrativo e financeiro", + "123115": "Diretor financeiro", + "123205": "Diretor de recursos humanos", + "123210": "Diretor de relações de trabalho", + "123305": "Diretor comercial", + "123310": "Diretor de marketing", + "123405": "Diretor de suprimentos", + "123410": "Diretor de suprimentos no serviço público", + "123605": "Diretor de serviços de informática", + "123705": "Diretor de Pesquisa e Desenvolvimento (P&D)", + "123805": "Diretor de manutenção", + "131105": "Diretor de serviços culturais", + "131110": "Diretor de serviços sociais", + "131115": "Gerente de serviços culturais", + "131120": "Gerente de serviços sociais", + "131205": "Diretor de serviços de saúde", + "131210": "Gerente de serviços de saúde", + "131215": "Tecnólogo em gestão hospitalar", + "131305": "Diretor de instituição educacional da área privada", + "131310": "Diretor de instituição educacional pública", + "131315": "Gerente de instituição educacional da área privada", + "131320": "Gerente de serviços educacionais da área pública", + "141105": "Gerente de produção e operações aqüícolas", + "141110": "Gerente de produção e operações florestais", + "141115": "Gerente de produção e operações agropecuárias", + "141120": "Gerente de produção e operações pesqueiras", + "141205": "Gerente de produção e operações", + "141305": "Gerente de produção e operações da construção civil e obras públicas", + "141405": "Comerciante atacadista", + "141410": "Comerciante varejista", + "141415": "Gerente de loja e supermercado", + "141420": "Gerente de operações de serviços de assistência técnica", + "141505": "Gerente de hotel", + "141510": "Gerente de restaurante", + "141515": "Gerente de bar", + "141520": "Gerente de pensão", + "141525": "Gerente de turismo", + "141605": "Gerente de operações de transportes", + "141610": "Gerente de operações de correios e telecomunicações", + "141615": "Gerente de logística (armazenagem e distribuição)", + "141705": "Gerente de produtos bancários", + "141710": "Gerente de agência", + "141715": "Gerente de câmbio e comércio exterior", + "141720": "Gerente de crédito e cobrança", + "141725": "Gerente de crédito imobiliário", + "141730": "Gerente de crédito rural", + "141735": "Gerente de recuperação de crédito", + "142105": "Gerente administrativo", + "142110": "Gerente de riscos", + "142115": "Gerente financeiro", + "142120": "Tecnólogo em gestão administrativo- financeira", + "142205": "Gerente de recursos humanos", + "142210": "Gerente de departamento pessoal", + "142305": "Gerente comercial", + "142310": "Gerente de comunicação", + "142315": "Gerente de marketing", + "142320": "Gerente de vendas", + "142325": "Relações públicas", + "142330": "Analista de negócios", + "142335": "Analista de pesquisa de mercado", + "142340": "Ouvidor", + "142405": "Gerente de compras", + "142410": "Gerente de suprimentos", + "142415": "Gerente de almoxarifado", + "142505": "Gerente de rede", + "142510": "Gerente de desenvolvimento de sistemas", + "142515": "Gerente de produção de tecnologia da informação", + "142520": "Gerente de projetos de tecnologia da informação", + "142525": "Gerente de segurança de tecnologia da informação", + "142530": "Gerente de suporte técnico de tecnologia da informação", + "142535": "Tecnólogo em gestão da tecnologia da informação", + "142605": "Gerente de Pesquisa e Desenvolvimento (P&D)", + "142610": "Especialista em desenvolvimento de cigarros", + "142705": "Gerente de projetos e serviços de manutenção", + "142710": "Tecnólogo em sistemas biomédicos", + "201105": "Bioengenheiro", + "201110": "Biotecnologista", + "201115": "Geneticista", + "201205": "Pesquisador em metrologia", + "201210": "Especialista em calibrações metrológicas", + "201215": "Especialista em ensaios metrológicos", + "201220": "Especialista em instrumentação metrológica", + "201225": "Especialista em materiais de referência metrológica", + "202105": "Engenheiro mecatrônico", + "202110": "Engenheiro de controle e automação", + "202115": "Tecnólogo em mecatrônica", + "202120": "Tecnólogo em automação industrial", + "203005": "Pesquisador em biologia ambiental", + "203010": "Pesquisador em biologia animal", + "203015": "Pesquisador em biologia de microorganismos e parasitas", + "203020": "Pesquisador em biologia humana", + "203025": "Pesquisador em biologia vegetal", + "203105": "Pesquisador em ciências da computação e informática", + "203110": "Pesquisador em ciências da terra e meio ambiente", + "203115": "Pesquisador em física", + "203120": "Pesquisador em matemática", + "203125": "Pesquisador em química", + "203205": "Pesquisador de engenharia civil", + "203210": "Pesquisador de engenharia e tecnologia (outras áreas da engenharia)", + "203215": "Pesquisador de engenharia elétrica e eletrônica", + "203220": "Pesquisador de engenharia mecânica", + "203225": "Pesquisador de engenharia metalúrgica", + "203230": "Pesquisador de engenharia química", + "203305": "Pesquisador de clínica médica", + "203310": "Pesquisador de medicina básica", + "203315": "Pesquisador em medicina veterinária", + "203320": "Pesquisador em saúde coletiva", + "203405": "Pesquisador em ciências agronômicas", + "203410": "Pesquisador em ciências da pesca e aqüicultura", + "203415": "Pesquisador em ciências da zootecnia", + "203420": "Pesquisador em ciências florestais", + "203505": "Pesquisador em ciências sociais e humanas", + "203510": "Pesquisador em economia", + "203515": "Pesquisador em ciências da educação", + "203520": "Pesquisador em história", + "203525": "Pesquisador em psicologia", + "204105": "Perito criminal", + "211105": "Atuário", + "211110": "Especialista em pesquisa operacional", + "211115": "Matemático", + "211120": "Matemático aplicado", + "211205": "Estatístico", + "211210": "Estatístico (estatística aplicada)", + "211215": "Estatístico teórico", + "212205": "Engenheiro de aplicativos em computação", + "212210": "Engenheiro de equipamentos em computação", + "212215": "Engenheiros de sistemas operacionais em computação", + "212305": "Administrador de banco de dados", + "212310": "Administrador de redes", + "212315": "Administrador de sistemas operacionais", + "212320": "Administrador em segurança da informação", + "212405": "Analista de desenvolvimento de sistemas", + "212410": "Analista de redes e de comunicação de dados", + "212415": "Analista de sistemas de automação", + "212420": "Analista de suporte computacional", + "213105": "Físico", + "213110": "Físico (acústica)", + "213115": "Físico (atômica e molecular)", + "213120": "Físico (cosmologia)", + "213125": "Físico (estatística e matemática)", + "213130": "Físico (fluidos)", + "213135": "Físico (instrumentação)", + "213140": "Físico (matéria condensada)", + "213145": "Físico (materiais)", + "213150": "Físico (medicina)", + "213155": "Físico (nuclear e reatores)", + "213160": "Físico (óptica)", + "213165": "Físico (partículas e campos)", + "213170": "Físico (plasma)", + "213175": "Físico (térmica)", + "213205": "Químico", + "213210": "Químico industrial", + "213215": "Tecnólogo em processos químicos", + "213305": "Astrônomo", + "213310": "Geofísico espacial", + "213315": "Meteorologista", + "213405": "Geólogo", + "213410": "Geólogo de engenharia", + "213415": "Geofísico", + "213420": "Geoquímico", + "213425": "Hidrogeólogo", + "213430": "Paleontólogo", + "213435": "Petrógrafo", + "213440": "Oceanógrafo", + "214005": "Engenheiro ambiental", + "214010": "Tecnólogo em meio ambiente", + "214105": "Arquiteto de edificações", + "214110": "Arquiteto de interiores", + "214115": "Arquiteto de patrimônio", + "214120": "Arquiteto paisagista", + "214125": "Arquiteto urbanista", + "214130": "Urbanista", + "214205": "Engenheiro civil", + "214210": "Engenheiro civil (aeroportos)", + "214215": "Engenheiro civil (edificações)", + "214220": "Engenheiro civil (estruturas metálicas)", + "214225": "Engenheiro civil (ferrovias e metrovias)", + "214230": "Engenheiro civil (geotécnia)", + "214235": "Engenheiro civil (hidrologia)", + "214240": "Engenheiro civil (hidráulica)", + "214245": "Engenheiro civil (pontes e viadutos)", + "214250": "Engenheiro civil (portos e vias navegáveis)", + "214255": "Engenheiro civil (rodovias)", + "214260": "Engenheiro civil (saneamento)", + "214265": "Engenheiro civil (túneis)", + "214270": "Engenheiro civil (transportes e trânsito)", + "214280": "Tecnólogo em construção civil", + "214305": "Engenheiro eletricista", + "214310": "Engenheiro eletrônico", + "214315": "Engenheiro eletricista de manutenção", + "214320": "Engenheiro eletricista de projetos", + "214325": "Engenheiro eletrônico de manutenção", + "214330": "Engenheiro eletrônico de projetos", + "214335": "Engenheiro de manutenção de telecomunicações", + "214340": "Engenheiro de telecomunicações", + "214345": "Engenheiro projetista de telecomunicações", + "214350": "Engenheiro de redes de comunicação", + "214360": "Tecnólogo em eletricidade", + "214365": "Tecnólogo em eletrônica", + "214370": "Tecnólogo em telecomunicações", + "214405": "Engenheiro mecânico", + "214410": "Engenheiro mecânico automotivo", + "214415": "Engenheiro mecânico (energia nuclear)", + "214420": "Engenheiro mecânico industrial", + "214425": "Engenheiro aeronáutico", + "214430": "Engenheiro naval", + "214435": "Tecnólogo em fabricação mecânica", + "214505": "Engenheiro químico", + "214510": "Engenheiro químico (indústria química)", + "214515": "Engenheiro químico (mineração", + "214520": "Engenheiro químico (papel e celulose)", + "214525": "Engenheiro químico (petróleo e borracha)", + "214530": "Engenheiro químico (utilidades e meio ambiente)", + "214535": "Tecnólogo em produção sulcroalcooleira", + "214605": "Engenheiro de materiais", + "214610": "Engenheiro metalurgista", + "214615": "Tecnólogo em metalurgia", + "214705": "Engenheiro de minas", + "214710": "Engenheiro de minas (beneficiamento)", + "214715": "Engenheiro de minas (lavra a céu aberto)", + "214720": "Engenheiro de minas (lavra subterrânea)", + "214725": "Engenheiro de minas (pesquisa mineral)", + "214730": "Engenheiro de minas (planejamento)", + "214735": "Engenheiro de minas (processo)", + "214740": "Engenheiro de minas (projeto)", + "214745": "Tecnólogo em petróleo e gás", + "214750": "Tecnólogo em rochas ornamentais", + "214805": "Engenheiro agrimensor", + "214810": "Engenheiro cartógrafo", + "214905": "Engenheiro de produção", + "214910": "Engenheiro de controle de qualidade", + "214915": "Engenheiro de segurança do trabalho", + "214920": "Engenheiro de riscos", + "214925": "Engenheiro de tempos e movimentos", + "214930": "Tecnólogo em produção industrial", + "214935": "Tecnólogo em segurança do trabalho", + "215105": "Agente de manobra e docagem", + "215110": "Capitão de manobra da Marinha Mercante", + "215115": "Comandante da Marinha Mercante", + "215120": "Coordenador de operações de combate à poluição no meio aquaviário", + "215125": "Imediato da Marinha Mercante", + "215130": "Inspetor de terminal", + "215135": "Inspetor naval", + "215140": "Oficial de quarto de navegação da Marinha Mercante", + "215145": "Prático de portos da Marinha Mercante", + "215150": "Vistoriador naval", + "215205": "Oficial superior de máquinas da Marinha Mercante", + "215210": "Primeiro oficial de máquinas da Marinha Mercante", + "215215": "Segundo oficial de máquinas da Marinha Mercante", + "215220": "Superintendente técnico no transporte aquaviário", + "215305": "Piloto de aeronaves", + "215310": "Piloto de ensaios em vôo", + "215315": "Instrutor de vôo", + "221105": "Biólogo", + "221205": "Biomédico", + "222105": "Engenheiro agrícola", + "222110": "Engenheiro agrônomo", + "222115": "Engenheiro de pesca", + "222120": "Engenheiro florestal", + "222205": "Engenheiro de alimentos", + "222215": "Tecnólogo em alimentos", + "223119": "Médico em eletroencefalografia", + "223150": "Médico perito", + "223204": "Cirurgião dentista - auditor", + "223208": "Cirurgião dentista - clínico geral", + "223212": "Cirurgião dentista - endodontista", + "223216": "Cirurgião dentista - epidemiologista", + "223220": "Cirurgião dentista - estomatologista", + "223224": "Cirurgião dentista - implantodontista", + "223228": "Cirurgião dentista - odontogeriatra", + "223232": "Cirurgião dentista - odontologista legal", + "223236": "Cirurgião dentista - odontopediatra", + "223240": "Cirurgião dentista - ortopedista e ortodontista", + "223244": "Cirurgião dentista - patologista bucal", + "223248": "Cirurgião dentista - periodontista", + "223252": "Cirurgião dentista - protesiólogo bucomaxilofacial", + "223256": "Cirurgião dentista - protesista", + "223260": "Cirurgião dentista - radiologista", + "223264": "Cirurgião dentista - reabilitador oral", + "223268": "Cirurgião dentista - traumatologista bucomaxilofacial", + "223272": "Cirurgião dentista de saúde coletiva", + "223276": "Cirurgião dentista - odontologia do trabalho", + "223280": "Cirurgião dentista - dentística", + "223284": "Cirurgião dentista - disfunção temporomandibular e dor orofacial", + "223288": "Cirurgião dentista - odontologia para pacientes com necessidades especiais", + "223293": "Cirurgião-dentista da estratégia de saúde da família", + "223305": "Médico veterinário", + "223310": "Zootecnista", + "223405": "Farmacêutico", + "223415": "Farmacêutico analista clínico", + "223420": "Farmacêutico de alimentos", + "223425": "Farmacêutico práticas integrativas e complementares", + "223430": "Farmacêutico em saúde pública", + "223435": "Farmacêutico industrial", + "223440": "Farmacêutico toxicologista", + "223445": "Farmacêutico hospitalar e clínico", + "223505": "Enfermeiro", + "223510": "Enfermeiro auditor", + "223515": "Enfermeiro de bordo", + "223520": "Enfermeiro de centro cirúrgico", + "223525": "Enfermeiro de terapia intensiva", + "223530": "Enfermeiro do trabalho", + "223535": "Enfermeiro nefrologista", + "223540": "Enfermeiro neonatologista", + "223545": "Enfermeiro obstétrico", + "223550": "Enfermeiro psiquiátrico", + "223555": "Enfermeiro puericultor e pediátrico", + "223560": "Enfermeiro sanitarista", + "223565": "Enfermeiro da estratégia de saúde da família", + "223570": "Perfusionista", + "223605": "Fisioterapeuta geral", + "223625": "Fisioterapeuta respiratória", + "223630": "Fisioterapeuta neurofuncional", + "223635": "Fisioterapeuta traumato-ortopédica funcional", + "223640": "Fisioterapeuta osteopata", + "223645": "Fisioterapeuta quiropraxista", + "223650": "Fisioterapeuta acupunturista", + "223655": "Fisioterapeuta esportivo", + "223660": "Fisioterapeuta do trabalho", + "223705": "Dietista", + "223710": "Nutricionista", + "223810": "Fonoaudiólogo", + "223815": "Fonoaudiólogo educacional", + "223820": "Fonoaudiólogo em audiologia", + "223825": "Fonoaudiólogo em disfagia", + "223830": "Fonoaudiólogo em linguagem", + "223835": "Fonoaudiólogo em motricidade orofacial", + "223840": "Fonoaudiólogo em saúde coletiva", + "223845": "Fonoaudiólogo em voz", + "223905": "Terapeuta ocupacional", + "223910": "Ortoptista", + "224105": "Avaliador físico", + "224110": "Ludomotricista", + "224115": "Preparador de atleta", + "224120": "Preparador físico", + "224125": "Técnico de desporto individual e coletivo (exceto futebol)", + "224130": "Técnico de laboratório e fiscalização desportiva", + "224135": "Treinador profissional de futebol", + "225103": "Médico infectologista", + "225105": "Médico acupunturista", + "225106": "Médico legista", + "225109": "Médico nefrologista", + "225110": "Médico alergista e imunologista", + "225112": "Médico neurologista", + "225115": "Médico angiologista", + "225118": "Médico nutrologista", + "225120": "Médico cardiologista", + "225121": "Médico oncologista clínico", + "225122": "Médico cancerologista pediátrico", + "225124": "Médico pediatra", + "225125": "Médico clínico", + "225127": "Médico pneumologista", + "225130": "Médico de família e comunidade", + "225133": "Médico psiquiatra", + "225135": "Médico dermatologista", + "225136": "Médico reumatologista", + "225139": "Médico sanitarista", + "225140": "Médico do trabalho", + "225142": "Médico da estratégia de saúde da família", + "225145": "Médico em medicina de tráfego", + "225148": "Médico anatomopatologista", + "225150": "Médico em medicina intensiva", + "225151": "Médico anestesiologista", + "225155": "Médico endocrinologista e metabologista", + "225160": "Médico fisiatra", + "225165": "Médico gastroenterologista", + "225170": "Médico generalista", + "225175": "Médico geneticista", + "225180": "Médico geriatra", + "225185": "Médico hematologista", + "225195": "Médico homeopata", + "225203": "Médico em cirurgia vascular", + "225210": "Médico cirurgião cardiovascular", + "225215": "Médico cirurgião de cabeça e pescoço", + "225220": "Médico cirurgião do aparelho digestivo", + "225225": "Médico cirurgião geral", + "225230": "Médico cirurgião pediátrico", + "225235": "Médico cirurgião plástico", + "225240": "Médico cirurgião torácico", + "225250": "Médico ginecologista e obstetra", + "225255": "Médico mastologista", + "225260": "Médico neurocirurgião", + "225265": "Médico oftalmologista", + "225270": "Médico ortopedista e traumatologista", + "225275": "Médico otorrinolaringologista", + "225280": "Médico coloproctologista", + "225285": "Médico urologista", + "225290": "Médico cancerologista cirurgíco", + "225295": "Médico cirurgião da mão", + "225305": "Médico citopatologista", + "225310": "Médico em endoscopia", + "225315": "Médico em medicina nuclear", + "225320": "Médico em radiologia e diagnóstico por imagem", + "225325": "Médico patologista", + "225330": "Médico radioterapeuta", + "225335": "Médico patologista clínico / medicina laboratorial", + "225340": "Médico hemoterapeuta", + "225345": "Médico hiperbarista", + "225350": "Médico neurofisiologista clínico", + "226105": "Quiropraxista", + "226110": "Osteopata", + "226305": "Musicoterapeuta", + "226310": "Arteterapeuta", + "226315": "Equoterapeuta", + "231105": "Professor de nível superior na educação infantil (quatro a seis anos)", + "231110": "Professor de nível superior na educação infantil (zero a três anos)", + "231205": + "Professor da educação de jovens e adultos do ensino fundamental (primeira a quarta série)", + "231210": "Professor de nível superior do ensino fundamental (primeira a quarta série)", + "231305": "Professor de ciências exatas e naturais do ensino fundamental", + "231310": "Professor de educação artística do ensino fundamental", + "231315": "Professor de educação física do ensino fundamental", + "231320": "Professor de geografia do ensino fundamental", + "231325": "Professor de história do ensino fundamental", + "231330": "Professor de língua estrangeira moderna do ensino fundamental", + "231335": "Professor de língua portuguesa do ensino fundamental", + "231340": "Professor de matemática do ensino fundamental", + "232105": "Professor de artes no ensino médio", + "232110": "Professor de biologia no ensino médio", + "232115": "Professor de disciplinas pedagógicas no ensino médio", + "232120": "Professor de educação física no ensino médio", + "232125": "Professor de filosofia no ensino médio", + "232130": "Professor de física no ensino médio", + "232135": "Professor de geografia no ensino médio", + "232140": "Professor de história no ensino médio", + "232145": "Professor de língua e literatura brasileira no ensino médio", + "232150": "Professor de língua estrangeira moderna no ensino médio", + "232155": "Professor de matemática no ensino médio", + "232160": "Professor de psicologia no ensino médio", + "232165": "Professor de química no ensino médio", + "232170": "Professor de sociologia no ensino médio", + "233105": "Professor da área de meio ambiente", + "233110": "Professor de desenho técnico", + "233115": "Professor de técnicas agrícolas", + "233120": "Professor de técnicas comerciais e secretariais", + "233125": "Professor de técnicas de enfermagem", + "233130": "Professor de técnicas industriais", + "233135": "Professor de tecnologia e cálculo técnico", + "233205": "Instrutor de aprendizagem e treinamento agropecuário", + "233210": "Instrutor de aprendizagem e treinamento industrial", + "233215": "Professor de aprendizagem e treinamento comercial", + "233220": "Professor instrutor de ensino e aprendizagem agroflorestal", + "233225": "Professor instrutor de ensino e aprendizagem em serviços", + "234105": "Professor de matemática aplicada (no ensino superior)", + "234110": "Professor de matemática pura (no ensino superior)", + "234115": "Professor de estatística (no ensino superior)", + "234120": "Professor de computação (no ensino superior)", + "234125": "Professor de pesquisa operacional (no ensino superior)", + "234205": "Professor de física (ensino superior)", + "234210": "Professor de química (ensino superior)", + "234215": "Professor de astronomia (ensino superior)", + "234305": "Professor de arquitetura", + "234310": "Professor de engenharia", + "234315": "Professor de geofísica", + "234320": "Professor de geologia", + "234405": "Professor de ciências biológicas do ensino superior", + "234410": "Professor de educação física no ensino superior", + "234415": "Professor de enfermagem do ensino superior", + "234420": "Professor de farmácia e bioquímica", + "234425": "Professor de fisioterapia", + "234430": "Professor de fonoaudiologia", + "234435": "Professor de medicina", + "234440": "Professor de medicina veterinária", + "234445": "Professor de nutrição", + "234450": "Professor de odontologia", + "234455": "Professor de terapia ocupacional", + "234460": "Professor de zootecnia do ensino superior", + "234505": "Professor de ensino superior na área de didática", + "234510": "Professor de ensino superior na área de orientação educacional", + "234515": "Professor de ensino superior na área de pesquisa educacional", + "234520": "Professor de ensino superior na área de prática de ensino", + "234604": "Professor de língua alema", + "234608": "Professor de língua italiana", + "234612": "Professor de língua francesa", + "234616": "Professor de língua inglesa", + "234620": "Professor de língua espanhola", + "234624": "Professor de língua portuguesa", + "234628": "Professor de literatura brasileira", + "234632": "Professor de literatura portuguesa", + "234636": "Professor de literatura alema", + "234640": "Professor de literatura comparada", + "234644": "Professor de literatura espanhola", + "234648": "Professor de literatura francesa", + "234652": "Professor de literatura inglesa", + "234656": "Professor de literatura italiana", + "234660": "Professor de literatura de línguas estrangeiras modernas", + "234664": "Professor de outras línguas e literaturas", + "234668": "Professor de línguas estrangeiras modernas", + "234672": "Professor de lingüística e lingüística aplicada", + "234676": "Professor de filologia e crítica textual", + "234680": "Professor de semiótica", + "234684": "Professor de teoria da literatura", + "234705": "Professor de antropologia do ensino superior", + "234710": "Professor de arquivologia do ensino superior", + "234715": "Professor de biblioteconomia do ensio superior", + "234720": "Professor de ciência política do ensino superior", + "234725": "Professor de comunicação social do ensino superior", + "234730": "Professor de direito do ensino superior", + "234735": "Professor de filosofia do ensino superior", + "234740": "Professor de geografia do ensino superior", + "234745": "Professor de história do ensino superior", + "234750": "Professor de jornalismo", + "234755": "Professor de museologia do ensino superior", + "234760": "Professor de psicologia do ensino superior", + "234765": "Professor de serviço social do ensino superior", + "234770": "Professor de sociologia do ensino superior", + "234805": "Professor de economia", + "234810": "Professor de administração", + "234815": "Professor de contabilidade", + "234905": "Professor de artes do espetáculo no ensino superior", + "234910": "Professor de artes visuais no ensino superior (artes plásticas e multimídia)", + "234915": "Professor de música no ensino superior", + "239205": "Professor de alunos com deficiência auditiva e surdos", + "239210": "Professor de alunos com deficiência física", + "239215": "Professor de alunos com deficiência mental", + "239220": "Professor de alunos com deficiência múltipla", + "239225": "Professor de alunos com deficiência visual", + "239405": "Coordenador pedagógico", + "239410": "Orientador educacional", + "239415": "Pedagogo", + "239420": "Professor de técnicas e recursos audiovisuais", + "239425": "Psicopedagogo", + "239430": "Supervisor de ensino", + "239435": "Designer educacional", + "241005": "Advogado", + "241010": "Advogado de empresa", + "241015": "Advogado (direito civil)", + "241020": "Advogado (direito público)", + "241025": "Advogado (direito penal)", + "241030": "Advogado (áreas especiais)", + "241035": "Advogado (direito do trabalho)", + "241040": "Consultor jurídico", + "241205": "Advogado da união", + "241210": "Procurador autárquico", + "241215": "Procurador da fazenda nacional", + "241220": "Procurador do estado", + "241225": "Procurador do município", + "241230": "Procurador Federal", + "241235": "Procurador fundacional", + "241305": "Oficial de registro de contratos marítimos", + "241310": "Oficial do registro civil de pessoas juridicas", + "241315": "Oficial do registro civil de pessoas naturais", + "241320": "Oficial do registro de distribuições", + "241325": "Oficial do registro de imóveis", + "241330": "Oficial do registro de títulos e documentos", + "241335": "Tabelião de notas", + "241340": "Tabelião de protestos", + "242205": "Procurador da república", + "242210": "Procurador de justiça", + "242215": "Procurador de justiça militar", + "242220": "Procurador do trabalho", + "242225": "Procurador regional da república", + "242230": "Procurador regional do trabalho", + "242235": "Promotor de justiça", + "242240": "Subprocurador de justiça militar", + "242245": "Subprocurador-geral da república", + "242250": "Subprocurador-geral do trabalho", + "242305": "Delegado de polícia", + "242405": "Defensor público", + "242410": "Procurador da assistência judiciária", + "242905": "Oficial de inteligência", + "242910": "Oficial técnico de inteligência", + "251105": "Antropólogo", + "251110": "Arqueólogo", + "251115": "Cientista político", + "251120": "Sociólogo", + "251205": "Economista", + "251210": "Economista agroindustrial", + "251215": "Economista financeiro", + "251220": "Economista industrial", + "251225": "Economista do setor público", + "251230": "Economista ambiental", + "251235": "Economista regional e urbano", + "251305": "Geógrafo", + "251405": "Filósofo", + "251505": "Psicólogo educacional", + "251510": "Psicólogo clínico", + "251515": "Psicólogo do esporte", + "251520": "Psicólogo hospitalar", + "251525": "Psicólogo jurídico", + "251530": "Psicólogo social", + "251535": "Psicólogo do trânsito", + "251540": "Psicólogo do trabalho", + "251545": "Neuropsicólogo", + "251550": "Psicanalista", + "251555": "Psicólogo Acupunturista", + "251605": "Assistente social", + "251610": "Economista doméstico", + "252105": "Administrador", + "252205": "Auditor (contadores e afins)", + "252210": "Contador", + "252215": "Perito contábil", + "252305": "Secretária executiva", + "252310": "Secretário bilíngüe", + "252315": "Secretária trilíngüe", + "252320": "Tecnólogo em secretariado escolar", + "252405": "Analista de recursos humanos", + "252505": "Administrador de fundos e carteiras de investimento", + "252510": "Analista de câmbio", + "252515": "Analista de cobrança (instituições financeiras)", + "252525": "Analista de crédito (instituições financeiras)", + "252530": "Analista de crédito rural", + "252535": "Analista de leasing", + "252540": "Analista de produtos bancários", + "252545": "Analista financeiro (instituições financeiras)", + "252605": "Gestor em segurança", + "253110": "Redator de publicidade", + "253115": "Agente publicitário", + "253205": "Gerente de captação (fundos e investimentos institucionais)", + "253210": "Gerente de clientes especiais (private)", + "253215": "Gerente de contas - pessoa física e jurídica", + "253220": "Gerente de grandes contas (corporate)", + "253225": "Operador de negócios", + "253305": "Corretor de valores", + "254105": "Auditor-fiscal da Receita Federal", + "254110": "Técnico da Receita Federal", + "254205": "Auditor-fiscal da previdência social", + "254305": "Auditor-fiscal do trabalho", + "254310": "Agente de higiene e segurança", + "254405": "Fiscal de tributos estadual", + "254410": "Fiscal de tributos municipal", + "254415": "Técnico de tributos estadual", + "254420": "Técnico de tributos municipal", + "261105": "Arquivista pesquisador (jornalismo)", + "261110": "Assessor de imprensa", + "261115": "Diretor de redação", + "261120": "Editor", + "261125": "Jornalista", + "261130": "Produtor de texto", + "261135": "Repórter (exclusive rádio e televisão)", + "261140": "Revisor de texto", + "261205": "Bibliotecário", + "261210": "Documentalista", + "261215": "Analista de informações (pesquisador de informações de rede)", + "261305": "Arquivista", + "261310": "Museólogo", + "261405": "Filólogo", + "261410": "Intérprete", + "261415": "Lingüista", + "261420": "Tradutor", + "261425": "Intérprete de língua de sinais", + "261505": "Autor-roteirista", + "261510": "Crítico", + "261515": "Escritor de ficção", + "261520": "Escritor de não ficção", + "261525": "Poeta", + "261530": "Redator de textos técnicos", + "261605": "Editor de jornal", + "261610": "Editor de livro", + "261615": "Editor de mídia eletrônica", + "261620": "Editor de revista", + "261625": "Editor de revista científica", + "261705": "Ancora de rádio e televisão", + "261710": "Comentarista de rádio e televisão", + "261715": "Locutor de rádio e televisão", + "261720": "Locutor publicitário de rádio e televisão", + "261725": "Narrador em programas de rádio e televisão", + "261730": "Repórter de rádio e televisão", + "261805": "Fotógrafo", + "261810": "Fotógrafo publicitário", + "261815": "Fotógrafo retratista", + "261820": "Repóter fotográfico", + "262105": "Empresário de espetáculo", + "262110": "Produtor cinematográfico", + "262115": "Produtor de rádio", + "262120": "Produtor de teatro", + "262125": "Produtor de televisão", + "262130": "Tecnólogo em produção fonográfica", + "262135": "Tecnólogo em produção audiovisual", + "262205": "Diretor de cinema", + "262210": "Diretor de programas de rádio", + "262215": "Diretor de programas de televisão", + "262220": "Diretor teatral", + "262305": "Cenógrafo carnavalesco e festas populares", + "262310": "Cenógrafo de cinema", + "262315": "Cenógrafo de eventos", + "262320": "Cenógrafo de teatro", + "262325": "Cenógrafo de TV", + "262330": "Diretor de arte", + "262405": "Artista (artes visuais)", + "262410": "Desenhista industrial (designer)", + "262415": "Conservador-restaurador de bens culturais", + "262420": "Desenhista industrial de produto (designer de produto)", + "262425": "Desenhista industrial de produto de moda (designer de moda)", + "262505": "Ator", + "262605": "Compositor", + "262610": "Músico arranjador", + "262615": "Músico regente", + "262620": "Musicólogo", + "262705": "Músico intérprete cantor", + "262710": "Músico intérprete instrumentista", + "262805": "Assistente de coreografia", + "262810": "Bailarino (exceto danças populares)", + "262815": "Coreógrafo", + "262820": "Dramaturgo de dança", + "262825": "Ensaiador de dança", + "262830": "Professor de dança", + "262905": "Decorador de interiores de nível superior", + "263105": "Ministro de culto religioso", + "263110": "Missionário", + "263115": "Teólogo", + "271105": "Chefe de cozinha", + "271110": "Tecnólogo em gastronomia", + "300105": "Técnico em mecatrônica - automação da manufatura", + "300110": "Técnico em mecatrônica - robótica", + "300305": "Técnico em eletromecânica", + "301105": "Técnico de laboratório industrial", + "301110": "Técnico de laboratório de análises físico-químicas (materiais de construção)", + "301115": "Técnico químico de petróleo", + "301205": "Técnico de apoio à bioengenharia", + "311105": "Técnico químico", + "311110": "Técnico de celulose e papel", + "311115": "Técnico em curtimento", + "311205": "Técnico em petroquímica", + "311305": "Técnico em materiais", + "311405": "Técnico em borracha", + "311410": "Técnico em plástico", + "311505": "Técnico de controle de meio ambiente", + "311510": "Técnico de meteorologia", + "311515": "Técnico de utilidade (produção e distribuição de vapor", + "311520": "Técnico em tratamento de efluentes", + "311605": "Técnico têxtil", + "311610": "Técnico têxtil (tratamentos químicos)", + "311615": "Técnico têxtil de fiação", + "311620": "Técnico têxtil de malharia", + "311625": "Técnico têxtil de tecelagem", + "311705": "Colorista de papel", + "311710": "Colorista têxtil", + "311715": "Preparador de tintas", + "311720": "Preparador de tintas (fábrica de tecidos)", + "311725": "Tingidor de couros e peles", + "312105": "Técnico de obras civis", + "312205": "Técnico de estradas", + "312210": "Técnico de saneamento", + "312305": "Técnico em agrimensura", + "312310": "Técnico em geodésia e cartografia", + "312315": "Técnico em hidrografia", + "312320": "Topógrafo", + "313105": "Eletrotécnico", + "313110": "Eletrotécnico (produção de energia)", + "313115": "Eletroténico na fabricação", + "313120": "Técnico de manutenção elétrica", + "313125": "Técnico de manutenção elétrica de máquina", + "313130": "Técnico eletricista", + "313205": "Técnico de manutenção eletrônica", + "313210": "Técnico de manutenção eletrônica (circuitos de máquinas com comando numérico)", + "313215": "Técnico eletrônico", + "313220": "Técnico em manutenção de equipamentos de informática", + "313305": "Técnico de comunicação de dados", + "313310": "Técnico de rede (telecomunicações)", + "313315": "Técnico de telecomunicações (telefonia)", + "313320": "Técnico de transmissão (telecomunicações)", + "313405": "Técnico em calibração", + "313410": "Técnico em instrumentação", + "313415": "Encarregado de manutenção de instrumentos de controle", + "313505": "Técnico em fotônica", + "314105": "Técnico em mecânica de precisão", + "314110": "Técnico mecânico", + "314115": "Técnico mecânico (calefação", + "314120": "Técnico mecânico (máquinas)", + "314125": "Técnico mecânico (motores)", + "314205": "Técnico mecânico na fabricação de ferramentas", + "314210": "Técnico mecânico na manutenção de ferramentas", + "314305": "Técnico em automobilística", + "314310": "Técnico mecânico (aeronaves)", + "314315": "Técnico mecânico (embarcações)", + "314405": "Técnico de manutenção de sistemas e instrumentos", + "314410": "Técnico em manutenção de máquinas", + "314605": "Inspetor de soldagem", + "314610": "Técnico em caldeiraria", + "314615": "Técnico em estruturas metálicas", + "314620": "Técnico em soldagem", + "314705": "Técnico de acabamento em siderurgia", + "314710": "Técnico de aciaria em siderurgia", + "314715": "Técnico de fundição em siderurgia", + "314720": "Técnico de laminação em siderurgia", + "314725": "Técnico de redução na siderurgia (primeira fusão)", + "314730": "Técnico de refratário em siderurgia", + "316105": "Técnico em geofísica", + "316110": "Técnico em geologia", + "316115": "Técnico em geoquímica", + "316120": "Técnico em geotecnia", + "316305": "Técnico de mineração", + "316310": "Técnico de mineração (óleo e petróleo)", + "316315": "Técnico em processamento mineral (exceto petróleo)", + "316320": "Técnico em pesquisa mineral", + "316325": "Técnico de produção em refino de petróleo", + "316330": "Técnico em planejamento de lavra de minas", + "316335": "Desincrustador (poços de petróleo)", + "316340": "Cimentador (poços de petróleo)", + "317105": "Programador de internet", + "317110": "Programador de sistemas de informação", + "317115": "Programador de máquinas - ferramenta com comando numérico", + "317120": "Programador de multimídia", + "317205": "Operador de computador (inclusive microcomputador)", + "317210": "Técnico de apoio ao usuário de informática (helpdesk)", + "318005": "Desenhista técnico", + "318010": "Desenhista copista", + "318015": "Desenhista detalhista", + "318105": "Desenhista técnico (arquitetura)", + "318110": "Desenhista técnico (cartografia)", + "318115": "Desenhista técnico (construção civil)", + "318120": "Desenhista técnico (instalações hidrossanitárias)", + "318205": "Desenhista técnico mecânico", + "318210": "Desenhista técnico aeronáutico", + "318215": "Desenhista técnico naval", + "318305": "Desenhista técnico (eletricidade e eletrônica)", + "318310": "Desenhista técnico (calefação", + "318405": "Desenhista técnico (artes gráficas)", + "318410": "Desenhista técnico (ilustrações artísticas)", + "318415": "Desenhista técnico (ilustrações técnicas)", + "318420": "Desenhista técnico (indústria têxtil)", + "318425": "Desenhista técnico (mobiliário)", + "318430": "Desenhista técnico de embalagens", + "318505": "Desenhista projetista de arquitetura", + "318510": "Desenhista projetista de construção civil", + "318605": "Desenhista projetista de máquinas", + "318610": "Desenhista projetista mecânico", + "318705": "Desenhista projetista de eletricidade", + "318710": "Desenhista projetista eletrônico", + "318805": "Projetista de móveis", + "318810": "Modelista de roupas", + "318815": "Modelista de calçados", + "319105": "Técnico em calçados e artefatos de couro", + "319110": "Técnico em confecções do vestuário", + "319205": "Técnico do mobiliário", + "320105": "Técnico em bioterismo", + "320110": "Técnico em histologia", + "321105": "Técnico agrícola", + "321110": "Técnico agropecuário", + "321205": "Técnico em madeira", + "321210": "Técnico florestal", + "321305": "Técnico em piscicultura", + "321310": "Técnico em carcinicultura", + "321315": "Técnico em mitilicultura", + "321320": "Técnico em ranicultura", + "322105": "Técnico em acupuntura", + "322110": "Podólogo", + "322115": "Técnico em quiropraxia", + "322120": "Massoterapeuta", + "322125": "Terapeuta holístico", + "322130": "Esteticista", + "322135": "Doula", + "322205": "Técnico de enfermagem", + "322210": "Técnico de enfermagem de terapia intensiva", + "322215": "Técnico de enfermagem do trabalho", + "322220": "Técnico de enfermagem psiquiátrica", + "322225": "Instrumentador cirúrgico", + "322230": "Auxiliar de enfermagem", + "322235": "Auxiliar de enfermagem do trabalho", + "322240": "Auxiliar de saúde (navegação marítima)", + "322245": "Técnico de enfermagem da estratégia de saúde da família", + "322250": "Auxiliar de enfermagem da estratégia de saúde da família", + "322305": "Técnico em óptica e optometria", + "322405": "Técnico em saúde bucal", + "322410": "Protético dentário", + "322415": "Auxiliar em saúde bucal", + "322420": "Auxiliar de prótese dentária", + "322425": "Técnico em saúde bucal da estratégia de saúde da família", + "322430": "Auxiliar em saúde bucal da estratégia de saúde da família", + "322505": "Técnico de ortopedia", + "322605": "Técnico de imobilização ortopédica", + "323105": "Técnico em pecuária", + "324105": "Técnico em métodos eletrográficos em encefalografia", + "324110": "Técnico em métodos gráficos em cardiologia", + "324115": "Técnico em radiologia e imagenologia", + "324120": "Técnólogo em radiologia", + "324125": "Tecnólogo oftálmico", + "324205": "Técnico em patologia clínica", + "324210": "Auxiliar técnico em patologia clínica", + "325005": "Enólogo", + "325010": "Aromista", + "325015": "Perfumista", + "325105": "Auxiliar técnico em laboratório de farmácia", + "325110": "Técnico em laboratório de farmácia", + "325115": "Técnico em farmácia", + "325205": "Técnico de alimentos", + "325210": "Técnico em nutrição e dietética", + "325305": "Técnico em biotecnologia", + "325310": "Técnico em imunobiológicos", + "328105": "Embalsamador", + "328110": "Taxidermista", + "331105": "Professor de nível médio na educação infantil", + "331110": "Auxiliar de desenvolvimento infantil", + "331205": "Professor de nível médio no ensino fundamental", + "331305": "Professor de nível médio no ensino profissionalizante", + "332105": "Professor leigo no ensino fundamental", + "332205": "Professor prático no ensino profissionalizante", + "333105": "Instrutor de auto-escola", + "333110": "Instrutor de cursos livres", + "333115": "Professores de cursos livres", + "334105": "Inspetor de alunos de escola privada", + "334110": "Inspetor de alunos de escola pública", + "334115": "Monitor de transporte escolar", + "341105": "Piloto comercial (exceto linhas aéreas)", + "341110": "Piloto comercial de helicóptero (exceto linhas aéreas)", + "341115": "Mecânico de vôo", + "341120": "Piloto agrícola", + "341205": "Contramestre de cabotagem", + "341210": "Mestre de cabotagem", + "341215": "Mestre fluvial", + "341220": "Patrão de pesca de alto-mar", + "341225": "Patrão de pesca na navegação interior", + "341230": "Piloto fluvial", + "341305": "Condutor maquinista fluvial", + "341310": "Condutor maquinista marítimo", + "341315": "Eletricista de bordo", + "342105": "Analista de transporte em comércio exterior", + "342110": "Operador de transporte multimodal", + "342115": "Controlador de serviços de máquinas e veículos", + "342120": "Afretador", + "342125": "Tecnólogo em logística de transporte", + "342205": "Ajudante de despachante aduaneiro", + "342210": "Despachante aduaneiro", + "342305": "Chefe de serviço de transporte rodoviário (passageiros e cargas)", + "342310": "Inspetor de serviços de transportes rodoviários (passageiros e cargas)", + "342315": "Supervisor de carga e descarga", + "342405": "Agente de estação (ferrovia e metrô)", + "342410": "Operador de centro de controle (ferrovia e metrô)", + "342505": "Controlador de tráfego aéreo", + "342510": "Despachante operacional de vôo", + "342515": "Fiscal de aviação civil (fac)", + "342520": "Gerente da administração de aeroportos", + "342525": "Gerente de empresa aérea em aeroportos", + "342530": "Inspetor de aviação civil", + "342535": "Operador de atendimento aeroviário", + "342540": "Supervisor da administração de aeroportos", + "342545": "Supervisor de empresa aérea em aeroportos", + "342550": "Agente de proteção de aviação civil", + "342605": "Chefe de estação portuária", + "342610": "Supervisor de operações portuárias", + "351105": "Técnico de contabilidade", + "351110": "Chefe de contabilidade (técnico)", + "351115": "Consultor contábil (técnico)", + "351305": "Técnico em administração", + "351310": "Técnico em administração de comércio exterior", + "351315": "Agente de recrutamento e seleção", + "351405": "Escrevente", + "351410": "Escrivão judicial", + "351415": "Escrivão extra - judicial", + "351420": "Escrivão de polícia", + "351425": "Oficial de justiça", + "351430": "Auxiliar de serviços jurídicos", + "351505": "Técnico em secretariado", + "351510": "Taquígrafo", + "351515": "Estenotipista", + "351605": "Técnico em segurança no trabalho", + "351705": "Analista de seguros (técnico)", + "351710": "Analista de sinistros", + "351715": "Assistente comercial de seguros", + "351720": "Assistente técnico de seguros", + "351725": "Inspetor de risco", + "351730": "Inspetor de sinistros", + "351735": "Técnico de resseguros", + "351740": "Técnico de seguros", + "351805": "Detetive profissional", + "351810": "Investigador de polícia", + "351815": "Papiloscopista policial", + "351905": "Agente de inteligência", + "351910": "Agente técnico de inteligência", + "352205": "Agente de defesa ambiental", + "352210": "Agente de saúde pública", + "352305": "Metrologista", + "352310": "Agente fiscal de qualidade", + "352315": "Agente fiscal metrológico", + "352320": "Agente fiscal têxtil", + "352405": "Agente de direitos autorais", + "352410": "Avaliador de produtos do meio de comunicação", + "352420": "Técnico em direitos autorais", + "353205": "Técnico de operações e serviços bancários - câmbio", + "353210": "Técnico de operações e serviços bancários - crédito imobiliário", + "353215": "Técnico de operações e serviços bancários - crédito rural", + "353220": "Técnico de operações e serviços bancários - leasing", + "353225": "Técnico de operações e serviços bancários - renda fixa e variável", + "353230": "Tesoureiro de banco", + "353235": "Chefe de serviços bancários", + "354110": "Agenciador de propaganda", + "354120": "Agente de vendas de serviços", + "354125": "Assistente de vendas", + "354130": "Promotor de vendas especializado", + "354135": "Técnico de vendas", + "354140": "Técnico em atendimento e vendas", + "354145": "Vendedor pracista", + "354150": "Propagandista de produtos famacêuticos", + "354205": "Comprador", + "354210": "Supervisor de compras", + "354305": "Analista de exportação e importação", + "354405": "Leiloeiro", + "354410": "Avaliador de imóveis", + "354415": "Avaliador de bens móveis", + "354505": "Corretor de seguros", + "354605": "Corretor de imóveis", + "354705": "Representante comercial autônomo", + "354805": "Técnico em turismo", + "354810": "Operador de turismo", + "354815": "Agente de viagem", + "354820": "Organizador de evento", + "371105": "Auxiliar de biblioteca", + "371110": "Técnico em biblioteconomia", + "371205": "Colecionador de selos e moedas", + "371210": "Técnico em museologia", + "371305": "Técnico em programação visual", + "371310": "Técnico gráfico", + "371405": "Recreador de acantonamento", + "371410": "Recreador", + "372105": "Diretor de fotografia", + "372110": "Iluminador (televisão)", + "372115": "Operador de câmera de televisão", + "372205": "Operador de rede de teleprocessamento", + "372210": "Radiotelegrafista", + "373105": "Operador de áudio de continuidade (rádio)", + "373110": "Operador de central de rádio", + "373115": "Operador de externa (rádio)", + "373120": "Operador de gravação de rádio", + "373125": "Operador de transmissor de rádio", + "373205": "Técnico em operação de equipamentos de produção para televisão e produtoras de vídeo", + "373210": "Técnico em operação de equipamento de exibição de televisão", + "373215": "Técnico em operação de equipamentos de transmissão/recepção de televisão", + "373220": "Supervisor técnico operacional de sistemas de televisão e produtoras de vídeo", + "374105": "Técnico em gravação de áudio", + "374110": "Técnico em instalação de equipamentos de áudio", + "374115": "Técnico em masterização de áudio", + "374120": "Projetista de som", + "374125": "Técnico em sonorização", + "374130": "Técnico em mixagem de áudio", + "374135": "Projetista de sistemas de áudio", + "374140": "Microfonista", + "374145": "Dj (disc jockey)", + "374205": "Cenotécnico (cinema", + "374210": "Maquinista de cinema e vídeo", + "374215": "Maquinista de teatro e espetáculos", + "374305": "Operador de projetor cinematográfico", + "374310": "Operador-mantenedor de projetor cinematográfico", + "374405": "Editor de TV e vídeo", + "374410": "Finalizador de filmes", + "374415": "Finalizador de vídeo", + "374420": "Montador de filmes", + "375105": "Designer de interiores", + "375110": "Designer de vitrines", + "375115": "Visual merchandiser", + "375120": "Decorador de eventos", + "376105": "Dançarino tradicional", + "376110": "Dançarino popular", + "376205": "Acrobata", + "376210": "Artista aéreo", + "376215": "Artista de circo (outros)", + "376220": "Contorcionista", + "376225": "Domador de animais (circense)", + "376230": "Equilibrista", + "376235": "Mágico", + "376240": "Malabarista", + "376245": "Palhaço", + "376250": "Titeriteiro", + "376255": "Trapezista", + "376305": "Apresentador de eventos", + "376310": "Apresentador de festas populares", + "376315": "Apresentador de programas de rádio", + "376320": "Apresentador de programas de televisão", + "376325": "Apresentador de circo", + "376405": "Modelo artístico", + "376410": "Modelo de modas", + "376415": "Modelo publicitário", + "377105": "Atleta profissional (outras modalidades)", + "377110": "Atleta profissional de futebol", + "377115": "Atleta profissional de golfe", + "377120": "Atleta profissional de luta", + "377125": "Atleta profissional de tênis", + "377130": "Jóquei", + "377135": "Piloto de competição automobilística", + "377140": "Profissional de atletismo", + "377145": "Pugilista", + "377205": "Arbitro desportivo", + "377210": "Arbitro de atletismo", + "377215": "Arbitro de basquete", + "377220": "Arbitro de futebol", + "377225": "Arbitro de futebol de salão", + "377230": "Arbitro de judô", + "377235": "Arbitro de karatê", + "377240": "Arbitro de poló aquático", + "377245": "Arbitro de vôlei", + "391105": "Cronoanalista", + "391110": "Cronometrista", + "391115": "Controlador de entrada e saída", + "391120": "Planejista", + "391125": "Técnico de planejamento de produção", + "391130": "Técnico de planejamento e programação da manutenção", + "391135": "Técnico de matéria-prima e material", + "391205": "Inspetor de qualidade", + "391210": "Técnico de garantia da qualidade", + "391215": "Operador de inspeção de qualidade", + "391220": "Técnico de painel de controle", + "391225": "Escolhedor de papel", + "391230": "Técnico operacional de serviços de correios", + "395105": "Técnico de apoio em pesquisa e desenvolvimento (exceto agropecuário e florestal)", + "395110": "Técnico de apoio em pesquisa e desenvolvimento agropecuário florestal", + "410105": "Supervisor administrativo", + "410205": "Supervisor de almoxarifado", + "410210": "Supervisor de câmbio", + "410215": "Supervisor de contas a pagar", + "410220": "Supervisor de controle patrimonial", + "410225": "Supervisor de crédito e cobrança", + "410230": "Supervisor de orçamento", + "410235": "Supervisor de tesouraria", + "411005": "Auxiliar de escritório", + "411010": "Assistente administrativo", + "411015": "Atendente de judiciário", + "411020": "Auxiliar de judiciário", + "411025": "Auxiliar de cartório", + "411030": "Auxiliar de pessoal", + "411035": "Auxiliar de estatística", + "411040": "Auxiliar de seguros", + "411045": "Auxiliar de serviços de importação e exportação", + "411050": "Agente de microcrédito", + "412105": "Datilógrafo", + "412110": "Digitador", + "412115": "Operador de mensagens de telecomunicações (correios)", + "412120": "Supervisor de digitação e operação", + "412205": "Contínuo", + "413105": "Analista de folha de pagamento", + "413110": "Auxiliar de contabilidade", + "413115": "Auxiliar de faturamento", + "413205": "Atendente de agência", + "413210": "Caixa de banco", + "413215": "Compensador de banco", + "413220": "Conferente de serviços bancários", + "413225": "Escriturário de banco", + "413230": "Operador de cobrança bancária", + "414105": "Almoxarife", + "414110": "Armazenista", + "414115": "Balanceiro", + "414205": "Apontador de mão-de-obra", + "414210": "Apontador de produção", + "414215": "Conferente de carga e descarga", + "415105": "Arquivista de documentos", + "415115": "Codificador de dados", + "415120": "Fitotecário", + "415125": "Kardexista", + "415130": "Operador de máquina copiadora (exceto operador de gráfica rápida)", + "415205": "Carteiro", + "415210": "Operador de triagem e transbordo", + "420105": "Supervisor de caixas e bilheteiros (exceto caixa de banco)", + "420110": "Supervisor de cobrança", + "420115": "Supervisor de coletadores de apostas e de jogos", + "420120": "Supervisor de entrevistadores e recenseadores", + "420125": "Supervisor de recepcionistas", + "420130": "Supervisor de telefonistas", + "420135": "Supervisor de telemarketing e atendimento", + "421105": "Atendente comercial (agência postal)", + "421110": "Bilheteiro de transportes coletivos", + "421115": "Bilheteiro no serviço de diversões", + "421120": "Emissor de passagens", + "421125": "Operador de caixa", + "421205": "Recebedor de apostas (loteria)", + "421210": "Recebedor de apostas (turfe)", + "421305": "Cobrador externo", + "421310": "Cobrador interno", + "421315": "Localizador (cobrador)", + "422105": "Recepcionista", + "422110": "Recepcionista de consultório médico ou dentário", + "422115": "Recepcionista de seguro saúde", + "422120": "Recepcionista de hotel", + "422125": "Recepcionista de banco", + "422205": "Telefonista", + "422210": "Teleoperador", + "422215": "Monitor de teleatendimento", + "422220": "Operador de rádio-chamada", + "422305": "Operador de telemarketing ativo", + "422310": "Operador de telemarketing ativo e receptivo", + "422315": "Operador de telemarketing receptivo", + "422320": "Operador de telemarketing técnico", + "423105": "Despachante documentalista", + "423110": "Despachante de trânsito", + "424105": "Entrevistador censitário e de pesquisas amostrais", + "424110": "Entrevistador de pesquisa de opinião e mídia", + "424115": "Entrevistador de pesquisas de mercado", + "424120": "Entrevistador de preços", + "424125": "Escriturário em estatística", + "510105": "Supervisor de transportes", + "510110": "Administrador de edifícios", + "510115": "Supervisor de andar", + "510120": "Chefe de portaria de hotel", + "510130": "Chefe de bar", + "510135": "Maître", + "510205": "Supervisor de lavanderia", + "510305": "Supervisor de bombeiros", + "510310": "Supervisor de vigilantes", + "511105": "Comissário de vôo", + "511110": "Comissário de trem", + "511115": "Taifeiro (exceto militares)", + "511205": "Fiscal de transportes coletivos (exceto trem)", + "511210": "Despachante de transportes coletivos (exceto trem)", + "511215": "Cobrador de transportes coletivos (exceto trem)", + "511220": "Bilheteiro (estações de metrô", + "511405": "Guia de turismo", + "512105": "Empregado doméstico nos serviços gerais", + "512110": "Empregado doméstico arrumador", + "512115": "Empregado doméstico faxineiro", + "512120": "Empregado doméstico diarista", + "513105": "Mordomo de residência", + "513110": "Mordomo de hotelaria", + "513115": "Governanta de hotelaria", + "513205": "Cozinheiro geral", + "513210": "Cozinheiro do serviço doméstico", + "513215": "Cozinheiro industrial", + "513220": "Cozinheiro de hospital", + "513225": "Cozinheiro de embarcações", + "513305": "Camareira de teatro", + "513310": "Camareira de televisão", + "513315": "Camareiro de hotel", + "513320": "Camareiro de embarcações", + "513325": "Guarda-roupeira de cinema", + "513405": "Garçom", + "513410": "Garçom (serviços de vinhos)", + "513415": "Cumim", + "513420": "Barman", + "513425": "Copeiro", + "513430": "Copeiro de hospital", + "513435": "Atendente de lanchonete", + "513440": "Barista", + "513505": "Auxiliar nos serviços de alimentação", + "513605": "Churrasqueiro", + "513610": "Pizzaiolo", + "513615": "Sushiman", + "514105": "Ascensorista", + "514110": "Garagista", + "514115": "Sacristão", + "514120": "Zelador de edifício", + "514205": "Coletor de lixo domiciliar", + "514215": "Varredor de rua", + "514225": "Trabalhador de serviços de limpeza e conservação de áreas públicas", + "514230": "Coletor de resíduos sólidos de serviços de saúde", + "514305": "Limpador de vidros", + "514310": "Auxiliar de manutenção predial", + "514315": "Limpador de fachadas", + "514320": "Faxineiro", + "514325": "Trabalhador da manutenção de edificações", + "514330": "Limpador de piscinas", + "515105": "Agente comunitário de saúde", + "515110": "Atendente de enfermagem", + "515115": "Parteira leiga", + "515120": "Visitador sanitário", + "515125": "Agente indígena de saúde", + "515130": "Agente indígena de saneamento", + "515135": "Socorrista (exceto médicos e enfermeiros)", + "515205": "Auxiliar de banco de sangue", + "515210": "Auxiliar de farmácia de manipulação", + "515215": "Auxiliar de laboratório de análises clínicas", + "515220": "Auxiliar de laboratório de imunobiológicos", + "515225": "Auxiliar de produção farmacêutica", + "515305": "Educador social", + "515310": "Agente de ação social", + "515315": "Monitor de dependente químico", + "515320": "Conselheiro tutelar", + "515325": "Sócioeducador", + "516105": "Barbeiro", + "516110": "Cabeleireiro", + "516120": "Manicure", + "516125": "Maquiador", + "516130": "Maquiador de caracterização", + "516140": "Pedicure", + "516205": "Babá", + "516210": "Cuidador de idosos", + "516215": "Mae social", + "516220": "Cuidador em saúde", + "516305": "Lavadeiro", + "516310": "Lavador de roupas a maquina", + "516315": "Lavador de artefatos de tapeçaria", + "516320": "Limpador a seco", + "516325": "Passador de roupas em geral", + "516330": "Tingidor de roupas", + "516335": "Conferente-expedidor de roupas (lavanderias)", + "516340": "Atendente de lavanderia", + "516345": "Auxiliar de lavanderia", + "516405": "Lavador de roupas", + "516410": "Limpador de roupas a seco", + "516415": "Passador de roupas", + "516505": "Agente funerário", + "516605": "Operador de forno (serviços funerários)", + "516610": "Sepultador", + "516705": "Astrólogo", + "516710": "Numerólogo", + "516805": "Esotérico", + "516810": "Paranormal", + "517105": "Bombeiro de aeródromo", + "517110": "Bombeiro de segurança do trabalho", + "517115": "Salva-vidas", + "517205": "Agente de polícia federal", + "517210": "Policial rodoviário federal", + "517215": "Guarda-civil municipal", + "517220": "Agente de trânsito", + "517305": "Agente de proteção de aeroporto", + "517310": "Agente de segurança", + "517315": "Agente de segurança penitenciária", + "517320": "Vigia florestal", + "517325": "Vigia portuário", + "517330": "Vigilante", + "517335": "Guarda portuário", + "517405": "Porteiro (hotel)", + "517410": "Porteiro de edifícios", + "517415": "Porteiro de locais de diversão", + "517420": "Vigia", + "519105": "Ciclista mensageiro", + "519110": "Motociclista no transporte de documentos e pequenos volumes", + "519205": "Catador de material reciclável", + "519210": "Selecionador de material reciclável", + "519215": "Operador de prensa de material reciclável", + "519305": "Auxiliar de veterinário", + "519310": "Esteticista de animais domésticos", + "519315": "Banhista de animais domésticos", + "519320": "Tosador de animais domésticos", + "519805": "Profissional do sexo", + "519905": "Cartazeiro", + "519910": "Controlador de pragas", + "519915": "Engraxate", + "519920": "Gandula", + "519925": "Guardador de veículos", + "519930": "Lavador de garrafas", + "519935": "Lavador de veículos", + "519940": "Leiturista", + "519945": "Recepcionista de casas de espetáculos", + "520105": "Supervisor de vendas de serviços", + "520110": "Supervisor de vendas comercial", + "521105": "Vendedor em comércio atacadista", + "521110": "Vendedor de comércio varejista", + "521115": "Promotor de vendas", + "521120": "Demonstrador de mercadorias", + "521125": "Repositor de mercadorias", + "521130": "Atendente de farmácia - balconista", + "521135": "Frentista", + "523105": "Instalador de cortinas e persianas", + "523110": "Instalador de som e acessórios de veículos", + "523115": "Chaveiro", + "524105": "Vendedor em domicílio", + "524205": "Feirante", + "524210": "Jornaleiro (em banca de jornal)", + "524215": "Vendedor permissionário", + "524305": "Vendedor ambulante", + "524310": "Pipoqueiro ambulante", + "611005": "Produtor agropecuário", + "612005": "Produtor agrícola polivalente", + "612105": "Produtor de arroz", + "612110": "Produtor de cana-de-açúcar", + "612115": "Produtor de cereais de inverno", + "612120": "Produtor de gramíneas forrageiras", + "612125": "Produtor de milho e sorgo", + "612205": "Produtor de algodão", + "612210": "Produtor de curauá", + "612215": "Produtor de juta", + "612220": "Produtor de rami", + "612225": "Produtor de sisal", + "612305": "Produtor na olericultura de legumes", + "612310": "Produtor na olericultura de raízes", + "612315": "Produtor na olericultura de talos", + "612320": "Produtor na olericultura de frutos e sementes", + "612405": "Produtor de flores de corte", + "612410": "Produtor de flores em vaso", + "612415": "Produtor de forrações", + "612420": "Produtor de plantas ornamentais", + "612505": "Produtor de árvores frutíferas", + "612510": "Produtor de espécies frutíferas rasteiras", + "612515": "Produtor de espécies frutíferas trepadeiras", + "612605": "Cafeicultor", + "612610": "Produtor de cacau", + "612615": "Produtor de erva-mate", + "612620": "Produtor de fumo", + "612625": "Produtor de guaraná", + "612705": "Produtor da cultura de amendoim", + "612710": "Produtor da cultura de canola", + "612715": "Produtor da cultura de coco-da-baia", + "612720": "Produtor da cultura de dendê", + "612725": "Produtor da cultura de girassol", + "612730": "Produtor da cultura de linho", + "612735": "Produtor da cultura de mamona", + "612740": "Produtor da cultura de soja", + "612805": "Produtor de especiarias", + "612810": "Produtor de plantas aromáticas e medicinais", + "613005": "Criador em pecuária polivalente", + "613010": "Criador de animais domésticos", + "613105": "Criador de asininos e muares", + "613110": "Criador de bovinos (corte)", + "613115": "Criador de bovinos (leite)", + "613120": "Criador de bubalinos (corte)", + "613125": "Criador de bubalinos (leite)", + "613130": "Criador de eqüínos", + "613205": "Criador de caprinos", + "613210": "Criador de ovinos", + "613215": "Criador de suínos", + "613305": "Avicultor", + "613310": "Cunicultor", + "613405": "Apicultor", + "613410": "Criador de animais produtores de veneno", + "613415": "Minhocultor", + "613420": "Sericultor", + "620105": "Supervisor de exploração agrícola", + "620110": "Supervisor de exploração agropecuária", + "620115": "Supervisor de exploração pecuária", + "621005": "Trabalhador agropecuário em geral", + "622005": "Caseiro (agricultura)", + "622010": "Jardineiro", + "622015": "Trabalhador na produção de mudas e sementes", + "622020": "Trabalhador volante da agricultura", + "622105": "Trabalhador da cultura de arroz", + "622110": "Trabalhador da cultura de cana-de-açúcar", + "622115": "Trabalhador da cultura de milho e sorgo", + "622120": "Trabalhador da cultura de trigo", + "622205": "Trabalhador da cultura de algodão", + "622210": "Trabalhador da cultura de sisal", + "622215": "Trabalhador da cultura do rami", + "622305": "Trabalhador na olericultura (frutos e sementes)", + "622310": "Trabalhador na olericultura (legumes)", + "622315": "Trabalhador na olericultura (raízes", + "622320": "Trabalhador na olericultura (talos", + "622405": "Trabalhador no cultivo de flores e folhagens de corte", + "622410": "Trabalhador no cultivo de flores em vaso", + "622415": "Trabalhador no cultivo de forrações", + "622420": "Trabalhador no cultivo de mudas", + "622425": "Trabalhador no cultivo de plantas ornamentais", + "622505": "Trabalhador no cultivo de árvores frutíferas", + "622510": "Trabalhador no cultivo de espécies frutíferas rasteiras", + "622515": "Trabalhador no cultivo de trepadeiras frutíferas", + "622605": "Trabalhador da cultura de cacau", + "622610": "Trabalhador da cultura de café", + "622615": "Trabalhador da cultura de erva-mate", + "622620": "Trabalhador da cultura de fumo", + "622625": "Trabalhador da cultura de guaraná", + "622705": "Trabalhador na cultura de amendoim", + "622710": "Trabalhador na cultura de canola", + "622715": "Trabalhador na cultura de coco-da-baía", + "622720": "Trabalhador na cultura de dendê", + "622725": "Trabalhador na cultura de mamona", + "622730": "Trabalhador na cultura de soja", + "622735": "Trabalhador na cultura do girassol", + "622740": "Trabalhador na cultura do linho", + "622805": "Trabalhador da cultura de especiarias", + "622810": "Trabalhador da cultura de plantas aromáticas e medicinais", + "623005": "Adestrador de animais", + "623010": "Inseminador", + "623015": "Trabalhador de pecuária polivalente", + "623020": "Tratador de animais", + "623105": "Trabalhador da pecuária (asininos e muares)", + "623110": "Trabalhador da pecuária (bovinos corte)", + "623115": "Trabalhador da pecuária (bovinos leite)", + "623120": "Trabalhador da pecuária (bubalinos)", + "623125": "Trabalhador da pecuária (eqüinos)", + "623205": "Trabalhador da caprinocultura", + "623210": "Trabalhador da ovinocultura", + "623215": "Trabalhador da suinocultura", + "623305": "Trabalhador da avicultura de corte", + "623310": "Trabalhador da avicultura de postura", + "623315": "Operador de incubadora", + "623320": "Trabalhador da cunicultura", + "623325": "Sexador", + "623405": "Trabalhador em criatórios de animais produtores de veneno", + "623410": "Trabalhador na apicultura", + "623415": "Trabalhador na minhocultura", + "623420": "Trabalhador na sericicultura", + "630105": "Supervisor da aqüicultura", + "630110": "Supervisor da área florestal", + "631005": "Catador de caranguejos e siris", + "631010": "Catador de mariscos", + "631015": "Pescador artesanal de lagostas", + "631020": "Pescador artesanal de peixes e camaroes", + "631105": "Pescador artesanal de água doce", + "631205": "Pescador industrial", + "631210": "Pescador profissional", + "631305": "Criador de camaroes", + "631310": "Criador de jacarés", + "631315": "Criador de mexilhoes", + "631320": "Criador de ostras", + "631325": "Criador de peixes", + "631330": "Criador de quelônios", + "631335": "Criador de ras", + "631405": "Gelador industrial", + "631410": "Gelador profissional", + "631415": "Proeiro", + "631420": "Redeiro (pesca)", + "632005": "Guia florestal", + "632010": "Raizeiro", + "632015": "Viveirista florestal", + "632105": "Classificador de toras", + "632110": "Cubador de madeira", + "632115": "Identificador florestal", + "632120": "Operador de motosserra", + "632125": "Trabalhador de extração florestal", + "632205": "Seringueiro", + "632210": "Trabalhador da exploração de espécies produtoras de gomas não elásticas", + "632215": "Trabalhador da exploração de resinas", + "632305": "Trabalhador da exploração de andiroba", + "632310": "Trabalhador da exploração de babaçu", + "632315": "Trabalhador da exploração de bacaba", + "632320": "Trabalhador da exploração de buriti", + "632325": "Trabalhador da exploração de carnaúba", + "632330": "Trabalhador da exploração de coco-da-praia", + "632335": "Trabalhador da exploração de copaíba", + "632340": "Trabalhador da exploração de malva (paina)", + "632345": "Trabalhador da exploração de murumuru", + "632350": "Trabalhador da exploração de oiticica", + "632355": "Trabalhador da exploração de ouricuri", + "632360": "Trabalhador da exploração de pequi", + "632365": "Trabalhador da exploração de piaçava", + "632370": "Trabalhador da exploração de tucum", + "632405": "Trabalhador da exploração de açaí", + "632410": "Trabalhador da exploração de castanha", + "632415": "Trabalhador da exploração de pinhão", + "632420": "Trabalhador da exploração de pupunha", + "632505": "Trabalhador da exploração de árvores e arbustos produtores de substâncias aromát.", + "632510": "Trabalhador da exploração de cipós produtores de substâncias aromáticas", + "632515": "Trabalhador da exploração de madeiras tanantes", + "632520": "Trabalhador da exploração de raízes produtoras de substâncias aromáticas", + "632525": "Trabalhador da extração de substâncias aromáticas", + "632605": "Carvoeiro", + "632610": "Carbonizador", + "632615": "Ajudante de carvoaria", + "641005": "Operador de colheitadeira", + "641010": "Operador de máquinas de beneficiamento de produtos agrícolas", + "641015": "Tratorista agrícola", + "642005": "Operador de colhedor florestal", + "642010": "Operador de máquinas florestais estáticas", + "642015": "Operador de trator florestal", + "643005": + "Trabalhador na operação de sistema de irrigação localizada (microaspersão e gotejamento)", + "643010": "Trabalhador na operação de sistema de irrigação por aspersão (pivô central)", + "643015": "Trabalhador na operação de sistemas convencionais de irrigação por aspersão", + "643020": "Trabalhador na operação de sistemas de irrigação e aspersão (alto propelido)", + "643025": "Trabalhador na operação de sistemas de irrigação por superfície e drenagem", + "710105": "Supervisor de apoio operacional na mineração", + "710110": "Supervisor de extração de sal", + "710115": "Supervisor de perfuração e desmonte", + "710120": "Supervisor de produção na mineração", + "710125": "Supervisor de transporte na mineração", + "710205": "Mestre (construção civil)", + "710210": "Mestre de linhas (ferrovias)", + "710215": "Inspetor de terraplenagem", + "710220": "Supervisor de usina de concreto", + "710225": "Fiscal de pátio de usina de concreto", + "711105": "Amostrador de minérios", + "711110": "Canteiro", + "711115": "Destroçador de pedra", + "711120": "Detonador", + "711125": "Escorador de minas", + "711130": "Mineiro", + "711205": "Operador de caminhão (minas e pedreiras)", + "711210": "Operador de carregadeira", + "711215": "Operador de máquina cortadora (minas e pedreiras)", + "711220": "Operador de máquina de extração contínua (minas de carvão)", + "711225": "Operador de máquina perfuradora (minas e pedreiras)", + "711230": "Operador de máquina perfuratriz", + "711235": "Operador de motoniveladora (extração de minerais sólidos)", + "711240": "Operador de schutthecar", + "711245": "Operador de trator (minas e pedreiras)", + "711305": "Operador de sonda de percussão", + "711310": "Operador de sonda rotativa", + "711315": "Sondador (poços de petróleo e gás)", + "711320": "Sondador de poços (exceto de petróleo e gás)", + "711325": "Plataformista (petróleo)", + "711330": "Torrista (petróleo)", + "711405": "Garimpeiro", + "711410": "Operador de salina (sal marinho)", + "712105": "Moleiro de minérios", + "712110": "Operador de aparelho de flotação", + "712115": "Operador de aparelho de precipitação (minas de ouro ou prata)", + "712120": "Operador de britador de mandíbulas", + "712125": "Operador de espessador", + "712130": "Operador de jig (minas)", + "712135": "Operador de peneiras hidráulicas", + "712205": "Cortador de pedras", + "712210": "Gravador de inscrições em pedra", + "712215": "Gravador de relevos em pedra", + "712220": "Polidor de pedras", + "712225": "Torneiro (lavra de pedra)", + "712230": "Traçador de pedras", + "715105": "Operador de bate-estacas", + "715110": "Operador de compactadora de solos", + "715115": "Operador de escavadeira", + "715120": "Operador de máquina de abrir valas", + "715125": "Operador de máquinas de construção civil e mineração", + "715130": "Operador de motoniveladora", + "715135": "Operador de pá carregadeira", + "715140": "Operador de pavimentadora (asfalto", + "715145": "Operador de trator de lâmina", + "715205": "Calceteiro", + "715210": "Pedreiro", + "715215": "Pedreiro (chaminés industriais)", + "715220": "Pedreiro (material refratário)", + "715225": "Pedreiro (mineração)", + "715230": "Pedreiro de edificações", + "715305": "Armador de estrutura de concreto", + "715310": "Moldador de corpos de prova em usinas de concreto", + "715315": "Armador de estrutura de concreto armado", + "715405": "Operador de betoneira", + "715410": "Operador de bomba de concreto", + "715415": "Operador de central de concreto", + "715505": "Carpinteiro", + "715510": "Carpinteiro (esquadrias)", + "715515": "Carpinteiro (cenários)", + "715520": "Carpinteiro (mineração)", + "715525": "Carpinteiro de obras", + "715530": "Carpinteiro (telhados)", + "715535": "Carpinteiro de fôrmas para concreto", + "715540": "Carpinteiro de obras civis de arte (pontes", + "715545": "Montador de andaimes (edificações)", + "715605": "Eletricista de instalações (cenários)", + "715610": "Eletricista de instalações (edifícios)", + "715615": "Eletricista de instalações", + "715705": "Aplicador de asfalto impermeabilizante (coberturas)", + "715710": "Instalador de isolantes acústicos", + "715715": "Instalador de isolantes térmicos (refrigeração e climatização)", + "715720": "Instalador de isolantes térmicos de caldeira e tubulações", + "715725": "Instalador de material isolante", + "715730": "Instalador de material isolante", + "716105": "Acabador de superfícies de concreto", + "716110": "Revestidor de superfícies de concreto", + "716205": "Telhador (telhas de argila e materias similares)", + "716210": "Telhador (telhas de cimento-amianto)", + "716215": "Telhador (telhas metálicas)", + "716220": "Telhador (telhas pláticas)", + "716305": "Vidraceiro", + "716310": "Vidraceiro (edificações)", + "716315": "Vidraceiro (vitrais)", + "716405": "Gesseiro", + "716505": "Assoalhador", + "716510": "Ladrilheiro", + "716515": "Pastilheiro", + "716520": "Lustrador de piso", + "716525": "Marmorista (construção)", + "716530": "Mosaísta", + "716535": "Taqueiro", + "716605": "Calafetador", + "716610": "Pintor de obras", + "716615": "Revestidor de interiores (papel", + "717005": "Demolidor de edificações", + "717010": "Operador de martelete", + "717015": "Poceiro (edificações)", + "717020": "Servente de obras", + "717025": "Vibradorista", + "720105": "Mestre (afiador de ferramentas)", + "720110": "Mestre de caldeiraria", + "720115": "Mestre de ferramentaria", + "720120": "Mestre de forjaria", + "720125": "Mestre de fundição", + "720130": "Mestre de galvanoplastia", + "720135": "Mestre de pintura (tratamento de superfícies)", + "720140": "Mestre de soldagem", + "720145": "Mestre de trefilação de metais", + "720150": "Mestre de usinagem", + "720155": "Mestre serralheiro", + "720160": "Supervisor de controle de tratamento térmico", + "720205": "Mestre (construção naval)", + "720210": "Mestre (indústria de automotores e material de transportes)", + "720215": "Mestre (indústria de máquinas e outros equipamentos mecânicos)", + "720220": "Mestre de construção de fornos", + "721105": "Ferramenteiro", + "721110": "Ferramenteiro de mandris", + "721115": "Modelador de metais (fundição)", + "721205": "Operador de máquina de eletroerosão", + "721210": "Operador de máquinas operatrizes", + "721215": "Operador de máquinas-ferramenta convencionais", + "721220": "Operador de usinagem convencional por abrasão", + "721225": "Preparador de máquinas-ferramenta", + "721305": "Afiador de cardas", + "721310": "Afiador de cutelaria", + "721315": "Afiador de ferramentas", + "721320": "Afiador de serras", + "721325": "Polidor de metais", + "721405": "Operador de centro de usinagem com comando numérico", + "721410": "Operador de fresadora com comando numérico", + "721415": "Operador de mandriladora com comando numérico", + "721420": "Operador de máquina eletroerosão", + "721425": "Operador de retificadora com comando numérico", + "721430": "Operador de torno com comando numérico", + "722105": "Forjador", + "722110": "Forjador a martelo", + "722115": "Forjador prensista", + "722205": "Fundidor de metais", + "722210": "Lingotador", + "722215": "Operador de acabamento de peças fundidas", + "722220": "Operador de máquina centrifugadora de fundição", + "722225": "Operador de máquina de fundir sob pressão", + "722230": "Operador de vazamento (lingotamento)", + "722235": "Preparador de panelas (lingotamento)", + "722305": "Macheiro", + "722310": "Macheiro", + "722315": "Moldador", + "722320": "Moldador", + "722325": "Operador de equipamentos de preparação de areia", + "722330": "Operador de máquina de moldar automatizada", + "722405": "Cableador", + "722410": "Estirador de tubos de metal sem costura", + "722415": "Trefilador de metais", + "723105": "Cementador de metais", + "723110": "Normalizador de metais e de compósitos", + "723115": "Operador de equipamento para resfriamento", + "723120": "Operador de forno de tratamento térmico de metais", + "723125": "Temperador de metais e de compósitos", + "723205": "Decapador", + "723210": "Fosfatizador", + "723215": "Galvanizador", + "723220": "Metalizador a pistola", + "723225": "Metalizador (banho quente)", + "723230": "Operador de máquina recobridora de arame", + "723235": "Operador de zincagem (processo eletrolítico)", + "723240": "Oxidador", + "723305": "Operador de equipamento de secagem de pintura", + "723310": "Pintor a pincel e rolo (exceto obras e estruturas metálicas)", + "723315": "Pintor de estruturas metálicas", + "723320": "Pintor de veículos (fabricação)", + "723325": "Pintor por imersão", + "723330": "Pintor", + "724105": "Assentador de canalização (edificações)", + "724110": "Encanador", + "724115": "Instalador de tubulações", + "724120": "Instalador de tubulações (aeronaves)", + "724125": "Instalador de tubulações (embarcações)", + "724130": "Instalador de tubulações de gás combustível (produção e distribuição)", + "724135": "Instalador de tubulações de vapor (produção e distribuição)", + "724205": "Montador de estruturas metálicas", + "724210": "Montador de estruturas metálicas de embarcações", + "724215": "Rebitador a martelo pneumático", + "724220": "Preparador de estruturas metálicas", + "724225": "Riscador de estruturas metálicas", + "724230": "Rebitador", + "724305": "Brasador", + "724310": "Oxicortador a mão e a máquina", + "724315": "Soldador", + "724320": "Soldador a oxigás", + "724325": "Soldador elétrico", + "724405": "Caldeireiro (chapas de cobre)", + "724410": "Caldeireiro (chapas de ferro e aço)", + "724415": "Chapeador", + "724420": "Chapeador de carrocerias metálicas (fabricação)", + "724425": "Chapeador naval", + "724430": "Chapeador de aeronaves", + "724435": "Funileiro industrial", + "724440": "Serralheiro", + "724505": "Operador de máquina de cilindrar chapas", + "724510": "Operador de máquina de dobrar chapas", + "724515": "Prensista (operador de prensa)", + "724605": "Operador de laços de cabos de aço", + "724610": "Trançador de cabos de aço", + "725005": "Ajustador ferramenteiro", + "725010": "Ajustador mecânico", + "725015": "Ajustador mecânico (usinagem em bancada e em máquinas-ferramentas)", + "725020": "Ajustador mecânico em bancada", + "725025": "Ajustador naval (reparo e construção)", + "725105": "Montador de máquinas", + "725205": "Montador de máquinas", + "725210": "Montador de máquinas gráficas", + "725215": "Montador de máquinas operatrizes para madeira", + "725220": "Montador de máquinas têxteis", + "725225": "Montador de máquinas-ferramentas (usinagem de metais)", + "725305": "Montador de equipamento de levantamento", + "725310": "Montador de máquinas agrícolas", + "725315": "Montador de máquinas de minas e pedreiras", + "725320": "Montador de máquinas de terraplenagem", + "725405": "Mecânico montador de motores de aeronaves", + "725410": "Mecânico montador de motores de embarcações", + "725415": "Mecânico montador de motores de explosão e diesel", + "725420": "Mecânico montador de turboalimentadores", + "725505": "Montador de veículos (linha de montagem)", + "725510": "Operador de time de montagem", + "725605": "Montador de estruturas de aeronaves", + "725610": "Montador de sistemas de combustível de aeronaves", + "725705": "Mecânico de refrigeração", + "730105": "Supervisor de montagem e instalação eletroeletrônica", + "731105": "Montador de equipamentos eletrônicos (aparelhos médicos)", + "731110": "Montador de equipamentos eletrônicos (computadores e equipamentos auxiliares)", + "731115": "Montador de equipamentos elétricos (instrumentos de medição)", + "731120": "Montador de equipamentos elétricos (aparelhos eletrodomésticos)", + "731125": "Montador de equipamentos elétricos (centrais elétricas)", + "731130": "Montador de equipamentos elétricos (motores e dínamos)", + "731135": "Montador de equipamentos elétricos", + "731140": "Montador de equipamentos eletrônicos (instalações de sinalização)", + "731145": "Montador de equipamentos eletrônicos (máquinas industriais)", + "731150": "Montador de equipamentos eletrônicos", + "731155": "Montador de equipamentos elétricos (elevadores e equipamentos similares)", + "731160": "Montador de equipamentos elétricos (transformadores)", + "731165": "Bobinador eletricista", + "731170": "Bobinador eletricista", + "731175": "Operador de linha de montagem (aparelhos elétricos)", + "731180": "Operador de linha de montagem (aparelhos eletrônicos)", + "731205": "Montador de equipamentos eletrônicos (estação de rádio", + "731305": "Instalador-reparador de equipamentos de comutação em telefonia", + "731310": "Instalador-reparador de equipamentos de energia em telefonia", + "731315": "Instalador-reparador de equipamentos de transmissão em telefonia", + "731320": "Instalador-reparador de linhas e aparelhos de telecomunicações", + "731325": "Instalador-reparador de redes e cabos telefônicos", + "731330": "Reparador de aparelhos de telecomunicações em laboratório", + "732105": "Eletricista de manutenção de linhas elétricas", + "732110": "Emendador de cabos elétricos e telefônicos (aéreos e subterrâneos)", + "732115": "Examinador de cabos", + "732120": "Instalador de linhas elétricas de alta e baixa-tensão (rede aérea e subterrânea)", + "732125": "Instalador eletricista (tração de veículos)", + "732130": "Instalador-reparador de redes telefônicas e de comunicação de dados", + "732135": "Ligador de linhas telefônicas", + "740105": "Supervisor da mecânica de precisão", + "740110": "Supervisor de fabricação de instrumentos musicais", + "741105": "Ajustador de instrumentos de precisão", + "741110": "Montador de instrumentos de óptica", + "741115": "Montador de instrumentos de precisão", + "741120": "Relojoeiro (fabricação)", + "741125": "Relojoeiro (reparação)", + "742105": "Afinador de instrumentos musicais", + "742110": "Confeccionador de acordeão", + "742115": "Confeccionador de instrumentos de corda", + "742120": "Confeccionador de instrumentos de percussão (pele", + "742125": "Confeccionador de instrumentos de sopro (madeira)", + "742130": "Confeccionador de instrumentos de sopro (metal)", + "742135": "Confeccionador de orgão", + "742140": "Confeccionador de piano", + "750105": "Supervisor de joalheria", + "750205": + "Supervisor da indústria de minerais não metálicos (exceto os derivados de petróleo e carvão)", + "751005": "Engastador (jóias)", + "751010": "Joalheiro", + "751015": "Joalheiro (reparações)", + "751020": "Lapidador (jóias)", + "751105": "Bate-folha a máquina", + "751110": "Fundidor (joalheria e ourivesaria)", + "751115": "Gravador (joalheria e ourivesaria)", + "751120": "Laminador de metais preciosos a mão", + "751125": "Ourives", + "751130": "Trefilador (joalheria e ourivesaria)", + "752105": "Artesão modelador (vidros)", + "752110": "Moldador (vidros)", + "752115": "Soprador de vidro", + "752120": "Transformador de tubos de vidro", + "752205": "Aplicador serigráfico em vidros", + "752210": "Cortador de vidro", + "752215": "Gravador de vidro a água-forte", + "752220": "Gravador de vidro a esmeril", + "752225": "Gravador de vidro a jato de areia", + "752230": "Lapidador de vidros e cristais", + "752235": "Surfassagista", + "752305": "Ceramista", + "752310": "Ceramista (torno de pedal e motor)", + "752315": "Ceramista (torno semi-automático)", + "752320": "Ceramista modelador", + "752325": "Ceramista moldador", + "752330": "Ceramista prensador", + "752405": "Decorador de cerâmica", + "752410": "Decorador de vidro", + "752415": "Decorador de vidro à pincel", + "752420": "Operador de esmaltadeira", + "752425": "Operador de espelhamento", + "752430": "Pintor de cerâmica", + "760105": "Contramestre de acabamento (indústria têxtil)", + "760110": "Contramestre de fiação (indústria têxtil)", + "760115": "Contramestre de malharia (indústria têxtil)", + "760120": "Contramestre de tecelagem (indústria têxtil)", + "760125": "Mestre (indústria têxtil e de confecções)", + "760205": "Supervisor de curtimento", + "760305": "Encarregado de corte na confecção do vestuário", + "760310": "Encarregado de costura na confecção do vestuário", + "760405": "Supervisor (indústria de calçados e artefatos de couro)", + "760505": "Supervisor da confecção de artefatos de tecidos", + "760605": "Supervisor das artes gráficas (indústria editorial e gráfica)", + "761005": "Operador polivalente da indústria têxtil", + "761105": "Classificador de fibras têxteis", + "761110": "Lavador de la", + "761205": "Operador de abertura (fiação)", + "761210": "Operador de binadeira", + "761215": "Operador de bobinadeira", + "761220": "Operador de cardas", + "761225": "Operador de conicaleira", + "761230": "Operador de filatório", + "761235": "Operador de laminadeira e reunideira", + "761240": "Operador de maçaroqueira", + "761245": "Operador de open-end", + "761250": "Operador de passador (fiação)", + "761255": "Operador de penteadeira", + "761260": "Operador de retorcedeira", + "761303": "Tecelão (redes)", + "761306": "Tecelão (rendas e bordados)", + "761309": "Tecelão (tear automático)", + "761312": "Tecelão (tear jacquard)", + "761315": "Tecelão (tear mecânico de maquineta)", + "761318": "Tecelão (tear mecânico de xadrez)", + "761321": "Tecelão (tear mecânico liso)", + "761324": "Tecelão (tear mecânico", + "761327": "Tecelão de malhas", + "761330": "Tecelão de malhas (máquina circular)", + "761333": "Tecelão de malhas (máquina retilínea)", + "761336": "Tecelão de meias", + "761339": "Tecelão de meias (máquina circular)", + "761342": "Tecelão de meias (máquina retilínea)", + "761345": "Tecelão de tapetes", + "761348": "Operador de engomadeira de urdume", + "761351": "Operador de espuladeira", + "761354": "Operador de máquina de cordoalha", + "761357": "Operador de urdideira", + "761360": "Passamaneiro a máquina", + "761363": "Remetedor de fios", + "761366": "Picotador de cartoes jacquard", + "761405": "Alvejador (tecidos)", + "761410": "Estampador de tecido", + "761415": "Operador de calandras (tecidos)", + "761420": "Operador de chamuscadeira de tecidos", + "761425": "Operador de impermeabilizador de tecidos", + "761430": "Operador de máquina de lavar fios e tecidos", + "761435": "Operador de rameuse", + "761805": "Inspetor de estamparia (produção têxtil)", + "761810": "Revisor de fios (produção têxtil)", + "761815": "Revisor de tecidos acabados", + "761820": "Revisor de tecidos crus", + "762005": "Trabalhador polivalente do curtimento de couros e peles", + "762105": "Classificador de peles", + "762110": "Descarnador de couros e peles", + "762115": "Estirador de couros e peles (preparação)", + "762120": "Fuloneiro", + "762125": "Rachador de couros e peles", + "762205": "Curtidor (couros e peles)", + "762210": "Classificador de couros", + "762215": "Enxugador de couros", + "762220": "Rebaixador de couros", + "762305": "Estirador de couros e peles (acabamento)", + "762310": "Fuloneiro no acabamento de couros e peles", + "762315": "Lixador de couros e peles", + "762320": "Matizador de couros e peles", + "762325": "Operador de máquinas do acabamento de couros e peles", + "762330": "Prensador de couros e peles", + "762335": "Palecionador de couros e peles", + "762340": "Preparador de couros curtidos", + "762345": "Vaqueador de couros e peles", + "763005": "Alfaiate", + "763010": "Costureira de peças sob encomenda", + "763015": "Costureira de reparação de roupas", + "763020": "Costureiro de roupa de couro e pele", + "763105": "Auxiliar de corte (preparação da confecção de roupas)", + "763110": "Cortador de roupas", + "763115": "Enfestador de roupas", + "763120": "Riscador de roupas", + "763125": "Ajudante de confecção", + "763205": "Costureiro de roupas de couro e pele", + "763210": "Costureiro na confecção em série", + "763215": "Costureiro", + "763305": "Arrematadeira", + "763310": "Bordador", + "763315": "Marcador de peças confeccionadas para bordar", + "763320": "Operador de máquina de costura de acabamento", + "763325": "Passadeira de peças confeccionadas", + "764005": "Trabalhador polivalente da confecção de calçados", + "764105": "Cortador de calçados", + "764110": "Cortador de solas e palmilhas", + "764115": "Preparador de calçados", + "764120": "Preparador de solas e palmilhas", + "764205": "Costurador de calçados", + "764210": "Montador de calçados", + "764305": "Acabador de calçados", + "765005": "Confeccionador de artefatos de couro (exceto sapatos)", + "765010": "Chapeleiro de senhoras", + "765015": "Boneleiro", + "765105": "Cortador de artefatos de couro (exceto roupas e calçados)", + "765110": "Cortador de tapeçaria", + "765205": "Colchoeiro (confecção de colchoes)", + "765215": "Confeccionador de brinquedos de pano", + "765225": "Confeccionador de velas náuticas", + "765230": "Estofador de avioes", + "765235": "Estofador de móveis", + "765310": "Costurador de artefatos de couro", + "765315": "Montador de artefatos de couro (exceto roupas e calçados)", + "765405": "Trabalhador do acabamento de artefatos de tecidos e couros", + "766105": "Copiador de chapa", + "766115": "Gravador de matriz para flexografia (clicherista)", + "766120": "Editor de texto e imagem", + "766125": "Montador de fotolito (analógico e digital)", + "766130": "Gravador de matriz para rotogravura (eletromecânico e químico)", + "766135": "Gravador de matriz calcográfica", + "766140": "Gravador de matriz serigráfica", + "766145": "Operador de sistemas de prova (analógico e digital)", + "766150": "Operador de processo de tratamento de imagem", + "766155": "Programador visual gráfico", + "766205": "Impressor (serigrafia)", + "766210": "Impressor calcográfico", + "766215": "Impressor de ofsete (plano e rotativo)", + "766220": "Impressor de rotativa", + "766225": "Impressor de rotogravura", + "766230": "Impressor digital", + "766235": "Impressor flexográfico", + "766240": "Impressor letterset", + "766245": "Impressor tampográfico", + "766250": "Impressor tipográfico", + "766305": "Acabador de embalagens (flexíveis e cartotécnicas)", + "766310": "Impressor de corte e vinco", + "766315": "Operador de acabamento (indústria gráfica)", + "766320": "Operador de guilhotina (corte de papel)", + "766325": "Preparador de matrizes de corte e vinco", + "766405": "Laboratorista fotográfico", + "766410": "Revelador de filmes fotográficos", + "766415": "Revelador de filmes fotográficos", + "766420": "Auxiliar de radiologia (revelação fotográfica)", + "768105": "Tecelão (tear manual)", + "768110": "Tecelão de tapetes", + "768115": "Tricoteiro", + "768120": "Redeiro", + "768125": "Chapeleiro (chapéus de palha)", + "768130": "Crocheteiro", + "768205": "Bordador", + "768210": "Cerzidor", + "768305": "Artífice do couro", + "768310": "Cortador de calçados", + "768315": "Costurador de artefatos de couro", + "768320": "Sapateiro (calçados sob medida)", + "768325": "Seleiro", + "768605": "Tipógrafo", + "768610": "Linotipista", + "768615": "Monotipista", + "768620": "Paginador", + "768625": "Pintor de letreiros", + "768630": "Confeccionador de carimbos de borracha", + "768705": "Gravador", + "768710": "Restaurador de livros", + "770105": "Mestre (indústria de madeira e mobiliário)", + "770110": "Mestre carpinteiro", + "771105": "Marceneiro", + "771110": "Modelador de madeira", + "771115": "Maquetista na marcenaria", + "771120": "Tanoeiro", + "772105": "Classificador de madeira", + "772110": "Impregnador de madeira", + "772115": "Secador de madeira", + "773105": "Cortador de laminados de madeira", + "773110": "Operador de serras no desdobramento de madeira", + "773115": "Serrador de bordas no desdobramento de madeira", + "773120": "Serrador de madeira", + "773125": "Serrador de madeira (serra circular múltipla)", + "773130": "Serrador de madeira (serra de fita múltipla)", + "773205": "Operador de máquina intercaladora e placas (compensados)", + "773210": "Prensista de aglomerados", + "773215": "Prensista de compensados", + "773220": "Preparador de aglomerantes", + "773305": "Operador de desempenadeira na usinagem convencional de madeira", + "773310": "Operador de entalhadeira (usinagem de madeira)", + "773315": "Operador de fresadora (usinagem de madeira)", + "773320": "Operador de lixadeira (usinagem de madeira)", + "773325": "Operador de máquina de usinagem madeira", + "773330": "Operador de molduradora (usinagem de madeira)", + "773335": "Operador de plaina desengrossadeira", + "773340": "Operador de serras (usinagem de madeira)", + "773345": "Operador de torno automático (usinagem de madeira)", + "773350": "Operador de tupia (usinagem de madeira)", + "773355": "Torneiro na usinagem convencional de madeira", + "773405": "Operador de máquina bordatriz", + "773410": "Operador de máquina de cortina d#água (produção de móveis)", + "773415": "Operador de máquina de usinagem de madeira (produção em série)", + "773420": "Operador de prensa de alta freqüência na usinagem de madeira", + "773505": "Operador de centro de usinagem de madeira (cnc)", + "773510": "Operador de máquinas de usinar madeira (cnc)", + "774105": "Montador de móveis e artefatos de madeira", + "775105": "Entalhador de madeira", + "775110": "Folheador de móveis de madeira", + "775115": "Lustrador de peças de madeira", + "775120": "Marcheteiro", + "776405": "Cesteiro", + "776410": "Confeccionador de escovas", + "776415": "Confeccionador de escovas", + "776420": "Confeccionador de móveis de vime", + "776425": "Esteireiro", + "776430": "Vassoureiro", + "777105": "Carpinteiro naval (construção de pequenas embarcações)", + "777110": "Carpinteiro naval (embarcações)", + "777115": "Carpinteiro naval (estaleiros)", + "777205": "Carpinteiro de carretas", + "777210": "Carpinteiro de carrocerias", + "780105": "Supervisor de embalagem e etiquetagem", + "781105": "Condutor de processos robotizados de pintura", + "781110": "Condutor de processos robotizados de soldagem", + "781305": "Operador de veículos subaquáticos controlados remotamente", + "781705": "Mergulhador profissional (raso e profundo)", + "782105": "Operador de draga", + "782110": "Operador de guindaste (fixo)", + "782115": "Operador de guindaste móvel", + "782120": "Operador de máquina rodoferroviária", + "782125": "Operador de monta-cargas (construção civil)", + "782130": "Operador de ponte rolante", + "782135": "Operador de pórtico rolante", + "782140": "Operador de talha elétrica", + "782145": "Sinaleiro (ponte-rolante)", + "782205": "Guincheiro (construção civil)", + "782210": "Operador de docagem", + "782220": "Operador de empilhadeira", + "782305": "Motorista de carro de passeio", + "782310": "Motorista de furgão ou veículo similar", + "782315": "Motorista de táxi", + "782410": "Motorista de ônibus urbano", + "782415": "Motorista de trólebus", + "782505": "Caminhoneiro autônomo (rotas regionais e internacionais)", + "782510": "Motorista de caminhão (rotas regionais e internacionais)", + "782515": "Motorista operacional de guincho", + "782605": "Operador de trem de metrô", + "782610": "Maquinista de trem", + "782615": "Maquinista de trem metropolitano", + "782620": "Motorneiro", + "782625": "Auxiliar de maquinista de trem", + "782630": "Operador de teleférico (passageiros)", + "782705": "Marinheiro de convés (marítimo e fluviário)", + "782710": "Marinheiro de máquinas", + "782715": "Moço de convés (marítimo e fluviário)", + "782720": "Moço de máquinas (marítimo e fluviário)", + "782725": "Marinheiro de esporte e recreio", + "782805": "Condutor de veículos de tração animal (ruas e estradas)", + "782810": "Tropeiro", + "782815": "Boiadeiro", + "782820": "Condutor de veículos a pedais", + "783105": "Agente de pátio", + "783110": "Manobrador", + "783205": "Carregador (aeronaves)", + "783210": "Carregador (armazém)", + "783215": "Carregador (veículos de transportes terrestres)", + "783220": "Estivador", + "783225": "Ajudante de motorista", + "783230": "Bloqueiro (trabalhador portuário)", + "784105": "Embalador", + "784110": "Embalador", + "784115": "Operador de máquina de etiquetar", + "784120": "Operador de máquina de envasar líquidos", + "784125": "Operador de prensa de enfardamento", + "784205": "Alimentador de linha de produção", + "791105": "Artesão bordador", + "791110": "Artesão ceramista", + "791115": "Artesão com material reciclável", + "791120": "Artesão confeccionador de biojóias e ecojóias", + "791125": "Artesão do couro", + "791130": "Artesão escultor", + "791135": "Artesão moveleiro (exceto reciclado)", + "791140": "Artesão tecelão", + "791145": "Artesão trançador", + "791150": "Artesão crocheteiro", + "791155": "Artesão tricoteiro", + "791160": "Artesão rendeiro", + "810105": "Mestre (indústria petroquímica e carboquímica)", + "810110": "Mestre de produção química", + "810205": "Mestre (indústria de borracha e plástico)", + "810305": "Mestre de produção farmacêutica", + "811005": "Operador de processos químicos e petroquímicos", + "811010": "Operador de sala de controle de instalações químicas", + "811105": "Moleiro (tratamentos químicos e afins)", + "811110": "Operador de máquina misturadeira (tratamentos químicos e afins)", + "811115": "Operador de britadeira (tratamentos químicos e afins)", + "811120": "Operador de concentração", + "811125": "Trabalhador da fabricação de resinas e vernizes", + "811130": "Trabalhador de fabricação de tintas", + "811205": "Operador de calcinação (tratamento químico e afins)", + "811215": "Operador de tratamento químico de materiais radioativos", + "811305": "Operador de centrifugadora (tratamentos químicos e afins)", + "811310": "Operador de exploração de petróleo", + "811315": "Operador de filtro de secagem (mineração)", + "811320": "Operador de filtro de tambor rotativo (tratamentos químicos e afins)", + "811325": "Operador de filtro-esteira (mineração)", + "811330": "Operador de filtro-prensa (tratamentos químicos e afins)", + "811335": "Operador de filtros de parafina (tratamentos químicos e afins)", + "811405": "Destilador de madeira", + "811410": "Destilador de produtos químicos (exceto petróleo)", + "811415": "Operador de alambique de funcionamento contínuo (produtos químicos", + "811420": "Operador de aparelho de reação e conversão (produtos químicos", + "811425": "Operador de equipamento de destilação de álcool", + "811430": "Operador de evaporador na destilação", + "811505": "Operador de painel de controle (refinação de petróleo)", + "811510": "Operador de transferência e estocagem - na refinação do petróleo", + "811605": "Operador de britador de coque", + "811610": "Operador de carro de apagamento e coque", + "811615": "Operador de destilação e subprodutos de coque", + "811620": "Operador de enfornamento e desenfornamento de coque", + "811625": "Operador de exaustor (coqueria)", + "811630": "Operador de painel de controle", + "811635": "Operador de preservação e controle térmico", + "811640": "Operador de reator de coque de petróleo", + "811645": "Operador de refrigeração (coqueria)", + "811650": "Operador de sistema de reversão (coqueria)", + "811705": "Bamburista", + "811710": "Calandrista de borracha", + "811715": "Confeccionador de pneumáticos", + "811725": "Confeccionador de velas por imersão", + "811735": "Confeccionador de velas por moldagem", + "811745": "Laminador de plástico", + "811750": "Moldador de borracha por compressão", + "811760": "Moldador de plástico por compressão", + "811770": "Moldador de plástico por injeção", + "811775": "Trefilador de borracha", + "811805": "Operador de máquina de produtos farmacêuticos", + "811810": "Drageador (medicamentos)", + "811815": "Operador de máquina de fabricação de cosméticos", + "811820": "Operador de máquina de fabricação de produtos de higiene e limpeza (sabão", + "812105": "Pirotécnico", + "812110": "Trabalhador da fabricação de munição e explosivos", + "813105": "Cilindrista (petroquímica e afins)", + "813110": "Operador de calandra (química", + "813115": "Operador de extrusora (química", + "813120": "Operador de processo (química", + "813125": "Operador de produção (química", + "813130": "Técnico de operação (química", + "818105": "Assistente de laboratório industrial", + "818110": "Auxiliar de laboratório de análises físico-químicas", + "820105": "Mestre de siderurgia", + "820110": "Mestre de aciaria", + "820115": "Mestre de alto-forno", + "820120": "Mestre de forno elétrico", + "820125": "Mestre de laminação", + "820205": "Supervisor de fabricação de produtos cerâmicos", + "820210": "Supervisor de fabricação de produtos de vidro", + "821105": "Operador de centro de controle", + "821110": "Operador de máquina de sinterizar", + "821205": "Forneiro e operador (alto-forno)", + "821210": "Forneiro e operador (conversor a oxigênio)", + "821215": "Forneiro e operador (forno elétrico)", + "821220": "Forneiro e operador (refino de metais não-ferrosos)", + "821225": "Forneiro e operador de forno de redução direta", + "821230": "Operador de aciaria (basculamento de convertedor)", + "821235": "Operador de aciaria (dessulfuração de gusa)", + "821240": "Operador de aciaria (recebimento de gusa)", + "821245": "Operador de área de corrida", + "821250": "Operador de desgaseificação", + "821255": "Soprador de convertedor", + "821305": "Operador de laminador", + "821310": "Operador de laminador de barras a frio", + "821315": "Operador de laminador de barras a quente", + "821320": "Operador de laminador de metais não-ferrosos", + "821325": "Operador de laminador de tubos", + "821330": "Operador de montagem de cilindros e mancais", + "821335": "Recuperador de guias e cilindros", + "821405": "Encarregado de acabamento de chapas e metais (têmpera)", + "821410": "Escarfador", + "821415": "Marcador de produtos (siderúrgico e metalúrgico)", + "821420": "Operador de bobinadeira de tiras a quente", + "821425": "Operador de cabine de laminação (fio-máquina)", + "821430": "Operador de escória e sucata", + "821435": "Operador de jato abrasivo", + "821440": "Operador de tesoura mecânica e máquina de corte", + "821445": "Preparador de sucata e aparas", + "821450": "Rebarbador de metal", + "822105": "Forneiro de cubilô", + "822110": "Forneiro de forno-poço", + "822115": "Forneiro de fundição (forno de redução)", + "822120": "Forneiro de reaquecimento e tratamento térmico na metalurgia", + "822125": "Forneiro de revérbero", + "823105": "Preparador de massa (fabricação de abrasivos)", + "823110": "Preparador de massa (fabricação de vidro)", + "823115": "Preparador de massa de argila", + "823120": "Preparador de barbotina", + "823125": "Preparador de esmaltes (cerâmica)", + "823130": "Preparador de aditivos", + "823135": "Operador de atomizador", + "823210": "Extrusor de fios ou fibras de vidro", + "823215": "Forneiro na fundição de vidro", + "823220": "Forneiro no recozimento de vidro", + "823230": "Moldador de abrasivos na fabricação de cerâmica", + "823235": "Operador de banho metálico de vidro por flutuação", + "823240": "Operador de máquina de soprar vidro", + "823245": "Operador de máquina extrusora de varetas e tubos de vidro", + "823250": "Operador de prensa de moldar vidro", + "823255": "Temperador de vidro", + "823265": "Trabalhador na fabricação de produtos abrasivos", + "823305": "Classificador e empilhador de tijolos refratários", + "823315": "Forneiro (materiais de construção)", + "823320": "Trabalhador da elaboração de pré-fabricados (cimento amianto)", + "823325": "Trabalhador da elaboração de pré-fabricados (concreto armado)", + "823330": "Trabalhador da fabricação de pedras artificiais", + "828105": "Oleiro (fabricação de telhas)", + "828110": "Oleiro (fabricação de tijolos)", + "830105": "Mestre (indústria de celulose", + "831105": "Cilindreiro na preparação de pasta para fabricação de papel", + "831110": "Operador de branqueador de pasta para fabricação de papel", + "831115": "Operador de digestor de pasta para fabricação de papel", + "831120": "Operador de lavagem e depuração de pasta para fabricação de papel", + "831125": "Operador de máquina de secar celulose", + "832105": "Calandrista de papel", + "832110": "Operador de cortadeira de papel", + "832115": "Operador de máquina de fabricar papel (fase úmida)", + "832120": "Operador de máquina de fabricar papel (fase seca)", + "832125": "Operador de máquina de fabricar papel e papelão", + "832135": "Operador de rebobinadeira na fabricação de papel e papelão", + "833105": "Cartonageiro", + "833110": "Confeccionador de bolsas", + "833115": "Confeccionador de sacos de celofane", + "833120": "Operador de máquina de cortar e dobrar papelão", + "833125": "Operador de prensa de embutir papelão", + "833205": "Cartonageiro", + "840105": "Supervisor de produção da indústria alimentícia", + "840110": "Supervisor da indústria de bebidas", + "840115": "Supervisor da indústria de fumo", + "840120": "Chefe de confeitaria", + "841105": "Moleiro de cereais (exceto arroz)", + "841110": "Moleiro de especiarias", + "841115": "Operador de processo de moagem", + "841205": "Moedor de sal", + "841210": "Refinador de sal", + "841305": "Operador de cristalização na refinação de açucar", + "841310": "Operador de equipamentos de refinação de açúcar (processo contínuo)", + "841315": "Operador de moenda na fabricação de açúcar", + "841320": "Operador de tratamento de calda na refinação de açúcar", + "841408": "Cozinhador (conservação de alimentos)", + "841416": "Cozinhador de carnes", + "841420": "Cozinhador de frutas e legumes", + "841428": "Cozinhador de pescado", + "841432": "Desidratador de alimentos", + "841440": "Esterilizador de alimentos", + "841444": "Hidrogenador de óleos e gorduras", + "841448": "Lagareiro", + "841456": "Operador de câmaras frias", + "841460": "Operador de preparação de grãos vegetais (óleos e gorduras)", + "841464": "Prensador de frutas (exceto oleaginosas)", + "841468": "Preparador de rações", + "841472": "Refinador de óleo e gordura", + "841476": "Trabalhador de fabricação de margarina", + "841484": "Trabalhador de preparação de pescados (limpeza)", + "841505": "Trabalhador de tratamento do leite e fabricação de laticínios e afins", + "841605": "Misturador de café", + "841610": "Torrador de café", + "841615": "Moedor de café", + "841620": "Operador de extração de café solúvel", + "841625": "Torrador de cacau", + "841630": "Misturador de chá ou mate", + "841705": "Alambiqueiro", + "841710": "Filtrador de cerveja", + "841715": "Fermentador", + "841720": "Trabalhador de fabricação de vinhos", + "841725": "Malteiro (germinação)", + "841730": "Cozinhador de malte", + "841735": "Dessecador de malte", + "841740": "Vinagreiro", + "841745": "Xaropeiro", + "841805": "Operador de forno (fabricação de paes", + "841810": "Operador de máquinas de fabricação de doces", + "841815": "Operador de máquinas de fabricação de chocolates e achocolatados", + "842105": "Preparador de melado e essência de fumo", + "842110": "Processador de fumo", + "842115": "Classificador de fumo", + "842120": "Auxiliar de processamento de fumo", + "842125": "Operador de máquina de fabricar cigarros", + "842135": "Operador de máquina de preparação de matéria prima para produção de cigarros", + "842205": "Preparador de fumo na fabricação de charutos", + "842210": "Operador de máquina de fabricar charutos e cigarrilhas", + "842215": "Classificador de charutos", + "842220": "Cortador de charutos", + "842225": "Celofanista na fabricação de charutos", + "842230": "Charuteiro a mão", + "842235": "Degustador de charutos", + "848105": "Defumador de carnes e pescados", + "848110": "Salgador de alimentos", + "848115": "Salsicheiro (fabricação de lingüiça", + "848205": "Pasteurizador", + "848210": "Queijeiro na fabricação de laticínio", + "848215": "Manteigueiro na fabricação de laticínio", + "848305": "Padeiro", + "848310": "Confeiteiro", + "848315": "Masseiro (massas alimentícias)", + "848325": "Trabalhador de fabricação de sorvete", + "848405": "Degustador de café", + "848410": "Degustador de chá", + "848415": "Degustador de derivados de cacau", + "848420": "Degustador de vinhos ou licores", + "848425": "Classificador de grãos", + "848505": "Abatedor", + "848510": "Açougueiro", + "848515": "Desossador", + "848520": "Magarefe", + "848525": "Retalhador de carne", + "848605": "Trabalhador do beneficiamento de fumo", + "860105": "Supervisor de manutenção eletromecânica (utilidades)", + "860110": "Supervisor de operação de fluidos (distribuição", + "860115": "Supervisor de operação elétrica (geração", + "861105": "Operador de central hidrelétrica", + "861110": "Operador de quadro de distribuição de energia elétrica", + "861115": "Operador de central termoelétrica", + "861120": "Operador de reator nuclear", + "861205": "Operador de subestação", + "862105": "Foguista (locomotivas a vapor)", + "862110": "Maquinista de embarcações", + "862115": "Operador de bateria de gás de hulha", + "862120": "Operador de caldeira", + "862130": "Operador de compressor de ar", + "862140": "Operador de estação de bombeamento", + "862150": "Operador de máquinas fixas", + "862155": "Operador de utilidade (produção e distribuição de vapor", + "862205": "Operador de estação de captação", + "862305": "Operador de estação de tratamento de água e efluentes", + "862310": "Operador de forno de incineração no tratamento de água", + "862405": "Operador de instalação de extração", + "862505": "Operador de instalação de refrigeração", + "862510": "Operador de refrigeração com amônia", + "862515": "Operador de instalação de ar-condicionado", + "910105": "Encarregado de manutenção mecânica de sistemas operacionais", + "910110": "Supervisor de manutenção de aparelhos térmicos", + "910115": "Supervisor de manutenção de bombas", + "910120": "Supervisor de manutenção de máquinas gráficas", + "910125": "Supervisor de manutenção de máquinas industriais têxteis", + "910130": "Supervisor de manutenção de máquinas operatrizes e de usinagem", + "910205": "Supervisor da manutenção e reparação de veículos leves", + "910210": "Supervisor da manutenção e reparação de veículos pesados", + "910905": "Supervisor de reparos linhas férreas", + "910910": "Supervisor de manutenção de vias férreas", + "911105": "Mecânico de manutenção de bomba injetora (exceto de veículos automotores)", + "911110": "Mecânico de manutenção de bombas", + "911115": "Mecânico de manutenção de compressores de ar", + "911120": "Mecânico de manutenção de motores diesel (exceto de veículos automotores)", + "911125": "Mecânico de manutenção de redutores", + "911130": "Mecânico de manutenção de turbinas (exceto de aeronaves)", + "911135": "Mecânico de manutenção de turbocompressores", + "911205": "Mecânico de manutenção e instalação de aparelhos de climatização e refrigeração", + "911305": "Mecânico de manutenção de máquinas", + "911310": "Mecânico de manutenção de máquinas gráficas", + "911315": "Mecânico de manutenção de máquinas operatrizes (lavra de madeira)", + "911320": "Mecânico de manutenção de máquinas têxteis", + "911325": "Mecânico de manutenção de máquinas-ferramentas (usinagem de metais)", + "913105": "Mecânico de manutenção de aparelhos de levantamento", + "913110": "Mecânico de manutenção de equipamento de mineração", + "913115": "Mecânico de manutenção de máquinas agrícolas", + "913120": "Mecânico de manutenção de máquinas de construção e terraplenagem", + "914105": "Mecânico de manutenção de aeronaves", + "914110": + "Mecânico de manutenção de sistema hidráulico de aeronaves (serviços de pista e hangar)", + "914205": "Mecânico de manutenção de motores e equipamentos navais", + "914305": "Mecânico de manutenção de veículos ferroviários", + "914405": "Mecânico de manutenção de automóveis", + "914410": "Mecânico de manutenção de empilhadeiras e outros veículos de cargas leves", + "914415": "Mecânico de manutenção de motocicletas", + "914420": "Mecânico de manutenção de tratores", + "914425": "Mecânico de veículos automotores a diesel (exceto tratores)", + "915105": "Técnico em manutenção de instrumentos de medição e precisão", + "915110": "Técnico em manutenção de hidrômetros", + "915115": "Técnico em manutenção de balanças", + "915205": "Restaurador de instrumentos musicais (exceto cordas arcadas)", + "915210": "Reparador de instrumentos musicais", + "915215": "Luthier (restauração de cordas arcadas)", + "915305": "Técnico em manutenção de equipamentos e instrumentos médico-hospitalares", + "915405": "Reparador de equipamentos fotográficos", + "919105": "Lubrificador industrial", + "919110": "Lubrificador de veículos automotores (exceto embarcações)", + "919115": "Lubrificador de embarcações", + "919205": "Mecânico de manutenção de máquinas cortadoras de grama", + "919305": "Mecânico de manutenção de aparelhos esportivos e de ginástica", + "919310": "Mecânico de manutenção de bicicletas e veículos similares", + "919315": "Montador de bicicletas", + "950105": "Supervisor de manutenção elétrica de alta tensão industrial", + "950110": "Supervisor de manutenção eletromecânica industrial", + "950205": "Encarregado de manutenção elétrica de veículos", + "950305": "Supervisor de manutenção eletromecânica", + "951105": "Eletricista de manutenção eletroeletrônica", + "951305": "Instalador de sistemas eletroeletrônicos de segurança", + "951310": "Mantenedor de sistemas eletroeletrônicos de segurança", + "953105": "Eletricista de instalações (aeronaves)", + "953110": "Eletricista de instalações (embarcações)", + "953115": "Eletricista de instalações (veículos automotores e máquinas operatrizes", + "954105": "Eletromecânico de manutenção de elevadores", + "954110": "Eletromecânico de manutenção de escadas rolantes", + "954115": "Eletromecânico de manutenção de portas automáticas", + "954120": "Mecânico de manutenção de instalações mecânicas de edifícios", + "954125": "Operador eletromecânico", + "954205": "Reparador de aparelhos eletrodomésticos (exceto imagem e som)", + "954210": "Reparador de rádio", + "954305": "Reparador de equipamentos de escritório", + "991105": "Conservador de via permanente (trilhos)", + "991110": "Inspetor de via permanente (trilhos)", + "991115": "Operador de máquinas especiais em conservação de via permanente (trilhos)", + "991120": "Soldador aluminotérmico em conservação de trilhos", + "991205": "Mantenedor de equipamentos de parques de diversões e similares", + "991305": "Funileiro de veículos (reparação)", + "991310": "Montador de veículos (reparação)", + "991315": "Pintor de veículos (reparação)", + "992105": "Alinhador de pneus", + "992110": "Balanceador", + "992115": "Borracheiro", + "992120": "Lavador de peças", + "992205": "Encarregado geral de operações de conservação de vias permanentes (exceto trilhos)", + "992210": "Encarregado de equipe de conservação de vias permanentes (exceto trilhos)", + "992215": "Operador de ceifadeira na conservação de vias permanentes", + "992220": "Pedreiro de conservação de vias permanentes (exceto trilhos)", + "992225": "Auxiliar geral de conservação de vias permanentes (exceto trilhos)", + "010105": "Oficial General da Aeronáutica", + "010110": "Oficial General do Exército", + "010115": "Oficial General da Marinha", + "010205": "Oficial da Aeronáutica", + "010210": "Oficial do Exército", + "010215": "Oficial da Marinha", + "010305": "Praça da Aeronáutica", + "010310": "Praça do Exército", + "010315": "Praça da Marinha", + "020105": "Coronel da Polícia Militar", + "020110": "Tenente-Coronel da Polícia Militar", + "020115": "Major da Polícia Militar", + "020205": "Capitão da Polícia Militar", + "020305": "Primeiro Tenente de Polícia Militar", + "020310": "Segundo Tenente de Polícia Militar", + "021105": "Subtenente da Polícia Militar", + "021205": "Cabo da Polícia Militar", + "021210": "Soldado da Polícia Militar", + "030105": "Coronel Bombeiro Militar", + "030110": "Major Bombeiro Militar", + "030115": "Tenente-Coronel Bombeiro Militar", + "030205": "Capitão Bombeiro Militar", + "030305": "Tenente do Corpo de Bombeiros Militar", + "031105": "Subtenente Bombeiro Militar", + "031110": "Sargento Bombeiro Militar", + "031205": "Cabo Bombeiro Militar", + "031210": "Soldado Bombeiro Militar", +}; diff --git a/src/get-cbo/get-cbo.test.ts b/src/get-cbo/get-cbo.test.ts new file mode 100644 index 00000000..39134e05 --- /dev/null +++ b/src/get-cbo/get-cbo.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getCbo } from "./get-cbo"; + +describe("getCbo", () => { + it("should return the occupation for a code without a mask", () => { + expect(getCbo("212405")).toEqual({ + code: "212405", + title: "Analista de desenvolvimento de sistemas", + }); + }); + + it("should return the occupation for a code with the hyphen mask", () => { + expect(getCbo("2124-05")).toEqual({ + code: "212405", + title: "Analista de desenvolvimento de sistemas", + }); + }); + + it("should return the occupation for a code given as a number", () => { + expect(getCbo(212405)).toEqual({ + code: "212405", + title: "Analista de desenvolvimento de sistemas", + }); + }); + + it("should return a fresh object that does not leak the internal table", () => { + const first = getCbo("212405"); + const second = getCbo("212405"); + expect(first).not.toBe(second); + }); + + it("should return null for an unknown six digit code", () => { + expect(getCbo("000000")).toBeNull(); + }); + + it("should return null when the digit count is not six", () => { + expect(getCbo("21240")).toBeNull(); + expect(getCbo("2124055")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getCbo("")).toBeNull(); + }); + + it("should return null for null and undefined", () => { + // @ts-expect-error not a string or number + expect(getCbo(null)).toBeNull(); + // @ts-expect-error not a string or number + expect(getCbo(undefined)).toBeNull(); + }); + + it("should return null for whitespace only", () => { + expect(getCbo(" ")).toBeNull(); + }); +}); diff --git a/src/get-cbo/get-cbo.ts b/src/get-cbo/get-cbo.ts new file mode 100644 index 00000000..73380dd1 --- /dev/null +++ b/src/get-cbo/get-cbo.ts @@ -0,0 +1,41 @@ +import { CBO_TITLES } from "../_internals/constants/cbo"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * A CBO (Classificação Brasileira de Ocupações) occupation. + */ +export type Cbo = { + /** The 6 digit occupation code, without the hyphen mask. */ + code: string; + /** The official occupation title. */ + title: string; +}; + +/** + * Looks a CBO (Classificação Brasileira de Ocupações) code up in the official CBO 2002 + * table. + * + * @param {string|number} value - The CBO code to look up, with or without the hyphen + * mask, e.g. `"2124-05"`, `"212405"` or `212405`. + * @returns {Cbo|null} The matching occupation, or null when the code is unknown or invalid. + * + * @example + * ```typescript + * getCbo("2124-05"); // { code: "212405", title: "Analista de desenvolvimento de sistemas" } + * getCbo("999999"); // null + * ``` + * + * @see Official: http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf + * @see Based on: https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json + * Community mirror of the official table used to build `CBO_TITLES`. + */ +export const getCbo = (value: string | number): Cbo | null => { + if (isNullish(value) || value === "") return null; + + const digits = sanitizeToDigits(value); + + if (digits.length !== 6 || !(digits in CBO_TITLES)) return null; + + return { code: digits, title: CBO_TITLES[digits] }; +}; diff --git a/src/is-valid-cbo/is-valid-cbo.test.ts b/src/is-valid-cbo/is-valid-cbo.test.ts new file mode 100644 index 00000000..7943d4e6 --- /dev/null +++ b/src/is-valid-cbo/is-valid-cbo.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidCbo } from "./is-valid-cbo"; + +describe("isValidCbo", () => { + it("should validate a CBO code without a mask", () => { + expect(isValidCbo("212405")).toBe(true); + }); + + it("should validate a CBO code with the hyphen mask", () => { + expect(isValidCbo("2124-05")).toBe(true); + }); + + it("should validate a CBO code given as a number", () => { + expect(isValidCbo(212405)).toBe(true); + }); + + it("should validate a CBO code with surrounding whitespace", () => { + expect(isValidCbo(" 212405 ")).toBe(true); + }); + + it("should return false for an unknown six digit code", () => { + expect(isValidCbo("000000")).toBe(false); + }); + + it("should return false when the digit count is not six", () => { + expect(isValidCbo("21240")).toBe(false); + expect(isValidCbo("2124055")).toBe(false); + }); + + it("should return false for an empty string", () => { + expect(isValidCbo("")).toBe(false); + }); + + it("should return false for null and undefined", () => { + // @ts-expect-error not a string or number + expect(isValidCbo(null)).toBe(false); + // @ts-expect-error not a string or number + expect(isValidCbo(undefined)).toBe(false); + }); + + it("should return false for whitespace only", () => { + expect(isValidCbo(" ")).toBe(false); + }); + + it("should return false for a non numeric string", () => { + expect(isValidCbo("abcdef")).toBe(false); + }); +}); diff --git a/src/is-valid-cbo/is-valid-cbo.ts b/src/is-valid-cbo/is-valid-cbo.ts new file mode 100644 index 00000000..99234925 --- /dev/null +++ b/src/is-valid-cbo/is-valid-cbo.ts @@ -0,0 +1,31 @@ +import { CBO_TITLES } from "../_internals/constants/cbo"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Validates if a CBO (Classificação Brasileira de Ocupações) code exists in the official + * CBO 2002 table. + * + * @param {string|number} value - The CBO code to be validated, with or without the hyphen + * mask, e.g. `"2124-05"`, `"212405"` or `212405`. + * @returns {boolean} True when the code is a known 6 digit occupation code, false otherwise. + * + * @example + * ```typescript + * isValidCbo("2124-05"); // true + * isValidCbo("212405"); // true + * isValidCbo(212405); // true + * isValidCbo("999999"); // false + * ``` + * + * @see Official: http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf + * @see Based on: https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json + * Community mirror of the official table used to build `CBO_TITLES`. + */ +export const isValidCbo = (value: string | number): boolean => { + if (isNullish(value) || value === "") return false; + + const digits = sanitizeToDigits(value); + + return digits.length === 6 && digits in CBO_TITLES; +}; From 5e3b679becff011b12f140a87a7b4ac6333f2548 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:54 -0300 Subject: [PATCH 17/22] feat(cnae): add formatCnae, getCnae and isValidCnae NNNN-N/NN CNAE (CONCLA) formatting/lookup/validation against a table with no check digit, generated by scripts/cnae.ts. --- scripts/cnae.ts | 54 + src/_internals/constants/cnae.ts | 1520 +++++++++++++++++++++++ src/format-cnae/format-cnae.test.ts | 31 + src/format-cnae/format-cnae.ts | 30 + src/get-cnae/get-cnae.test.ts | 50 + src/get-cnae/get-cnae.ts | 40 + src/is-valid-cnae/is-valid-cnae.test.ts | 48 + src/is-valid-cnae/is-valid-cnae.ts | 29 + 8 files changed, 1802 insertions(+) create mode 100644 scripts/cnae.ts create mode 100644 src/_internals/constants/cnae.ts create mode 100644 src/format-cnae/format-cnae.test.ts create mode 100644 src/format-cnae/format-cnae.ts create mode 100644 src/get-cnae/get-cnae.test.ts create mode 100644 src/get-cnae/get-cnae.ts create mode 100644 src/is-valid-cnae/is-valid-cnae.test.ts create mode 100644 src/is-valid-cnae/is-valid-cnae.ts diff --git a/scripts/cnae.ts b/scripts/cnae.ts new file mode 100644 index 00000000..4d63850f --- /dev/null +++ b/scripts/cnae.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +import { writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; + +const scriptsDir = dirname(fileURLToPath(import.meta.url)); + +type CnaeSubclass = { + id: string; + descricao: string; +}; + +const main = async () => { + const response = await fetchWithRetry("https://servicodados.ibge.gov.br/api/v2/cnae/subclasses"); + + if (!response.ok) { + throw new Error(`IBGE CNAE request failed with status ${response.status}`); + } + + const json: CnaeSubclass[] = await response.json(); + + const entries = json + .filter((subclass) => /^\d{7}$/.test(subclass.id)) + .sort((subclassA, subclassB) => (subclassA.id > subclassB.id ? 1 : -1)) + .map((subclass) => [subclass.id, subclass.descricao] as const); + + const data: Record = {}; + for (const [id, descricao] of entries) { + data[id] = descricao; + } + + await writeFile( + resolve(scriptsDir, "..", "./src/_internals/constants/cnae.ts"), + `/** + * CNAE 2.3 (Classificação Nacional de Atividades Econômicas) subclasses, indexed by the + * raw 7 digit code, mapping to the official subclass description. + * + * Generated by \`node ./scripts/cnae.ts\`. Do not edit by hand. + * + * @see https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + * @see https://concla.ibge.gov.br/classificacoes/por-tema/atividades-economicas/classificacao-nacional-de-atividades-economicas + */ +export const CNAE_SUBCLASSES: Record = ${JSON.stringify(data)}; +`, + ); +}; + +await main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +}); diff --git a/src/_internals/constants/cnae.ts b/src/_internals/constants/cnae.ts new file mode 100644 index 00000000..53d21bc6 --- /dev/null +++ b/src/_internals/constants/cnae.ts @@ -0,0 +1,1520 @@ +/** + * CNAE 2.3 (Classificação Nacional de Atividades Econômicas) subclasses, indexed by the + * raw 7 digit code, mapping to the official subclass description. + * + * Generated by `node ./scripts/cnae.ts`. Do not edit by hand. + * + * @see https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + * @see https://concla.ibge.gov.br/classificacoes/por-tema/atividades-economicas/classificacao-nacional-de-atividades-economicas + */ +export const CNAE_SUBCLASSES: Record = { + "1011201": "FRIGORÍFICO - ABATE DE BOVINOS", + "1011202": "FRIGORÍFICO - ABATE DE EQÜINOS", + "1011203": "FRIGORÍFICO - ABATE DE OVINOS E CAPRINOS", + "1011204": "FRIGORÍFICO - ABATE DE BUFALINOS", + "1011205": "MATADOURO - ABATE DE RESES SOB CONTRATO - EXCETO ABATE DE SUÍNOS", + "1012101": "ABATE DE AVES", + "1012102": "ABATE DE PEQUENOS ANIMAIS", + "1012103": "FRIGORÍFICO - ABATE DE SUÍNOS", + "1012104": "MATADOURO - ABATE DE SUÍNOS SOB CONTRATO", + "1013901": "FABRICAÇÃO DE PRODUTOS DE CARNE", + "1013902": "PREPARAÇÃO DE SUBPRODUTOS DO ABATE", + "1020101": "PRESERVAÇÃO DE PEIXES, CRUSTÁCEOS E MOLUSCOS", + "1020102": "FABRICAÇÃO DE CONSERVAS DE PEIXES, CRUSTÁCEOS E MOLUSCOS", + "1031700": "FABRICAÇÃO DE CONSERVAS DE FRUTAS", + "1032501": "FABRICAÇÃO DE CONSERVAS DE PALMITO", + "1032599": "FABRICAÇÃO DE CONSERVAS DE LEGUMES E OUTROS VEGETAIS, EXCETO PALMITO", + "1033301": "FABRICAÇÃO DE SUCOS CONCENTRADOS DE FRUTAS, HORTALIÇAS E LEGUMES", + "1033302": "FABRICAÇÃO DE SUCOS DE FRUTAS, HORTALIÇAS E LEGUMES, EXCETO CONCENTRADOS", + "1041400": "FABRICAÇÃO DE ÓLEOS VEGETAIS EM BRUTO, EXCETO ÓLEO DE MILHO", + "1042200": "FABRICAÇÃO DE ÓLEOS VEGETAIS REFINADOS, EXCETO ÓLEO DE MILHO", + "1043100": + "FABRICAÇÃO DE MARGARINA E OUTRAS GORDURAS VEGETAIS E DE ÓLEOS NÃO COMESTÍVEIS DE ANIMAIS", + "1051100": "PREPARAÇÃO DO LEITE", + "1052000": "FABRICAÇÃO DE LATICÍNIOS", + "1053800": "FABRICAÇÃO DE SORVETES E OUTROS GELADOS COMESTÍVEIS", + "1061901": "BENEFICIAMENTO DE ARROZ", + "1061902": "FABRICAÇÃO DE PRODUTOS DO ARROZ", + "1062700": "MOAGEM DE TRIGO E FABRICAÇÃO DE DERIVADOS", + "1063500": "FABRICAÇÃO DE FARINHA DE MANDIOCA E DERIVADOS", + "1064300": "FABRICAÇÃO DE FARINHA DE MILHO E DERIVADOS, EXCETO ÓLEOS DE MILHO", + "1065101": "FABRICAÇÃO DE AMIDOS E FÉCULAS DE VEGETAIS", + "1065102": "FABRICAÇÃO DE ÓLEO DE MILHO EM BRUTO", + "1065103": "FABRICAÇÃO DE ÓLEO DE MILHO REFINADO", + "1066000": "FABRICAÇÃO DE ALIMENTOS PARA ANIMAIS", + "1069400": "MOAGEM E FABRICAÇÃO DE PRODUTOS DE ORIGEM VEGETAL NÃO ESPECIFICADOS ANTERIORMENTE", + "1071600": "FABRICAÇÃO DE AÇÚCAR EM BRUTO", + "1072401": "FABRICAÇÃO DE AÇÚCAR DE CANA REFINADO", + "1072402": "FABRICAÇÃO DE AÇÚCAR DE CEREAIS (DEXTROSE) E DE BETERRABA", + "1081301": "BENEFICIAMENTO DE CAFÉ", + "1081302": "TORREFAÇÃO E MOAGEM DE CAFÉ", + "1082100": "FABRICAÇÃO DE PRODUTOS À BASE DE CAFÉ", + "1091101": "FABRICAÇÃO DE PRODUTOS DE PANIFICAÇÃO INDUSTRIAL", + "1091102": + "FABRICAÇÃO DE PRODUTOS DE PADARIA E CONFEITARIA COM PREDOMINÂNCIA DE PRODUÇÃO PRÓPRIA", + "1092900": "FABRICAÇÃO DE BISCOITOS E BOLACHAS", + "1093701": "FABRICAÇÃO DE PRODUTOS DERIVADOS DO CACAU E DE CHOCOLATES", + "1093702": "FABRICAÇÃO DE FRUTAS CRISTALIZADAS, BALAS E SEMELHANTES", + "1094500": "FABRICAÇÃO DE MASSAS ALIMENTÍCIAS", + "1095300": "FABRICAÇÃO DE ESPECIARIAS, MOLHOS, TEMPEROS E CONDIMENTOS", + "1096100": "FABRICAÇÃO DE ALIMENTOS E PRATOS PRONTOS", + "1099601": "FABRICAÇÃO DE VINAGRES", + "1099602": "FABRICAÇÃO DE PÓS ALIMENTÍCIOS", + "1099603": "FABRICAÇÃO DE FERMENTOS E LEVEDURAS", + "1099604": "FABRICAÇÃO DE GELO COMUM", + "1099605": "FABRICAÇÃO DE PRODUTOS PARA INFUSÃO (CHÁ, MATE, ETC.)", + "1099606": "FABRICAÇÃO DE ADOÇANTES NATURAIS E ARTIFICIAIS", + "1099607": "FABRICAÇÃO DE ALIMENTOS DIETÉTICOS E COMPLEMENTOS ALIMENTARES", + "1099699": "FABRICAÇÃO DE OUTROS PRODUTOS ALIMENTÍCIOS NÃO ESPECIFICADOS ANTERIORMENTE", + "1111901": "FABRICAÇÃO DE AGUARDENTE DE CANA DE AÇÚCAR", + "1111902": "FABRICAÇÃO DE OUTRAS AGUARDENTES E BEBIDAS DESTILADAS", + "1112700": "FABRICAÇÃO DE VINHO", + "1113501": "FABRICAÇÃO DE MALTE, INCLUSIVE MALTE UÍSQUE", + "1113502": "FABRICAÇÃO DE CERVEJAS E CHOPES", + "1121600": "FABRICAÇÃO DE ÁGUAS ENVASADAS", + "1122401": "FABRICAÇÃO DE REFRIGERANTES", + "1122402": "FABRICAÇÃO DE CHÁ MATE E OUTROS CHÁS PRONTOS PARA CONSUMO", + "1122403": "FABRICAÇÃO DE REFRESCOS, XAROPES E PÓS PARA REFRESCOS, EXCETO REFRESCOS DE FRUTAS", + "1122404": "FABRICAÇÃO DE BEBIDAS ISOTÔNICAS", + "1122499": "FABRICAÇÃO DE OUTRAS BEBIDAS NÃO ALCOÓLICAS NÃO ESPECIFICADAS ANTERIORMENTE", + "1210700": "PROCESSAMENTO INDUSTRIAL DO FUMO", + "1220401": "FABRICAÇÃO DE CIGARROS", + "1220402": "FABRICAÇÃO DE CIGARRILHAS E CHARUTOS", + "1220403": "FABRICAÇÃO DE FILTROS PARA CIGARROS", + "1220499": "FABRICAÇÃO DE OUTROS PRODUTOS DO FUMO, EXCETO CIGARROS, CIGARRILHAS E CHARUTOS", + "1311100": "PREPARAÇÃO E FIAÇÃO DE FIBRAS DE ALGODÃO", + "1312000": "PREPARAÇÃO E FIAÇÃO DE FIBRAS TÊXTEIS NATURAIS, EXCETO ALGODÃO", + "1313800": "FIAÇÃO DE FIBRAS ARTIFICIAIS E SINTÉTICAS", + "1314600": "FABRICAÇÃO DE LINHAS PARA COSTURAR E BORDAR", + "1321900": "TECELAGEM DE FIOS DE ALGODÃO", + "1322700": "TECELAGEM DE FIOS DE FIBRAS TÊXTEIS NATURAIS, EXCETO ALGODÃO", + "1323500": "TECELAGEM DE FIOS DE FIBRAS ARTIFICIAIS E SINTÉTICAS", + "1330800": "FABRICAÇÃO DE TECIDOS DE MALHA", + "1340501": "ESTAMPARIA E TEXTURIZAÇÃO EM FIOS, TECIDOS, ARTEFATOS TÊXTEIS E PEÇAS DO VESTUÁRIO", + "1340502": + "ALVEJAMENTO, TINGIMENTO E TORÇÃO EM FIOS, TECIDOS, ARTEFATOS TÊXTEIS E PEÇAS DO VESTUÁRIO", + "1340599": + "OUTROS SERVIÇOS DE ACABAMENTO EM FIOS, TECIDOS, ARTEFATOS TÊXTEIS E PEÇAS DO VESTUÁRIO", + "1351100": "FABRICAÇÃO DE ARTEFATOS TÊXTEIS PARA USO DOMÉSTICO", + "1352900": "FABRICAÇÃO DE ARTEFATOS DE TAPEÇARIA", + "1353700": "FABRICAÇÃO DE ARTEFATOS DE CORDOARIA", + "1354500": "FABRICAÇÃO DE TECIDOS ESPECIAIS, INCLUSIVE ARTEFATOS", + "1359600": "FABRICAÇÃO DE OUTROS PRODUTOS TÊXTEIS NÃO ESPECIFICADOS ANTERIORMENTE", + "1411801": "CONFECÇÃO DE ROUPAS ÍNTIMAS", + "1411802": "FACÇÃO DE ROUPAS ÍNTIMAS", + "1412601": + "CONFECÇÃO DE PEÇAS DE VESTUÁRIO, EXCETO ROUPAS ÍNTIMAS E AS CONFECCIONADAS SOB MEDIDA", + "1412602": "CONFECÇÃO, SOB MEDIDA, DE PEÇAS DO VESTUÁRIO, EXCETO ROUPAS ÍNTIMAS", + "1412603": "FACÇÃO DE PEÇAS DO VESTUÁRIO, EXCETO ROUPAS ÍNTIMAS", + "1413401": "CONFECÇÃO DE ROUPAS PROFISSIONAIS, EXCETO SOB MEDIDA", + "1413402": "CONFECÇÃO, SOB MEDIDA, DE ROUPAS PROFISSIONAIS", + "1413403": "FACÇÃO DE ROUPAS PROFISSIONAIS", + "1414200": "FABRICAÇÃO DE ACESSÓRIOS DO VESTUÁRIO, EXCETO PARA SEGURANÇA E PROTEÇÃO", + "1421500": "FABRICAÇÃO DE MEIAS", + "1422300": + "FABRICAÇÃO DE ARTIGOS DO VESTUÁRIO, PRODUZIDOS EM MALHARIAS E TRICOTAGENS, EXCETO MEIAS", + "1510600": "CURTIMENTO E OUTRAS PREPARAÇÕES DE COURO", + "1521100": "FABRICAÇÃO DE ARTIGOS PARA VIAGEM, BOLSAS E SEMELHANTES DE QUALQUER MATERIAL", + "1529700": "FABRICAÇÃO DE ARTEFATOS DE COURO NÃO ESPECIFICADOS ANTERIORMENTE", + "1531901": "FABRICAÇÃO DE CALÇADOS DE COURO", + "1531902": "ACABAMENTO DE CALÇADOS DE COURO SOB CONTRATO", + "1532700": "FABRICAÇÃO DE TÊNIS DE QUALQUER MATERIAL", + "1533500": "FABRICAÇÃO DE CALÇADOS DE MATERIAL SINTÉTICO", + "1539400": "FABRICAÇÃO DE CALÇADOS DE MATERIAIS NÃO ESPECIFICADOS ANTERIORMENTE", + "1540800": "FABRICAÇÃO DE PARTES PARA CALÇADOS, DE QUALQUER MATERIAL", + "1610203": "SERRARIAS COM DESDOBRAMENTO DE MADEIRA EM BRUTO", + "1610204": "SERRARIAS SEM DESDOBRAMENTO DE MADEIRA EM BRUTO - RESSERRAGEM", + "1610205": "SERVIÇO DE TRATAMENTO DE MADEIRA REALIZADO SOB CONTRATO", + "1621800": + "FABRICAÇÃO DE MADEIRA LAMINADA E DE CHAPAS DE MADEIRA COMPENSADA, PRENSADA E AGLOMERADA", + "1622601": "FABRICAÇÃO DE CASAS DE MADEIRA PRÉ FABRICADAS", + "1622602": + "FABRICAÇÃO DE ESQUADRIAS DE MADEIRA E DE PEÇAS DE MADEIRA PARA INSTALAÇÕES INDUSTRIAIS E COMERCIAIS", + "1622699": "FABRICAÇÃO DE OUTROS ARTIGOS DE CARPINTARIA PARA CONSTRUÇÃO", + "1623400": "FABRICAÇÃO DE ARTEFATOS DE TANOARIA E DE EMBALAGENS DE MADEIRA", + "1629301": "FABRICAÇÃO DE ARTEFATOS DIVERSOS DE MADEIRA, EXCETO MÓVEIS", + "1629302": + "FABRICAÇÃO DE ARTEFATOS DIVERSOS DE CORTIÇA, BAMBU, PALHA, VIME E OUTROS MATERIAIS TRANÇADOS, EXCETO MÓVEIS", + "1710900": "FABRICAÇÃO DE CELULOSE E OUTRAS PASTAS PARA A FABRICAÇÃO DE PAPEL", + "1721400": "FABRICAÇÃO DE PAPEL", + "1722200": "FABRICAÇÃO DE CARTOLINA E PAPEL CARTÃO", + "1731100": "FABRICAÇÃO DE EMBALAGENS DE PAPEL", + "1732000": "FABRICAÇÃO DE EMBALAGENS DE CARTOLINA E PAPEL CARTÃO", + "1733800": "FABRICAÇÃO DE CHAPAS E DE EMBALAGENS DE PAPELÃO ONDULADO", + "1741901": "FABRICAÇÃO DE FORMULÁRIOS CONTÍNUOS", + "1741902": + "FABRICAÇÃO DE PRODUTOS DE PAPEL, CARTOLINA, PAPEL CARTÃO E PAPELÃO ONDULADO PARA USO COMERCIAL E DE ESCRITÓRIO", + "1742701": "FABRICAÇÃO DE FRALDAS DESCARTÁVEIS", + "1742702": "FABRICAÇÃO DE ABSORVENTES HIGIÊNICOS", + "1742799": + "FABRICAÇÃO DE PRODUTOS DE PAPEL PARA USO DOMÉSTICO E HIGIÊNICO SANITÁRIO NÃO ESPECIFICADOS ANTERIORMENTE", + "1749400": + "FABRICAÇÃO DE PRODUTOS DE PASTAS CELULÓSICAS, PAPEL, CARTOLINA, PAPEL CARTÃO E PAPELÃO ONDULADO NÃO ESPECIFICADOS ANTERIORMENTE", + "1811301": "IMPRESSÃO DE JORNAIS", + "1811302": "IMPRESSÃO DE LIVROS, REVISTAS E OUTRAS PUBLICAÇÕES PERIÓDICAS", + "1812100": "IMPRESSÃO DE MATERIAL DE SEGURANÇA", + "1813001": "IMPRESSÃO DE MATERIAL PARA USO PUBLICITÁRIO", + "1813099": "IMPRESSÃO DE MATERIAL PARA OUTROS USOS", + "1821100": "SERVIÇOS DE PRÉ IMPRESSÃO", + "1822901": "SERVIÇOS DE ENCADERNAÇÃO E PLASTIFICAÇÃO", + "1822999": "SERVIÇOS DE ACABAMENTOS GRÁFICOS, EXCETO ENCADERNAÇÃO E PLASTIFICAÇÃO", + "1830001": "REPRODUÇÃO DE SOM EM QUALQUER SUPORTE", + "1830002": "REPRODUÇÃO DE VÍDEO EM QUALQUER SUPORTE", + "1830003": "REPRODUÇÃO DE SOFTWARE EM QUALQUER SUPORTE", + "1910100": "COQUERIAS", + "1921700": "FABRICAÇÃO DE PRODUTOS DO REFINO DE PETRÓLEO", + "1922501": "FORMULAÇÃO DE COMBUSTÍVEIS", + "1922502": "RERREFINO DE ÓLEOS LUBRIFICANTES", + "1922599": "FABRICAÇÃO DE OUTROS PRODUTOS DERIVADOS DO PETRÓLEO, EXCETO PRODUTOS DO REFINO", + "1931400": "FABRICAÇÃO DE ÁLCOOL", + "1932200": "FABRICAÇÃO DE BIOCOMBUSTÍVEIS, EXCETO ÁLCOOL", + "2011800": "FABRICAÇÃO DE CLORO E ÁLCALIS", + "2012600": "FABRICAÇÃO DE INTERMEDIÁRIOS PARA FERTILIZANTES", + "2013401": "FABRICAÇÃO DE ADUBOS E FERTILIZANTES ORGANOMINERAIS", + "2013402": "FABRICAÇÃO DE ADUBOS E FERTILIZANTES, EXCETO ORGANOMINERAIS", + "2014200": "FABRICAÇÃO DE GASES INDUSTRIAIS", + "2019301": "ELABORAÇÃO DE COMBUSTÍVEIS NUCLEARES", + "2019399": "FABRICAÇÃO DE OUTROS PRODUTOS QUÍMICOS INORGÂNICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "2021500": "FABRICAÇÃO DE PRODUTOS PETROQUÍMICOS BÁSICOS", + "2022300": "FABRICAÇÃO DE INTERMEDIÁRIOS PARA PLASTIFICANTES, RESINAS E FIBRAS", + "2029100": "FABRICAÇÃO DE PRODUTOS QUÍMICOS ORGÂNICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "2031200": "FABRICAÇÃO DE RESINAS TERMOPLÁSTICAS", + "2032100": "FABRICAÇÃO DE RESINAS TERMOFIXAS", + "2033900": "FABRICAÇÃO DE ELASTÔMEROS", + "2040100": "FABRICAÇÃO DE FIBRAS ARTIFICIAIS E SINTÉTICAS", + "2051700": "FABRICAÇÃO DE DEFENSIVOS AGRÍCOLAS", + "2052500": "FABRICAÇÃO DE DESINFESTANTES DOMISSANITÁRIOS", + "2061400": "FABRICAÇÃO DE SABÕES E DETERGENTES SINTÉTICOS", + "2062200": "FABRICAÇÃO DE PRODUTOS DE LIMPEZA E POLIMENTO", + "2063100": "FABRICAÇÃO DE COSMÉTICOS, PRODUTOS DE PERFUMARIA E DE HIGIENE PESSOAL", + "2071100": "FABRICAÇÃO DE TINTAS, VERNIZES, ESMALTES E LACAS", + "2072000": "FABRICAÇÃO DE TINTAS DE IMPRESSÃO", + "2073800": "FABRICAÇÃO DE IMPERMEABILIZANTES, SOLVENTES E PRODUTOS AFINS", + "2091600": "FABRICAÇÃO DE ADESIVOS E SELANTES", + "2092401": "FABRICAÇÃO DE PÓLVORAS, EXPLOSIVOS E DETONANTES", + "2092402": "FABRICAÇÃO DE ARTIGOS PIROTÉCNICOS", + "2092403": "FABRICAÇÃO DE FÓSFOROS DE SEGURANÇA", + "2093200": "FABRICAÇÃO DE ADITIVOS DE USO INDUSTRIAL", + "2094100": "FABRICAÇÃO DE CATALISADORES", + "2099101": + "FABRICAÇÃO DE CHAPAS, FILMES, PAPÉIS E OUTROS MATERIAIS E PRODUTOS QUÍMICOS PARA FOTOGRAFIA", + "2099199": "FABRICAÇÃO DE OUTROS PRODUTOS QUÍMICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "2110600": "FABRICAÇÃO DE PRODUTOS FARMOQUÍMICOS", + "2121101": "FABRICAÇÃO DE MEDICAMENTOS ALOPÁTICOS PARA USO HUMANO", + "2121102": "FABRICAÇÃO DE MEDICAMENTOS HOMEOPÁTICOS PARA USO HUMANO", + "2121103": "FABRICAÇÃO DE MEDICAMENTOS FITOTERÁPICOS PARA USO HUMANO", + "2122000": "FABRICAÇÃO DE MEDICAMENTOS PARA USO VETERINÁRIO", + "2123800": "FABRICAÇÃO DE PREPARAÇÕES FARMACÊUTICAS", + "2211100": "FABRICAÇÃO DE PNEUMÁTICOS E DE CÂMARAS DE AR", + "2212900": "REFORMA DE PNEUMÁTICOS USADOS", + "2219600": "FABRICAÇÃO DE ARTEFATOS DE BORRACHA NÃO ESPECIFICADOS ANTERIORMENTE", + "2221800": "FABRICAÇÃO DE LAMINADOS PLANOS E TUBULARES DE MATERIAL PLÁSTICO", + "2222600": "FABRICAÇÃO DE EMBALAGENS DE MATERIAL PLÁSTICO", + "2223400": "FABRICAÇÃO DE TUBOS E ACESSÓRIOS DE MATERIAL PLÁSTICO PARA USO NA CONSTRUÇÃO", + "2229301": "FABRICAÇÃO DE ARTEFATOS DE MATERIAL PLÁSTICO PARA USO PESSOAL E DOMÉSTICO", + "2229302": "FABRICAÇÃO DE ARTEFATOS DE MATERIAL PLÁSTICO PARA USOS INDUSTRIAIS", + "2229303": + "FABRICAÇÃO DE ARTEFATOS DE MATERIAL PLÁSTICO PARA USO NA CONSTRUÇÃO, EXCETO TUBOS E ACESSÓRIOS", + "2229399": + "FABRICAÇÃO DE ARTEFATOS DE MATERIAL PLÁSTICO PARA OUTROS USOS NÃO ESPECIFICADOS ANTERIORMENTE", + "2311700": "FABRICAÇÃO DE VIDRO PLANO E DE SEGURANÇA", + "2312500": "FABRICAÇÃO DE EMBALAGENS DE VIDRO", + "2319200": "FABRICAÇÃO DE ARTIGOS DE VIDRO", + "2320600": "FABRICAÇÃO DE CIMENTO", + "2330301": "FABRICAÇÃO DE ESTRUTURAS PRÉ MOLDADAS DE CONCRETO ARMADO, EM SÉRIE E SOB ENCOMENDA", + "2330302": "FABRICAÇÃO DE ARTEFATOS DE CIMENTO PARA USO NA CONSTRUÇÃO", + "2330303": "FABRICAÇÃO DE ARTEFATOS DE FIBROCIMENTO PARA USO NA CONSTRUÇÃO", + "2330304": "FABRICAÇÃO DE CASAS PRÉ MOLDADAS DE CONCRETO", + "2330305": "PREPARAÇÃO DE MASSA DE CONCRETO E ARGAMASSA PARA CONSTRUÇÃO", + "2330399": + "FABRICAÇÃO DE OUTROS ARTEFATOS E PRODUTOS DE CONCRETO, CIMENTO, FIBROCIMENTO, GESSO E MATERIAIS SEMELHANTES", + "2341900": "FABRICAÇÃO DE PRODUTOS CERÂMICOS REFRATÁRIOS", + "2342701": "FABRICAÇÃO DE AZULEJOS E PISOS", + "2342702": + "FABRICAÇÃO DE ARTEFATOS DE CERÂMICA E BARRO COZIDO PARA USO NA CONSTRUÇÃO, EXCETO AZULEJOS E PISOS", + "2349401": "FABRICAÇÃO DE MATERIAL SANITÁRIO DE CERÂMICA", + "2349499": "FABRICAÇÃO DE PRODUTOS CERÂMICOS NÃO REFRATÁRIOS NÃO ESPECIFICADOS ANTERIORMENTE", + "2391501": "BRITAMENTO DE PEDRAS, EXCETO ASSOCIADO À EXTRAÇÃO", + "2391502": "APARELHAMENTO DE PEDRAS PARA CONSTRUÇÃO, EXCETO ASSOCIADO À EXTRAÇÃO", + "2391503": + "APARELHAMENTO DE PLACAS E EXECUÇÃO DE TRABALHOS EM MÁRMORE, GRANITO, ARDÓSIA E OUTRAS PEDRAS", + "2392300": "FABRICAÇÃO DE CAL E GESSO", + "2399101": + "DECORAÇÃO, LAPIDAÇÃO, GRAVAÇÃO, VITRIFICAÇÃO E OUTROS TRABALHOS EM CERÂMICA, LOUÇA, VIDRO E CRISTAL", + "2399102": "FABRICAÇÃO DE ABRASIVOS", + "2399199": + "FABRICAÇÃO DE OUTROS PRODUTOS DE MINERAIS NÃO METÁLICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "2411300": "PRODUÇÃO DE FERRO GUSA", + "2412100": "PRODUÇÃO DE FERROLIGAS", + "2421100": "PRODUÇÃO DE SEMI ACABADOS DE AÇO", + "2422901": "PRODUÇÃO DE LAMINADOS PLANOS DE AÇO AO CARBONO, REVESTIDOS OU NÃO", + "2422902": "PRODUÇÃO DE LAMINADOS PLANOS DE AÇOS ESPECIAIS", + "2423701": "PRODUÇÃO DE TUBOS DE AÇO SEM COSTURA", + "2423702": "PRODUÇÃO DE LAMINADOS LONGOS DE AÇO, EXCETO TUBOS", + "2424501": "PRODUÇÃO DE ARAMES DE AÇO", + "2424502": "PRODUÇÃO DE RELAMINADOS, TREFILADOS E PERFILADOS DE AÇO, EXCETO ARAMES", + "2431800": "PRODUÇÃO DE TUBOS DE AÇO COM COSTURA", + "2439300": "PRODUÇÃO DE OUTROS TUBOS DE FERRO E AÇO", + "2441501": "PRODUÇÃO DE ALUMÍNIO E SUAS LIGAS EM FORMAS PRIMÁRIAS", + "2441502": "PRODUÇÃO DE LAMINADOS DE ALUMÍNIO", + "2442300": "METALURGIA DOS METAIS PRECIOSOS", + "2443100": "METALURGIA DO COBRE", + "2449101": "PRODUÇÃO DE ZINCO EM FORMAS PRIMÁRIAS", + "2449102": "PRODUÇÃO DE LAMINADOS DE ZINCO", + "2449103": "FABRICAÇÃO DE ÂNODOS PARA GALVANOPLASTIA", + "2449199": + "METALURGIA DE OUTROS METAIS NÃO FERROSOS E SUAS LIGAS NÃO ESPECIFICADOS ANTERIORMENTE", + "2451200": "FUNDIÇÃO DE FERRO E AÇO", + "2452100": "FUNDIÇÃO DE METAIS NÃO FERROSOS E SUAS LIGAS", + "2511000": "FABRICAÇÃO DE ESTRUTURAS METÁLICAS", + "2512800": "FABRICAÇÃO DE ESQUADRIAS DE METAL", + "2513600": "FABRICAÇÃO DE OBRAS DE CALDEIRARIA PESADA", + "2521700": "FABRICAÇÃO DE TANQUES, RESERVATÓRIOS METÁLICOS E CALDEIRAS PARA AQUECIMENTO CENTRAL", + "2522500": + "FABRICAÇÃO DE CALDEIRAS GERADORAS DE VAPOR, EXCETO PARA AQUECIMENTO CENTRAL E PARA VEÍCULOS", + "2531401": "PRODUÇÃO DE FORJADOS DE AÇO", + "2531402": "PRODUÇÃO DE FORJADOS DE METAIS NÃO FERROSOS E SUAS LIGAS", + "2532201": "PRODUÇÃO DE ARTEFATOS ESTAMPADOS DE METAL", + "2532202": "METALURGIA DO PÓ", + "2539001": "SERVIÇOS DE USINAGEM, TORNEARIA E SOLDA", + "2539002": "SERVIÇOS DE TRATAMENTO E REVESTIMENTO EM METAIS", + "2541100": "FABRICAÇÃO DE ARTIGOS DE CUTELARIA", + "2542000": "FABRICAÇÃO DE ARTIGOS DE SERRALHERIA, EXCETO ESQUADRIAS", + "2543800": "FABRICAÇÃO DE FERRAMENTAS", + "2550101": "FABRICAÇÃO DE EQUIPAMENTO BÉLICO PESADO, EXCETO VEÍCULOS MILITARES DE COMBATE", + "2550102": "FABRICAÇÃO DE ARMAS DE FOGO, OUTRAS ARMAS E MUNIÇÕES", + "2591800": "FABRICAÇÃO DE EMBALAGENS METÁLICAS", + "2592601": "FABRICAÇÃO DE PRODUTOS DE TREFILADOS DE METAL PADRONIZADOS", + "2592602": "FABRICAÇÃO DE PRODUTOS DE TREFILADOS DE METAL, EXCETO PADRONIZADOS", + "2593400": "FABRICAÇÃO DE ARTIGOS DE METAL PARA USO DOMÉSTICO E PESSOAL", + "2599301": "SERVIÇOS DE CONFECÇÃO DE ARMAÇÕES METÁLICAS PARA A CONSTRUÇÃO", + "2599302": "SERVIÇO DE CORTE E DOBRA DE METAIS", + "2599399": "FABRICAÇÃO DE OUTROS PRODUTOS DE METAL NÃO ESPECIFICADOS ANTERIORMENTE", + "2610800": "FABRICAÇÃO DE COMPONENTES ELETRÔNICOS", + "2621300": "FABRICAÇÃO DE EQUIPAMENTOS DE INFORMÁTICA", + "2622100": "FABRICAÇÃO DE PERIFÉRICOS PARA EQUIPAMENTOS DE INFORMÁTICA", + "2631100": "FABRICAÇÃO DE EQUIPAMENTOS TRANSMISSORES DE COMUNICAÇÃO, PEÇAS E ACESSÓRIOS", + "2632900": + "FABRICAÇÃO DE APARELHOS TELEFÔNICOS E DE OUTROS EQUIPAMENTOS DE COMUNICAÇÃO, PEÇAS E ACESSÓRIOS", + "2640000": + "FABRICAÇÃO DE APARELHOS DE RECEPÇÃO, REPRODUÇÃO, GRAVAÇÃO E AMPLIFICAÇÃO DE ÁUDIO E VÍDEO", + "2651500": "FABRICAÇÃO DE APARELHOS E EQUIPAMENTOS DE MEDIDA, TESTE E CONTROLE", + "2652300": "FABRICAÇÃO DE CRONÔMETROS E RELÓGIOS", + "2660400": + "FABRICAÇÃO DE APARELHOS ELETROMÉDICOS E ELETROTERAPÊUTICOS E EQUIPAMENTOS DE IRRADIAÇÃO", + "2670101": "FABRICAÇÃO DE EQUIPAMENTOS E INSTRUMENTOS ÓPTICOS, PEÇAS E ACESSÓRIOS", + "2670102": "FABRICAÇÃO DE APARELHOS FOTOGRÁFICOS E CINEMATOGRÁFICOS, PEÇAS E ACESSÓRIOS", + "2680900": "FABRICAÇÃO DE MÍDIAS VIRGENS, MAGNÉTICAS E ÓPTICAS", + "2710401": "FABRICAÇÃO DE GERADORES DE CORRENTE CONTÍNUA E ALTERNADA, PEÇAS E ACESSÓRIOS", + "2710402": + "FABRICAÇÃO DE TRANSFORMADORES, INDUTORES, CONVERSORES, SINCRONIZADORES E SEMELHANTES, PEÇAS E ACESSÓRIOS", + "2710403": "FABRICAÇÃO DE MOTORES ELÉTRICOS, PEÇAS E ACESSÓRIOS", + "2721000": + "FABRICAÇÃO DE PILHAS, BATERIAS E ACUMULADORES ELÉTRICOS, EXCETO PARA VEÍCULOS AUTOMOTORES", + "2722801": "FABRICAÇÃO DE BATERIAS E ACUMULADORES PARA VEÍCULOS AUTOMOTORES", + "2722802": "RECONDICIONAMENTO DE BATERIAS E ACUMULADORES PARA VEÍCULOS AUTOMOTORES", + "2731700": + "FABRICAÇÃO DE APARELHOS E EQUIPAMENTOS PARA DISTRIBUIÇÃO E CONTROLE DE ENERGIA ELÉTRICA", + "2732500": "FABRICAÇÃO DE MATERIAL ELÉTRICO PARA INSTALAÇÕES EM CIRCUITO DE CONSUMO", + "2733300": "FABRICAÇÃO DE FIOS, CABOS E CONDUTORES ELÉTRICOS ISOLADOS", + "2740601": "FABRICAÇÃO DE LÂMPADAS", + "2740602": "FABRICAÇÃO DE LUMINÁRIAS E OUTROS EQUIPAMENTOS DE ILUMINAÇÃO", + "2751100": + "FABRICAÇÃO DE FOGÕES, REFRIGERADORES E MÁQUINAS DE LAVAR E SECAR PARA USO DOMÉSTICO, PEÇAS E ACESSÓRIOS", + "2759701": "FABRICAÇÃO DE APARELHOS ELÉTRICOS DE USO PESSOAL, PEÇAS E ACESSÓRIOS", + "2759799": + "FABRICAÇÃO DE OUTROS APARELHOS ELETRODOMÉSTICOS NÃO ESPECIFICADOS ANTERIORMENTE, PEÇAS E ACESSÓRIOS", + "2790201": + "FABRICAÇÃO DE ELETRODOS, CONTATOS E OUTROS ARTIGOS DE CARVÃO E GRAFITA PARA USO ELÉTRICO, ELETROÍMÃS E ISOLADORES", + "2790202": "FABRICAÇÃO DE EQUIPAMENTOS PARA SINALIZAÇÃO E ALARME", + "2790299": + "FABRICAÇÃO DE OUTROS EQUIPAMENTOS E APARELHOS ELÉTRICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "2811900": + "FABRICAÇÃO DE MOTORES E TURBINAS, PEÇAS E ACESSÓRIOS, EXCETO PARA AVIÕES E VEÍCULOS RODOVIÁRIOS", + "2812700": + "FABRICAÇÃO DE EQUIPAMENTOS HIDRÁULICOS E PNEUMÁTICOS, PEÇAS E ACESSÓRIOS, EXCETO VÁLVULAS", + "2813500": "FABRICAÇÃO DE VÁLVULAS, REGISTROS E DISPOSITIVOS SEMELHANTES, PEÇAS E ACESSÓRIOS", + "2814301": "FABRICAÇÃO DE COMPRESSORES PARA USO INDUSTRIAL, PEÇAS E ACESSÓRIOS", + "2814302": "FABRICAÇÃO DE COMPRESSORES PARA USO NÃO INDUSTRIAL, PEÇAS E ACESSÓRIOS", + "2815101": "FABRICAÇÃO DE ROLAMENTOS PARA FINS INDUSTRIAIS", + "2815102": "FABRICAÇÃO DE EQUIPAMENTOS DE TRANSMISSÃO PARA FINS INDUSTRIAIS, EXCETO ROLAMENTOS", + "2821601": + "FABRICAÇÃO DE FORNOS INDUSTRIAIS, APARELHOS E EQUIPAMENTOS NÃO ELÉTRICOS PARA INSTALAÇÕES TÉRMICAS, PEÇAS E ACESSÓRIOS", + "2821602": "FABRICAÇÃO DE ESTUFAS E FORNOS ELÉTRICOS PARA FINS INDUSTRIAIS, PEÇAS E ACESSÓRIOS", + "2822401": + "FABRICAÇÃO DE MÁQUINAS, EQUIPAMENTOS E APARELHOS PARA TRANSPORTE E ELEVAÇÃO DE PESSOAS, PEÇAS E ACESSÓRIOS", + "2822402": + "FABRICAÇÃO DE MÁQUINAS, EQUIPAMENTOS E APARELHOS PARA TRANSPORTE E ELEVAÇÃO DE CARGAS, PEÇAS E ACESSÓRIOS", + "2823200": + "FABRICAÇÃO DE MÁQUINAS E APARELHOS DE REFRIGERAÇÃO E VENTILAÇÃO PARA USO INDUSTRIAL E COMERCIAL, PEÇAS E ACESSÓRIOS", + "2824101": "FABRICAÇÃO DE APARELHOS E EQUIPAMENTOS DE AR CONDICIONADO PARA USO INDUSTRIAL", + "2824102": "FABRICAÇÃO DE APARELHOS E EQUIPAMENTOS DE AR CONDICIONADO PARA USO NÃO INDUSTRIAL", + "2825900": + "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA SANEAMENTO BÁSICO E AMBIENTAL, PEÇAS E ACESSÓRIOS", + "2829101": + "FABRICAÇÃO DE MÁQUINAS DE ESCREVER, CALCULAR E OUTROS EQUIPAMENTOS NÃO ELETRÔNICOS PARA ESCRITÓRIO, PEÇAS E ACESSÓRIOS", + "2829199": + "FABRICAÇÃO DE OUTRAS MÁQUINAS E EQUIPAMENTOS DE USO GERAL NÃO ESPECIFICADOS ANTERIORMENTE, PEÇAS E ACESSÓRIOS", + "2831300": "FABRICAÇÃO DE TRATORES AGRÍCOLAS, PEÇAS E ACESSÓRIOS", + "2832100": "FABRICAÇÃO DE EQUIPAMENTOS PARA IRRIGAÇÃO AGRÍCOLA, PEÇAS E ACESSÓRIOS", + "2833000": + "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA A AGRICULTURA E PECUÁRIA, PEÇAS E ACESSÓRIOS, EXCETO PARA IRRIGAÇÃO", + "2840200": "FABRICAÇÃO DE MÁQUINAS FERRAMENTA, PEÇAS E ACESSÓRIOS", + "2851800": + "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA A PROSPECÇÃO E EXTRAÇÃO DE PETRÓLEO, PEÇAS E ACESSÓRIOS", + "2852600": + "FABRICAÇÃO DE OUTRAS MÁQUINAS E EQUIPAMENTOS PARA USO NA EXTRAÇÃO MINERAL, PEÇAS E ACESSÓRIOS, EXCETO NA EXTRAÇÃO DE PETRÓLEO", + "2853400": "FABRICAÇÃO DE TRATORES, PEÇAS E ACESSÓRIOS, EXCETO AGRÍCOLAS", + "2854200": + "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA TERRAPLENAGEM, PAVIMENTAÇÃO E CONSTRUÇÃO, PEÇAS E ACESSÓRIOS, EXCETO TRATORES", + "2861500": + "FABRICAÇÃO DE MÁQUINAS PARA A INDÚSTRIA METALÚRGICA, PEÇAS E ACESSÓRIOS, EXCETO MÁQUINAS FERRAMENTA", + "2862300": + "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA AS INDÚSTRIAS DE ALIMENTOS, BEBIDAS E FUMO, PEÇAS E ACESSÓRIOS", + "2863100": "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA A INDÚSTRIA TÊXTIL, PEÇAS E ACESSÓRIOS", + "2864000": + "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA AS INDÚSTRIAS DO VESTUÁRIO, DO COURO E DE CALÇADOS, PEÇAS E ACESSÓRIOS", + "2865800": + "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA AS INDÚSTRIAS DE CELULOSE, PAPEL E PAPELÃO E ARTEFATOS, PEÇAS E ACESSÓRIOS", + "2866600": + "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA A INDÚSTRIA DO PLÁSTICO, PEÇAS E ACESSÓRIOS", + "2869100": + "FABRICAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA USO INDUSTRIAL ESPECÍFICO NÃO ESPECIFICADOS ANTERIORMENTE, PEÇAS E ACESSÓRIOS", + "2910701": "FABRICAÇÃO DE AUTOMÓVEIS, CAMIONETAS E UTILITÁRIOS", + "2910702": "FABRICAÇÃO DE CHASSIS COM MOTOR PARA AUTOMÓVEIS, CAMIONETAS E UTILITÁRIOS", + "2910703": "FABRICAÇÃO DE MOTORES PARA AUTOMÓVEIS, CAMIONETAS E UTILITÁRIOS", + "2920401": "FABRICAÇÃO DE CAMINHÕES E ÔNIBUS", + "2920402": "FABRICAÇÃO DE MOTORES PARA CAMINHÕES E ÔNIBUS", + "2930101": "FABRICAÇÃO DE CABINES, CARROCERIAS E REBOQUES PARA CAMINHÕES", + "2930102": "FABRICAÇÃO DE CARROCERIAS PARA ÔNIBUS", + "2930103": + "FABRICAÇÃO DE CABINES, CARROCERIAS E REBOQUES PARA OUTROS VEÍCULOS AUTOMOTORES, EXCETO CAMINHÕES E ÔNIBUS", + "2941700": "FABRICAÇÃO DE PEÇAS E ACESSÓRIOS PARA O SISTEMA MOTOR DE VEÍCULOS AUTOMOTORES", + "2942500": + "FABRICAÇÃO DE PEÇAS E ACESSÓRIOS PARA OS SISTEMAS DE MARCHA E TRANSMISSÃO DE VEÍCULOS AUTOMOTORES", + "2943300": "FABRICAÇÃO DE PEÇAS E ACESSÓRIOS PARA O SISTEMA DE FREIOS DE VEÍCULOS AUTOMOTORES", + "2944100": + "FABRICAÇÃO DE PEÇAS E ACESSÓRIOS PARA O SISTEMA DE DIREÇÃO E SUSPENSÃO DE VEÍCULOS AUTOMOTORES", + "2945000": + "FABRICAÇÃO DE MATERIAL ELÉTRICO E ELETRÔNICO PARA VEÍCULOS AUTOMOTORES, EXCETO BATERIAS", + "2949201": "FABRICAÇÃO DE BANCOS E ESTOFADOS PARA VEÍCULOS AUTOMOTORES", + "2949299": + "FABRICAÇÃO DE OUTRAS PEÇAS E ACESSÓRIOS PARA VEÍCULOS AUTOMOTORES NÃO ESPECIFICADAS ANTERIORMENTE", + "2950600": "RECONDICIONAMENTO E RECUPERAÇÃO DE MOTORES PARA VEÍCULOS AUTOMOTORES", + "3011301": "CONSTRUÇÃO DE EMBARCAÇÕES DE GRANDE PORTE", + "3011302": + "CONSTRUÇÃO DE EMBARCAÇÕES PARA USO COMERCIAL E PARA USOS ESPECIAIS, EXCETO DE GRANDE PORTE", + "3012100": "CONSTRUÇÃO DE EMBARCAÇÕES PARA ESPORTE E LAZER", + "3031800": "FABRICAÇÃO DE LOCOMOTIVAS, VAGÕES E OUTROS MATERIAIS RODANTES", + "3032600": "FABRICAÇÃO DE PEÇAS E ACESSÓRIOS PARA VEÍCULOS FERROVIÁRIOS", + "3041500": "FABRICAÇÃO DE AERONAVES", + "3042300": "FABRICAÇÃO DE TURBINAS, MOTORES E OUTROS COMPONENTES E PEÇAS PARA AERONAVES", + "3050400": "FABRICAÇÃO DE VEÍCULOS MILITARES DE COMBATE", + "3091101": "FABRICAÇÃO DE MOTOCICLETAS", + "3091102": "FABRICAÇÃO DE PEÇAS E ACESSÓRIOS PARA MOTOCICLETAS", + "3092000": "FABRICAÇÃO DE BICICLETAS E TRICICLOS NÃO MOTORIZADOS, PEÇAS E ACESSÓRIOS", + "3099700": "FABRICAÇÃO DE EQUIPAMENTOS DE TRANSPORTE NÃO ESPECIFICADOS ANTERIORMENTE", + "3101200": "FABRICAÇÃO DE MÓVEIS COM PREDOMINÂNCIA DE MADEIRA", + "3102100": "FABRICAÇÃO DE MÓVEIS COM PREDOMINÂNCIA DE METAL", + "3103900": "FABRICAÇÃO DE MÓVEIS DE OUTROS MATERIAIS, EXCETO MADEIRA E METAL", + "3104700": "FABRICAÇÃO DE COLCHÕES", + "3211601": "LAPIDAÇÃO DE GEMAS", + "3211602": "FABRICAÇÃO DE ARTEFATOS DE JOALHERIA E OURIVESARIA", + "3211603": "CUNHAGEM DE MOEDAS E MEDALHAS", + "3212400": "FABRICAÇÃO DE BIJUTERIAS E ARTEFATOS SEMELHANTES", + "3220500": "FABRICAÇÃO DE INSTRUMENTOS MUSICAIS, PEÇAS E ACESSÓRIOS", + "3230200": "FABRICAÇÃO DE ARTEFATOS PARA PESCA E ESPORTE", + "3240001": "FABRICAÇÃO DE JOGOS ELETRÔNICOS", + "3240002": "FABRICAÇÃO DE MESAS DE BILHAR, DE SINUCA E ACESSÓRIOS NÃO ASSOCIADA À LOCAÇÃO", + "3240003": "FABRICAÇÃO DE MESAS DE BILHAR, DE SINUCA E ACESSÓRIOS ASSOCIADA À LOCAÇÃO", + "3240099": "FABRICAÇÃO DE OUTROS BRINQUEDOS E JOGOS RECREATIVOS NÃO ESPECIFICADOS ANTERIORMENTE", + "3250701": + "FABRICAÇÃO DE INSTRUMENTOS NÃO ELETRÔNICOS E UTENSÍLIOS PARA USO MÉDICO, CIRÚRGICO, ODONTOLÓGICO E DE LABORATÓRIO", + "3250702": "FABRICAÇÃO DE MOBILIÁRIO PARA USO MÉDICO, CIRÚRGICO, ODONTOLÓGICO E DE LABORATÓRIO", + "3250703": + "FABRICAÇÃO DE APARELHOS E UTENSÍLIOS PARA CORREÇÃO DE DEFEITOS FÍSICOS E APARELHOS ORTOPÉDICOS EM GERAL SOB ENCOMENDA", + "3250704": + "FABRICAÇÃO DE APARELHOS E UTENSÍLIOS PARA CORREÇÃO DE DEFEITOS FÍSICOS E APARELHOS ORTOPÉDICOS EM GERAL, EXCETO SOB ENCOMENDA", + "3250705": "FABRICAÇÃO DE MATERIAIS PARA MEDICINA E ODONTOLOGIA", + "3250706": "SERVIÇOS DE PRÓTESE DENTÁRIA", + "3250707": "FABRICAÇÃO DE ARTIGOS ÓPTICOS", + "3250709": "SERVIÇO DE LABORATÓRIO ÓPTICO", + "3291400": "FABRICAÇÃO DE ESCOVAS, PINCÉIS E VASSOURAS", + "3292201": "FABRICAÇÃO DE ROUPAS DE PROTEÇÃO E SEGURANÇA E RESISTENTES A FOGO", + "3292202": "FABRICAÇÃO DE EQUIPAMENTOS E ACESSÓRIOS PARA SEGURANÇA PESSOAL E PROFISSIONAL", + "3299001": "FABRICAÇÃO DE GUARDA CHUVAS E SIMILARES", + "3299002": "FABRICAÇÃO DE CANETAS, LÁPIS E OUTROS ARTIGOS PARA ESCRITÓRIO", + "3299003": "FABRICAÇÃO DE LETRAS, LETREIROS E PLACAS DE QUALQUER MATERIAL, EXCETO LUMINOSOS", + "3299004": "FABRICAÇÃO DE PAINÉIS E LETREIROS LUMINOSOS", + "3299005": "FABRICAÇÃO DE AVIAMENTOS PARA COSTURA", + "3299006": "FABRICAÇÃO DE VELAS, INCLUSIVE DECORATIVAS", + "3299099": "FABRICAÇÃO DE PRODUTOS DIVERSOS NÃO ESPECIFICADOS ANTERIORMENTE", + "3311200": + "MANUTENÇÃO E REPARAÇÃO DE TANQUES, RESERVATÓRIOS METÁLICOS E CALDEIRAS, EXCETO PARA VEÍCULOS", + "3312102": "MANUTENÇÃO E REPARAÇÃO DE APARELHOS E INSTRUMENTOS DE MEDIDA, TESTE E CONTROLE", + "3312103": + "MANUTENÇÃO E REPARAÇÃO DE APARELHOS ELETROMÉDICOS E ELETROTERAPÊUTICOS E EQUIPAMENTOS DE IRRADIAÇÃO", + "3312104": "MANUTENÇÃO E REPARAÇÃO DE EQUIPAMENTOS E INSTRUMENTOS ÓPTICOS", + "3313901": "MANUTENÇÃO E REPARAÇÃO DE GERADORES, TRANSFORMADORES E MOTORES ELÉTRICOS", + "3313902": "MANUTENÇÃO E REPARAÇÃO DE BATERIAS E ACUMULADORES ELÉTRICOS, EXCETO PARA VEÍCULOS", + "3313999": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS, APARELHOS E MATERIAIS ELÉTRICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "3314701": "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS MOTRIZES NÃO ELÉTRICAS", + "3314702": "MANUTENÇÃO E REPARAÇÃO DE EQUIPAMENTOS HIDRÁULICOS E PNEUMÁTICOS, EXCETO VÁLVULAS", + "3314703": "MANUTENÇÃO E REPARAÇÃO DE VÁLVULAS INDUSTRIAIS", + "3314704": "MANUTENÇÃO E REPARAÇÃO DE COMPRESSORES", + "3314705": "MANUTENÇÃO E REPARAÇÃO DE EQUIPAMENTOS DE TRANSMISSÃO PARA FINS INDUSTRIAIS", + "3314706": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS, APARELHOS E EQUIPAMENTOS PARA INSTALAÇÕES TÉRMICAS", + "3314707": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E APARELHOS DE REFRIGERAÇÃO E VENTILAÇÃO PARA USO INDUSTRIAL E COMERCIAL", + "3314708": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS, EQUIPAMENTOS E APARELHOS PARA TRANSPORTE E ELEVAÇÃO DE CARGAS", + "3314709": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS DE ESCREVER, CALCULAR E DE OUTROS EQUIPAMENTOS NÃO ELETRÔNICOS PARA ESCRITÓRIO", + "3314710": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA USO GERAL NÃO ESPECIFICADOS ANTERIORMENTE", + "3314711": "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA AGRICULTURA E PECUÁRIA", + "3314712": "MANUTENÇÃO E REPARAÇÃO DE TRATORES AGRÍCOLAS", + "3314713": "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS FERRAMENTA", + "3314714": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA A PROSPECÇÃO E EXTRAÇÃO DE PETRÓLEO", + "3314715": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA USO NA EXTRAÇÃO MINERAL, EXCETO NA EXTRAÇÃO DE PETRÓLEO", + "3314716": "MANUTENÇÃO E REPARAÇÃO DE TRATORES, EXCETO AGRÍCOLAS", + "3314717": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E EQUIPAMENTOS DE TERRAPLENAGEM, PAVIMENTAÇÃO E CONSTRUÇÃO, EXCETO TRATORES", + "3314718": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS PARA A INDÚSTRIA METALÚRGICA, EXCETO MÁQUINAS FERRAMENTA", + "3314719": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA AS INDÚSTRIAS DE ALIMENTOS, BEBIDAS E FUMO", + "3314720": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E EQUIPAMENTOS PARA A INDÚSTRIA TÊXTIL, DO VESTUÁRIO, DO COURO E CALÇADOS", + "3314721": + "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E APARELHOS PARA A INDÚSTRIA DE CELULOSE, PAPEL E PAPELÃO E ARTEFATOS", + "3314722": "MANUTENÇÃO E REPARAÇÃO DE MÁQUINAS E APARELHOS PARA A INDÚSTRIA DO PLÁSTICO", + "3314799": + "MANUTENÇÃO E REPARAÇÃO DE OUTRAS MÁQUINAS E EQUIPAMENTOS PARA USOS INDUSTRIAIS NÃO ESPECIFICADOS ANTERIORMENTE", + "3315500": "MANUTENÇÃO E REPARAÇÃO DE VEÍCULOS FERROVIÁRIOS", + "3316301": "MANUTENÇÃO E REPARAÇÃO DE AERONAVES, EXCETO A MANUTENÇÃO NA PISTA", + "3316302": "MANUTENÇÃO DE AERONAVES NA PISTA", + "3317101": "MANUTENÇÃO E REPARAÇÃO DE EMBARCAÇÕES E ESTRUTURAS FLUTUANTES", + "3317102": "MANUTENÇÃO E REPARAÇÃO DE EMBARCAÇÕES PARA ESPORTE E LAZER", + "3319800": "MANUTENÇÃO E REPARAÇÃO DE EQUIPAMENTOS E PRODUTOS NÃO ESPECIFICADOS ANTERIORMENTE", + "3321000": "INSTALAÇÃO DE MÁQUINAS E EQUIPAMENTOS INDUSTRIAIS", + "3329501": "SERVIÇOS DE MONTAGEM DE MÓVEIS DE QUALQUER MATERIAL", + "3329599": "INSTALAÇÃO DE OUTROS EQUIPAMENTOS NÃO ESPECIFICADOS ANTERIORMENTE", + "3511501": "GERAÇÃO DE ENERGIA ELÉTRICA", + "3511502": + "ATIVIDADES DE COORDENAÇÃO E CONTROLE DA OPERAÇÃO DA GERAÇÃO E TRANSMISSÃO DE ENERGIA ELÉTRICA", + "3512300": "TRANSMISSÃO DE ENERGIA ELÉTRICA", + "3513100": "COMÉRCIO ATACADISTA DE ENERGIA ELÉTRICA", + "3514000": "DISTRIBUIÇÃO DE ENERGIA ELÉTRICA", + "3520401": "PRODUÇÃO DE GÁS; PROCESSAMENTO DE GÁS NATURAL", + "3520402": "DISTRIBUIÇÃO DE COMBUSTÍVEIS GASOSOS POR REDES URBANAS", + "3530100": "PRODUÇÃO E DISTRIBUIÇÃO DE VAPOR, ÁGUA QUENTE E AR CONDICIONADO", + "3600601": "CAPTAÇÃO, TRATAMENTO E DISTRIBUIÇÃO DE ÁGUA", + "3600602": "DISTRIBUIÇÃO DE ÁGUA POR CAMINHÕES", + "3701100": "GESTÃO DE REDES DE ESGOTO", + "3702900": "ATIVIDADES RELACIONADAS A ESGOTO, EXCETO A GESTÃO DE REDES", + "3811400": "COLETA DE RESÍDUOS NÃO PERIGOSOS", + "3812200": "COLETA DE RESÍDUOS PERIGOSOS", + "3821100": "TRATAMENTO E DISPOSIÇÃO DE RESÍDUOS NÃO PERIGOSOS", + "3822000": "TRATAMENTO E DISPOSIÇÃO DE RESÍDUOS PERIGOSOS", + "3831901": "RECUPERAÇÃO DE SUCATAS DE ALUMÍNIO", + "3831999": "RECUPERAÇÃO DE MATERIAIS METÁLICOS, EXCETO ALUMÍNIO", + "3832700": "RECUPERAÇÃO DE MATERIAIS PLÁSTICOS", + "3839401": "USINAS DE COMPOSTAGEM", + "3839499": "RECUPERAÇÃO DE MATERIAIS NÃO ESPECIFICADOS ANTERIORMENTE", + "3900500": "DESCONTAMINAÇÃO E OUTROS SERVIÇOS DE GESTÃO DE RESÍDUOS", + "4110700": "INCORPORAÇÃO DE EMPREENDIMENTOS IMOBILIÁRIOS", + "4120400": "CONSTRUÇÃO DE EDIFÍCIOS", + "4211101": "CONSTRUÇÃO DE RODOVIAS E FERROVIAS", + "4211102": "PINTURA PARA SINALIZAÇÃO EM PISTAS RODOVIÁRIAS E AEROPORTOS", + "4212000": "CONSTRUÇÃO DE OBRAS DE ARTE ESPECIAIS", + "4213800": "OBRAS DE URBANIZAÇÃO - RUAS, PRAÇAS E CALÇADAS", + "4221901": "CONSTRUÇÃO DE BARRAGENS E REPRESAS PARA GERAÇÃO DE ENERGIA ELÉTRICA", + "4221902": "CONSTRUÇÃO DE ESTAÇÕES E REDES DE DISTRIBUIÇÃO DE ENERGIA ELÉTRICA", + "4221903": "MANUTENÇÃO DE REDES DE DISTRIBUIÇÃO DE ENERGIA ELÉTRICA", + "4221904": "CONSTRUÇÃO DE ESTAÇÕES E REDES DE TELECOMUNICAÇÕES", + "4221905": "MANUTENÇÃO DE ESTAÇÕES E REDES DE TELECOMUNICAÇÕES", + "4222701": + "CONSTRUÇÃO DE REDES DE ABASTECIMENTO DE ÁGUA, COLETA DE ESGOTO E CONSTRUÇÕES CORRELATAS, EXCETO OBRAS DE IRRIGAÇÃO", + "4222702": "OBRAS DE IRRIGAÇÃO", + "4223500": "CONSTRUÇÃO DE REDES DE TRANSPORTES POR DUTOS, EXCETO PARA ÁGUA E ESGOTO", + "4291000": "OBRAS PORTUÁRIAS, MARÍTIMAS E FLUVIAIS", + "4292801": "MONTAGEM DE ESTRUTURAS METÁLICAS", + "4292802": "OBRAS DE MONTAGEM INDUSTRIAL", + "4299501": "CONSTRUÇÃO DE INSTALAÇÕES ESPORTIVAS E RECREATIVAS", + "4299599": "OUTRAS OBRAS DE ENGENHARIA CIVIL NÃO ESPECIFICADAS ANTERIORMENTE", + "4311801": "DEMOLIÇÃO DE EDIFÍCIOS E OUTRAS ESTRUTURAS", + "4311802": "PREPARAÇÃO DE CANTEIRO E LIMPEZA DE TERRENO", + "4312600": "PERFURAÇÕES E SONDAGENS", + "4313400": "OBRAS DE TERRAPLENAGEM", + "4319300": "SERVIÇOS DE PREPARAÇÃO DO TERRENO NÃO ESPECIFICADOS ANTERIORMENTE", + "4321500": "INSTALAÇÃO E MANUTENÇÃO ELÉTRICA", + "4322301": "INSTALAÇÕES HIDRÁULICAS, SANITÁRIAS E DE GÁS", + "4322302": + "INSTALAÇÃO E MANUTENÇÃO DE SISTEMAS CENTRAIS DE AR CONDICIONADO, DE VENTILAÇÃO E REFRIGERAÇÃO", + "4322303": "INSTALAÇÕES DE SISTEMA DE PREVENÇÃO CONTRA INCÊNDIO", + "4329101": "INSTALAÇÃO DE PAINÉIS PUBLICITÁRIOS", + "4329102": "INSTALAÇÃO DE EQUIPAMENTOS PARA ORIENTAÇÃO À NAVEGAÇÃO MARÍTIMA FLUVIAL E LACUSTRE", + "4329103": "INSTALAÇÃO, MANUTENÇÃO E REPARAÇÃO DE ELEVADORES, ESCADAS E ESTEIRAS ROLANTES", + "4329104": + "MONTAGEM E INSTALAÇÃO DE SISTEMAS E EQUIPAMENTOS DE ILUMINAÇÃO E SINALIZAÇÃO EM VIAS PÚBLICAS, PORTOS E AEROPORTOS", + "4329105": "TRATAMENTOS TÉRMICOS, ACÚSTICOS OU DE VIBRAÇÃO", + "4329199": "OUTRAS OBRAS DE INSTALAÇÕES EM CONSTRUÇÕES NÃO ESPECIFICADAS ANTERIORMENTE", + "4330401": "IMPERMEABILIZAÇÃO EM OBRAS DE ENGENHARIA CIVIL", + "4330402": + "INSTALAÇÃO DE PORTAS, JANELAS, TETOS, DIVISÓRIAS E ARMÁRIOS EMBUTIDOS DE QUALQUER MATERIAL", + "4330403": "OBRAS DE ACABAMENTO EM GESSO E ESTUQUE", + "4330404": "SERVIÇOS DE PINTURA DE EDIFÍCIOS EM GERAL", + "4330405": "APLICAÇÃO DE REVESTIMENTOS E DE RESINAS EM INTERIORES E EXTERIORES", + "4330499": "OUTRAS OBRAS DE ACABAMENTO DA CONSTRUÇÃO", + "4391600": "OBRAS DE FUNDAÇÕES", + "4399101": "ADMINISTRAÇÃO DE OBRAS", + "4399102": "MONTAGEM E DESMONTAGEM DE ANDAIMES E OUTRAS ESTRUTURAS TEMPORÁRIAS", + "4399103": "OBRAS DE ALVENARIA", + "4399104": + "SERVIÇOS DE OPERAÇÃO E FORNECIMENTO DE EQUIPAMENTOS PARA TRANSPORTE E ELEVAÇÃO DE CARGAS E PESSOAS PARA USO EM OBRAS", + "4399105": "PERFURAÇÃO E CONSTRUÇÃO DE POÇOS DE ÁGUA", + "4399199": "SERVIÇOS ESPECIALIZADOS PARA CONSTRUÇÃO NÃO ESPECIFICADOS ANTERIORMENTE", + "4511101": "COMÉRCIO A VAREJO DE AUTOMÓVEIS, CAMIONETAS E UTILITÁRIOS NOVOS", + "4511102": "COMÉRCIO A VAREJO DE AUTOMÓVEIS, CAMIONETAS E UTILITÁRIOS USADOS", + "4511103": "COMÉRCIO POR ATACADO DE AUTOMÓVEIS, CAMIONETAS E UTILITÁRIOS NOVOS E USADOS", + "4511104": "COMÉRCIO POR ATACADO DE CAMINHÕES NOVOS E USADOS", + "4511105": "COMÉRCIO POR ATACADO DE REBOQUES E SEMI REBOQUES NOVOS E USADOS", + "4511106": "COMÉRCIO POR ATACADO DE ÔNIBUS E MICROÔNIBUS NOVOS E USADOS", + "4512901": "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE VEÍCULOS AUTOMOTORES", + "4512902": "COMÉRCIO SOB CONSIGNAÇÃO DE VEÍCULOS AUTOMOTORES", + "4520001": "SERVIÇOS DE MANUTENÇÃO E REPARAÇÃO MECÂNICA DE VEÍCULOS AUTOMOTORES", + "4520002": "SERVIÇOS DE LANTERNAGEM OU FUNILARIA E PINTURA DE VEÍCULOS AUTOMOTORES", + "4520003": "SERVIÇOS DE MANUTENÇÃO E REPARAÇÃO ELÉTRICA DE VEÍCULOS AUTOMOTORES", + "4520004": "SERVIÇOS DE ALINHAMENTO E BALANCEAMENTO DE VEÍCULOS AUTOMOTORES", + "4520005": "SERVIÇOS DE LAVAGEM, LUBRIFICAÇÃO E POLIMENTO DE VEÍCULOS AUTOMOTORES", + "4520006": "SERVIÇOS DE BORRACHARIA PARA VEÍCULOS AUTOMOTORES", + "4520007": + "SERVIÇOS DE INSTALAÇÃO, MANUTENÇÃO E REPARAÇÃO DE ACESSÓRIOS PARA VEÍCULOS AUTOMOTORES", + "4520008": "SERVIÇOS DE CAPOTARIA", + "4530701": "COMÉRCIO POR ATACADO DE PEÇAS E ACESSÓRIOS NOVOS PARA VEÍCULOS AUTOMOTORES", + "4530702": "COMÉRCIO POR ATACADO DE PNEUMÁTICOS E CÂMARAS DE AR", + "4530703": "COMÉRCIO A VAREJO DE PEÇAS E ACESSÓRIOS NOVOS PARA VEÍCULOS AUTOMOTORES", + "4530704": "COMÉRCIO A VAREJO DE PEÇAS E ACESSÓRIOS USADOS PARA VEÍCULOS AUTOMOTORES", + "4530705": "COMÉRCIO A VAREJO DE PNEUMÁTICOS E CÂMARAS DE AR", + "4530706": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE PEÇAS E ACESSÓRIOS NOVOS E USADOS PARA VEÍCULOS AUTOMOTORES", + "4541201": "COMÉRCIO POR ATACADO DE MOTOCICLETAS E MOTONETAS", + "4541202": "COMÉRCIO POR ATACADO DE PEÇAS E ACESSÓRIOS PARA MOTOCICLETAS E MOTONETAS", + "4541203": "COMÉRCIO A VAREJO DE MOTOCICLETAS E MOTONETAS NOVAS", + "4541204": "COMÉRCIO A VAREJO DE MOTOCICLETAS E MOTONETAS USADAS", + "4541206": "COMÉRCIO A VAREJO DE PEÇAS E ACESSÓRIOS NOVOS PARA MOTOCICLETAS E MOTONETAS", + "4541207": "COMÉRCIO A VAREJO DE PEÇAS E ACESSÓRIOS USADOS PARA MOTOCICLETAS E MOTONETAS", + "4542101": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE MOTOCICLETAS E MOTONETAS, PEÇAS E ACESSÓRIOS", + "4542102": "COMÉRCIO SOB CONSIGNAÇÃO DE MOTOCICLETAS E MOTONETAS", + "4543900": "MANUTENÇÃO E REPARAÇÃO DE MOTOCICLETAS E MOTONETAS", + "4611700": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE MATÉRIAS PRIMAS AGRÍCOLAS E ANIMAIS VIVOS", + "4612500": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE COMBUSTÍVEIS, MINERAIS, PRODUTOS SIDERÚRGICOS E QUÍMICOS", + "4613300": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE MADEIRA, MATERIAL DE CONSTRUÇÃO E FERRAGENS", + "4614100": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE MÁQUINAS, EQUIPAMENTOS, EMBARCAÇÕES E AERONAVES", + "4615000": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE ELETRODOMÉSTICOS, MÓVEIS E ARTIGOS DE USO DOMÉSTICO", + "4616800": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE TÊXTEIS, VESTUÁRIO, CALÇADOS E ARTIGOS DE VIAGEM", + "4617600": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE PRODUTOS ALIMENTÍCIOS, BEBIDAS E FUMO", + "4618401": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE MEDICAMENTOS, COSMÉTICOS E PRODUTOS DE PERFUMARIA", + "4618402": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE INSTRUMENTOS E MATERIAIS ODONTO MÉDICO HOSPITALARES", + "4618403": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE JORNAIS, REVISTAS E OUTRAS PUBLICAÇÕES", + "4618499": + "OUTROS REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO ESPECIALIZADO EM PRODUTOS NÃO ESPECIFICADOS ANTERIORMENTE", + "4619200": + "REPRESENTANTES COMERCIAIS E AGENTES DO COMÉRCIO DE MERCADORIAS EM GERAL NÃO ESPECIALIZADO", + "4621400": "COMÉRCIO ATACADISTA DE CAFÉ EM GRÃO", + "4622200": "COMÉRCIO ATACADISTA DE SOJA", + "4623101": "COMÉRCIO ATACADISTA DE ANIMAIS VIVOS", + "4623102": + "COMÉRCIO ATACADISTA DE COUROS, LÃS, PELES E OUTROS SUBPRODUTOS NÃO COMESTÍVEIS DE ORIGEM ANIMAL", + "4623103": "COMÉRCIO ATACADISTA DE ALGODÃO", + "4623104": "COMÉRCIO ATACADISTA DE FUMO EM FOLHA NÃO BENEFICIADO", + "4623105": "COMÉRCIO ATACADISTA DE CACAU", + "4623106": "COMÉRCIO ATACADISTA DE SEMENTES, FLORES, PLANTAS E GRAMAS", + "4623107": "COMÉRCIO ATACADISTA DE SISAL", + "4623108": + "COMÉRCIO ATACADISTA DE MATÉRIAS PRIMAS AGRÍCOLAS COM ATIVIDADE DE FRACIONAMENTO E ACONDICIONAMENTO ASSOCIADA", + "4623109": "COMÉRCIO ATACADISTA DE ALIMENTOS PARA ANIMAIS", + "4623199": "COMÉRCIO ATACADISTA DE MATÉRIAS PRIMAS AGRÍCOLAS NÃO ESPECIFICADAS ANTERIORMENTE", + "4631100": "COMÉRCIO ATACADISTA DE LEITE E LATICÍNIOS", + "4632001": "COMÉRCIO ATACADISTA DE CEREAIS E LEGUMINOSAS BENEFICIADOS", + "4632002": "COMÉRCIO ATACADISTA DE FARINHAS, AMIDOS E FÉCULAS", + "4632003": + "COMÉRCIO ATACADISTA DE CEREAIS E LEGUMINOSAS BENEFICIADOS, FARINHAS, AMIDOS E FÉCULAS, COM ATIVIDADE DE FRACIONAMENTO E ACONDICIONAMENTO ASSOCIADA", + "4633801": + "COMÉRCIO ATACADISTA DE FRUTAS, VERDURAS, RAÍZES, TUBÉRCULOS, HORTALIÇAS E LEGUMES FRESCOS", + "4633802": "COMÉRCIO ATACADISTA DE AVES VIVAS E OVOS", + "4633803": "COMÉRCIO ATACADISTA DE COELHOS E OUTROS PEQUENOS ANIMAIS VIVOS PARA ALIMENTAÇÃO", + "4634601": "COMÉRCIO ATACADISTA DE CARNES BOVINAS E SUÍNAS E DERIVADOS", + "4634602": "COMÉRCIO ATACADISTA DE AVES ABATIDAS E DERIVADOS", + "4634603": "COMÉRCIO ATACADISTA DE PESCADOS E FRUTOS DO MAR", + "4634699": "COMÉRCIO ATACADISTA DE CARNES E DERIVADOS DE OUTROS ANIMAIS", + "4635401": "COMÉRCIO ATACADISTA DE ÁGUA MINERAL", + "4635402": "COMÉRCIO ATACADISTA DE CERVEJA, CHOPE E REFRIGERANTE", + "4635403": + "COMÉRCIO ATACADISTA DE BEBIDAS COM ATIVIDADE DE FRACIONAMENTO E ACONDICIONAMENTO ASSOCIADA", + "4635499": "COMÉRCIO ATACADISTA DE BEBIDAS NÃO ESPECIFICADAS ANTERIORMENTE", + "4636201": "COMÉRCIO ATACADISTA DE FUMO BENEFICIADO", + "4636202": "COMÉRCIO ATACADISTA DE CIGARROS, CIGARRILHAS E CHARUTOS", + "4637101": "COMÉRCIO ATACADISTA DE CAFÉ TORRADO, MOÍDO E SOLÚVEL", + "4637102": "COMÉRCIO ATACADISTA DE AÇÚCAR", + "4637103": "COMÉRCIO ATACADISTA DE ÓLEOS E GORDURAS", + "4637104": "COMÉRCIO ATACADISTA DE PÃES, BOLOS, BISCOITOS E SIMILARES", + "4637105": "COMÉRCIO ATACADISTA DE MASSAS ALIMENTÍCIAS", + "4637106": "COMÉRCIO ATACADISTA DE SORVETES", + "4637107": "COMÉRCIO ATACADISTA DE CHOCOLATES, CONFEITOS, BALAS, BOMBONS E SEMELHANTES", + "4637199": + "COMÉRCIO ATACADISTA ESPECIALIZADO EM OUTROS PRODUTOS ALIMENTÍCIOS NÃO ESPECIFICADOS ANTERIORMENTE", + "4639701": "COMÉRCIO ATACADISTA DE PRODUTOS ALIMENTÍCIOS EM GERAL", + "4639702": + "COMÉRCIO ATACADISTA DE PRODUTOS ALIMENTÍCIOS EM GERAL, COM ATIVIDADE DE FRACIONAMENTO E ACONDICIONAMENTO ASSOCIADA", + "4641901": "COMÉRCIO ATACADISTA DE TECIDOS", + "4641902": "COMÉRCIO ATACADISTA DE ARTIGOS DE CAMA, MESA E BANHO", + "4641903": "COMÉRCIO ATACADISTA DE ARTIGOS DE ARMARINHO", + "4642701": + "COMÉRCIO ATACADISTA DE ARTIGOS DO VESTUÁRIO E ACESSÓRIOS, EXCETO PROFISSIONAIS E DE SEGURANÇA", + "4642702": + "COMÉRCIO ATACADISTA DE ROUPAS E ACESSÓRIOS PARA USO PROFISSIONAL E DE SEGURANÇA DO TRABALHO", + "4643501": "COMÉRCIO ATACADISTA DE CALÇADOS", + "4643502": "COMÉRCIO ATACADISTA DE BOLSAS, MALAS E ARTIGOS DE VIAGEM", + "4644301": "COMÉRCIO ATACADISTA DE MEDICAMENTOS E DROGAS DE USO HUMANO", + "4644302": "COMÉRCIO ATACADISTA DE MEDICAMENTOS E DROGAS DE USO VETERINÁRIO", + "4645101": + "COMÉRCIO ATACADISTA DE INSTRUMENTOS E MATERIAIS PARA USO MÉDICO, CIRÚRGICO, HOSPITALAR E DE LABORATÓRIOS", + "4645102": "COMÉRCIO ATACADISTA DE PRÓTESES E ARTIGOS DE ORTOPEDIA", + "4645103": "COMÉRCIO ATACADISTA DE PRODUTOS ODONTOLÓGICOS", + "4646001": "COMÉRCIO ATACADISTA DE COSMÉTICOS E PRODUTOS DE PERFUMARIA", + "4646002": "COMÉRCIO ATACADISTA DE PRODUTOS DE HIGIENE PESSOAL", + "4647801": "COMÉRCIO ATACADISTA DE ARTIGOS DE ESCRITÓRIO E DE PAPELARIA", + "4647802": "COMÉRCIO ATACADISTA DE LIVROS, JORNAIS E OUTRAS PUBLICAÇÕES", + "4649401": "COMÉRCIO ATACADISTA DE EQUIPAMENTOS ELÉTRICOS DE USO PESSOAL E DOMÉSTICO", + "4649402": "COMÉRCIO ATACADISTA DE APARELHOS ELETRÔNICOS DE USO PESSOAL E DOMÉSTICO", + "4649403": "COMÉRCIO ATACADISTA DE BICICLETAS, TRICICLOS E OUTROS VEÍCULOS RECREATIVOS", + "4649404": "COMÉRCIO ATACADISTA DE MÓVEIS E ARTIGOS DE COLCHOARIA", + "4649405": "COMÉRCIO ATACADISTA DE ARTIGOS DE TAPEÇARIA; PERSIANAS E CORTINAS", + "4649406": "COMÉRCIO ATACADISTA DE LUSTRES, LUMINÁRIAS E ABAJURES", + "4649407": "COMÉRCIO ATACADISTA DE FILMES, CDS, DVDS, FITAS E DISCOS", + "4649408": "COMÉRCIO ATACADISTA DE PRODUTOS DE HIGIENE, LIMPEZA E CONSERVAÇÃO DOMICILIAR", + "4649409": + "COMÉRCIO ATACADISTA DE PRODUTOS DE HIGIENE, LIMPEZA E CONSERVAÇÃO DOMICILIAR, COM ATIVIDADE DE FRACIONAMENTO E ACONDICIONAMENTO ASSOCIADA", + "4649410": + "COMÉRCIO ATACADISTA DE JÓIAS, RELÓGIOS E BIJUTERIAS, INCLUSIVE PEDRAS PRECIOSAS E SEMIPRECIOSAS LAPIDADAS", + "4649499": + "COMÉRCIO ATACADISTA DE OUTROS EQUIPAMENTOS E ARTIGOS DE USO PESSOAL E DOMÉSTICO NÃO ESPECIFICADOS ANTERIORMENTE", + "4651601": "COMÉRCIO ATACADISTA DE EQUIPAMENTOS DE INFORMÁTICA", + "4651602": "COMÉRCIO ATACADISTA DE SUPRIMENTOS PARA INFORMÁTICA", + "4652400": + "COMÉRCIO ATACADISTA DE COMPONENTES ELETRÔNICOS E EQUIPAMENTOS DE TELEFONIA E COMUNICAÇÃO", + "4661300": + "COMÉRCIO ATACADISTA DE MÁQUINAS, APARELHOS E EQUIPAMENTOS PARA USO AGROPECUÁRIO; PARTES E PEÇAS", + "4662100": + "COMÉRCIO ATACADISTA DE MÁQUINAS, EQUIPAMENTOS PARA TERRAPLENAGEM, MINERAÇÃO E CONSTRUÇÃO; PARTES E PEÇAS", + "4663000": "COMÉRCIO ATACADISTA DE MÁQUINAS E EQUIPAMENTOS PARA USO INDUSTRIAL; PARTES E PEÇAS", + "4664800": + "COMÉRCIO ATACADISTA DE MÁQUINAS, APARELHOS E EQUIPAMENTOS PARA USO ODONTO MÉDICO HOSPITALAR; PARTES E PEÇAS", + "4665600": "COMÉRCIO ATACADISTA DE MÁQUINAS E EQUIPAMENTOS PARA USO COMERCIAL; PARTES E PEÇAS", + "4669901": "COMÉRCIO ATACADISTA DE BOMBAS E COMPRESSORES; PARTES E PEÇAS", + "4669999": + "COMÉRCIO ATACADISTA DE OUTRAS MÁQUINAS E EQUIPAMENTOS NÃO ESPECIFICADOS ANTERIORMENTE; PARTES E PEÇAS", + "4671100": "COMÉRCIO ATACADISTA DE MADEIRA E PRODUTOS DERIVADOS", + "4672900": "COMÉRCIO ATACADISTA DE FERRAGENS E FERRAMENTAS", + "4673700": "COMÉRCIO ATACADISTA DE MATERIAL ELÉTRICO", + "4674500": "COMÉRCIO ATACADISTA DE CIMENTO", + "4679601": "COMÉRCIO ATACADISTA DE TINTAS, VERNIZES E SIMILARES", + "4679602": "COMÉRCIO ATACADISTA DE MÁRMORES E GRANITOS", + "4679603": "COMÉRCIO ATACADISTA DE VIDROS, ESPELHOS E VITRAIS", + "4679604": + "COMÉRCIO ATACADISTA ESPECIALIZADO DE MATERIAIS DE CONSTRUÇÃO NÃO ESPECIFICADOS ANTERIORMENTE", + "4679699": "COMÉRCIO ATACADISTA DE MATERIAIS DE CONSTRUÇÃO EM GERAL", + "4681801": + "COMÉRCIO ATACADISTA DE ÁLCOOL CARBURANTE, BIODIESEL, GASOLINA E DEMAIS DERIVADOS DE PETRÓLEO, EXCETO LUBRIFICANTES, NÃO REALIZADO POR TRANSPORTADOR RETALHISTA (T.R.R.)", + "4681802": "COMÉRCIO ATACADISTA DE COMBUSTÍVEIS REALIZADO POR TRANSPORTADOR RETALHISTA (T.R.R.)", + "4681803": "COMÉRCIO ATACADISTA DE COMBUSTÍVEIS DE ORIGEM VEGETAL, EXCETO ÁLCOOL CARBURANTE", + "4681804": "COMÉRCIO ATACADISTA DE COMBUSTÍVEIS DE ORIGEM MINERAL EM BRUTO", + "4681805": "COMÉRCIO ATACADISTA DE LUBRIFICANTES", + "4682600": "COMÉRCIO ATACADISTA DE GÁS LIQÜEFEITO DE PETRÓLEO (GLP)", + "4683400": + "COMÉRCIO ATACADISTA DE DEFENSIVOS AGRÍCOLAS, ADUBOS, FERTILIZANTES E CORRETIVOS DO SOLO", + "4684201": "COMÉRCIO ATACADISTA DE RESINAS E ELASTÔMEROS", + "4684202": "COMÉRCIO ATACADISTA DE SOLVENTES", + "4684299": + "COMÉRCIO ATACADISTA DE OUTROS PRODUTOS QUÍMICOS E PETROQUÍMICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "4685100": "COMÉRCIO ATACADISTA DE PRODUTOS SIDERÚRGICOS E METALÚRGICOS, EXCETO PARA CONSTRUÇÃO", + "4686901": "COMÉRCIO ATACADISTA DE PAPEL E PAPELÃO EM BRUTO", + "4686902": "COMÉRCIO ATACADISTA DE EMBALAGENS", + "4687701": "COMÉRCIO ATACADISTA DE RESÍDUOS DE PAPEL E PAPELÃO", + "4687702": "COMÉRCIO ATACADISTA DE RESÍDUOS E SUCATAS NÃO METÁLICOS, EXCETO DE PAPEL E PAPELÃO", + "4687703": "COMÉRCIO ATACADISTA DE RESÍDUOS E SUCATAS METÁLICOS", + "4689301": "COMÉRCIO ATACADISTA DE PRODUTOS DA EXTRAÇÃO MINERAL, EXCETO COMBUSTÍVEIS", + "4689302": "COMÉRCIO ATACADISTA DE FIOS E FIBRAS BENEFICIADOS", + "4689399": + "COMÉRCIO ATACADISTA ESPECIALIZADO EM OUTROS PRODUTOS INTERMEDIÁRIOS NÃO ESPECIFICADOS ANTERIORMENTE", + "4691500": + "COMÉRCIO ATACADISTA DE MERCADORIAS EM GERAL, COM PREDOMINÂNCIA DE PRODUTOS ALIMENTÍCIOS", + "4692300": + "COMÉRCIO ATACADISTA DE MERCADORIAS EM GERAL, COM PREDOMINÂNCIA DE INSUMOS AGROPECUÁRIOS", + "4693100": + "COMÉRCIO ATACADISTA DE MERCADORIAS EM GERAL, SEM PREDOMINÂNCIA DE ALIMENTOS OU DE INSUMOS AGROPECUÁRIOS", + "4711301": + "COMÉRCIO VAREJISTA DE MERCADORIAS EM GERAL, COM PREDOMINÂNCIA DE PRODUTOS ALIMENTÍCIOS HIPERMERCADOS", + "4711302": + "COMÉRCIO VAREJISTA DE MERCADORIAS EM GERAL, COM PREDOMINÂNCIA DE PRODUTOS ALIMENTÍCIOS - SUPERMERCADOS", + "4712100": + "COMÉRCIO VAREJISTA DE MERCADORIAS EM GERAL, COM PREDOMINÂNCIA DE PRODUTOS ALIMENTÍCIOS - MINIMERCADOS, MERCEARIAS E ARMAZÉNS", + "4713002": "LOJAS DE VARIEDADES, EXCETO LOJAS DE DEPARTAMENTOS OU MAGAZINES", + "4713004": "LOJAS DE DEPARTAMENTOS OU MAGAZINES, EXCETO LOJAS FRANCAS (DUTY FREE)", + "4713005": "LOJAS FRANCAS (DUTY FREE) DE AEROPORTOS, PORTOS E EM FRONTEIRAS TERRESTRES", + "4721102": "PADARIA E CONFEITARIA COM PREDOMINÂNCIA DE REVENDA", + "4721103": "COMÉRCIO VAREJISTA DE LATICÍNIOS E FRIOS", + "4721104": "COMÉRCIO VAREJISTA DE DOCES, BALAS, BOMBONS E SEMELHANTES", + "4722901": "COMÉRCIO VAREJISTA DE CARNES - AÇOUGUES", + "4722902": "PEIXARIA", + "4723700": "COMÉRCIO VAREJISTA DE BEBIDAS", + "4724500": "COMÉRCIO VAREJISTA DE HORTIFRUTIGRANJEIROS", + "4729601": "TABACARIA", + "4729602": "COMÉRCIO VAREJISTA DE MERCADORIAS EM LOJAS DE CONVENIÊNCIA", + "4729699": + "COMÉRCIO VAREJISTA DE PRODUTOS ALIMENTÍCIOS EM GERAL OU ESPECIALIZADO EM PRODUTOS ALIMENTÍCIOS NÃO ESPECIFICADOS ANTERIORMENTE", + "4731800": "COMÉRCIO VAREJISTA DE COMBUSTÍVEIS PARA VEÍCULOS AUTOMOTORES", + "4732600": "COMÉRCIO VAREJISTA DE LUBRIFICANTES", + "4741500": "COMÉRCIO VAREJISTA DE TINTAS E MATERIAIS PARA PINTURA", + "4742300": "COMÉRCIO VAREJISTA DE MATERIAL ELÉTRICO", + "4743100": "COMÉRCIO VAREJISTA DE VIDROS", + "4744001": "COMÉRCIO VAREJISTA DE FERRAGENS E FERRAMENTAS", + "4744002": "COMÉRCIO VAREJISTA DE MADEIRA E ARTEFATOS", + "4744003": "COMÉRCIO VAREJISTA DE MATERIAIS HIDRÁULICOS", + "4744004": "COMÉRCIO VAREJISTA DE CAL, AREIA, PEDRA BRITADA, TIJOLOS E TELHAS", + "4744005": "COMÉRCIO VAREJISTA DE MATERIAIS DE CONSTRUÇÃO NÃO ESPECIFICADOS ANTERIORMENTE", + "4744006": "COMÉRCIO VAREJISTA DE PEDRAS PARA REVESTIMENTO", + "4744099": "COMÉRCIO VAREJISTA DE MATERIAIS DE CONSTRUÇÃO EM GERAL", + "4751201": "COMÉRCIO VAREJISTA ESPECIALIZADO DE EQUIPAMENTOS E SUPRIMENTOS DE INFORMÁTICA", + "4751202": "RECARGA DE CARTUCHOS PARA EQUIPAMENTOS DE INFORMÁTICA", + "4752100": "COMÉRCIO VAREJISTA ESPECIALIZADO DE EQUIPAMENTOS DE TELEFONIA E COMUNICAÇÃO", + "4753900": "COMÉRCIO VAREJISTA ESPECIALIZADO DE ELETRODOMÉSTICOS E EQUIPAMENTOS DE ÁUDIO E VÍDEO", + "4754701": "COMÉRCIO VAREJISTA DE MÓVEIS", + "4754702": "COMÉRCIO VAREJISTA DE ARTIGOS DE COLCHOARIA", + "4754703": "COMÉRCIO VAREJISTA DE ARTIGOS DE ILUMINAÇÃO", + "4755501": "COMÉRCIO VAREJISTA DE TECIDOS", + "4755502": "COMERCIO VAREJISTA DE ARTIGOS DE ARMARINHO", + "4755503": "COMERCIO VAREJISTA DE ARTIGOS DE CAMA, MESA E BANHO", + "4756300": "COMÉRCIO VAREJISTA ESPECIALIZADO DE INSTRUMENTOS MUSICAIS E ACESSÓRIOS", + "4757100": + "COMÉRCIO VAREJISTA ESPECIALIZADO DE PEÇAS E ACESSÓRIOS PARA APARELHOS ELETROELETRÔNICOS PARA USO DOMÉSTICO, EXCETO INFORMÁTICA E COMUNICAÇÃO", + "4759801": "COMÉRCIO VAREJISTA DE ARTIGOS DE TAPEÇARIA, CORTINAS E PERSIANAS", + "4759899": + "COMÉRCIO VAREJISTA DE OUTROS ARTIGOS DE USO PESSOAL E DOMÉSTICO NÃO ESPECIFICADOS ANTERIORMENTE", + "4761001": "COMÉRCIO VAREJISTA DE LIVROS", + "4761002": "COMÉRCIO VAREJISTA DE JORNAIS E REVISTAS", + "4761003": "COMÉRCIO VAREJISTA DE ARTIGOS DE PAPELARIA", + "4762800": "COMÉRCIO VAREJISTA DE DISCOS, CDS, DVDS E FITAS", + "4763601": "COMÉRCIO VAREJISTA DE BRINQUEDOS E ARTIGOS RECREATIVOS", + "4763602": "COMÉRCIO VAREJISTA DE ARTIGOS ESPORTIVOS", + "4763603": "COMÉRCIO VAREJISTA DE BICICLETAS E TRICICLOS; PEÇAS E ACESSÓRIOS", + "4763604": "COMÉRCIO VAREJISTA DE ARTIGOS DE CAÇA, PESCA E CAMPING", + "4763605": "COMÉRCIO VAREJISTA DE EMBARCAÇÕES E OUTROS VEÍCULOS RECREATIVOS; PEÇAS E ACESSÓRIOS", + "4771701": "COMÉRCIO VAREJISTA DE PRODUTOS FARMACÊUTICOS, SEM MANIPULAÇÃO DE FÓRMULAS", + "4771702": "COMÉRCIO VAREJISTA DE PRODUTOS FARMACÊUTICOS, COM MANIPULAÇÃO DE FÓRMULAS", + "4771703": "COMÉRCIO VAREJISTA DE PRODUTOS FARMACÊUTICOS HOMEOPÁTICOS", + "4771704": "COMÉRCIO VAREJISTA DE MEDICAMENTOS VETERINÁRIOS", + "4772500": "COMÉRCIO VAREJISTA DE COSMÉTICOS, PRODUTOS DE PERFUMARIA E DE HIGIENE PESSOAL", + "4773300": "COMÉRCIO VAREJISTA DE ARTIGOS MÉDICOS E ORTOPÉDICOS", + "4774100": "COMÉRCIO VAREJISTA DE ARTIGOS DE ÓPTICA", + "4781400": "COMÉRCIO VAREJISTA DE ARTIGOS DO VESTUÁRIO E ACESSÓRIOS", + "4782201": "COMÉRCIO VAREJISTA DE CALÇADOS", + "4782202": "COMÉRCIO VAREJISTA DE ARTIGOS DE VIAGEM", + "4783101": "COMÉRCIO VAREJISTA DE ARTIGOS DE JOALHERIA", + "4783102": "COMÉRCIO VAREJISTA DE ARTIGOS DE RELOJOARIA", + "4784900": "COMÉRCIO VAREJISTA DE GÁS LIQÜEFEITO DE PETRÓLEO (GLP)", + "4785701": "COMÉRCIO VAREJISTA DE ANTIGÜIDADES", + "4785799": "COMÉRCIO VAREJISTA DE OUTROS ARTIGOS USADOS", + "4789001": "COMÉRCIO VAREJISTA DE SUVENIRES, BIJUTERIAS E ARTESANATOS", + "4789002": "COMÉRCIO VAREJISTA DE PLANTAS E FLORES NATURAIS", + "4789003": "COMÉRCIO VAREJISTA DE OBJETOS DE ARTE", + "4789004": + "COMÉRCIO VAREJISTA DE ANIMAIS VIVOS E DE ARTIGOS E ALIMENTOS PARA ANIMAIS DE ESTIMAÇÃO", + "4789005": "COMÉRCIO VAREJISTA DE PRODUTOS SANEANTES DOMISSANITÁRIOS", + "4789006": "COMÉRCIO VAREJISTA DE FOGOS DE ARTIFÍCIO E ARTIGOS PIROTÉCNICOS", + "4789007": "COMÉRCIO VAREJISTA DE EQUIPAMENTOS PARA ESCRITÓRIO", + "4789008": "COMÉRCIO VAREJISTA DE ARTIGOS FOTOGRÁFICOS E PARA FILMAGEM", + "4789009": "COMÉRCIO VAREJISTA DE ARMAS E MUNIÇÕES", + "4789099": "COMÉRCIO VAREJISTA DE OUTROS PRODUTOS NÃO ESPECIFICADOS ANTERIORMENTE", + "4911600": "TRANSPORTE FERROVIÁRIO DE CARGA", + "4912401": "TRANSPORTE FERROVIÁRIO DE PASSAGEIROS INTERMUNICIPAL E INTERESTADUAL", + "4912402": "TRANSPORTE FERROVIÁRIO DE PASSAGEIROS MUNICIPAL E EM REGIÃO METROPOLITANA", + "4912403": "TRANSPORTE METROVIÁRIO", + "4921301": "TRANSPORTE RODOVIÁRIO COLETIVO DE PASSAGEIROS, COM ITINERÁRIO FIXO, MUNICIPAL", + "4921302": + "TRANSPORTE RODOVIÁRIO COLETIVO DE PASSAGEIROS, COM ITINERÁRIO FIXO, INTERMUNICIPAL EM REGIÃO METROPOLITANA", + "4922101": + "TRANSPORTE RODOVIÁRIO COLETIVO DE PASSAGEIROS, COM ITINERÁRIO FIXO, INTERMUNICIPAL, EXCETO EM REGIÃO METROPOLITANA", + "4922102": "TRANSPORTE RODOVIÁRIO COLETIVO DE PASSAGEIROS, COM ITINERÁRIO FIXO, INTERESTADUAL", + "4922103": "TRANSPORTE RODOVIÁRIO COLETIVO DE PASSAGEIROS, COM ITINERÁRIO FIXO, INTERNACIONAL", + "4923001": "SERVIÇO DE TÁXI", + "4923002": "SERVIÇO DE TRANSPORTE DE PASSAGEIROS - LOCAÇÃO DE AUTOMÓVEIS COM MOTORISTA", + "4924800": "TRANSPORTE ESCOLAR", + "4929901": "TRANSPORTE RODOVIÁRIO COLETIVO DE PASSAGEIROS, SOB REGIME DE FRETAMENTO, MUNICIPAL", + "4929902": + "TRANSPORTE RODOVIÁRIO COLETIVO DE PASSAGEIROS, SOB REGIME DE FRETAMENTO, INTERMUNICIPAL, INTERESTADUAL E INTERNACIONAL", + "4929903": "ORGANIZAÇÃO DE EXCURSÕES EM VEÍCULOS RODOVIÁRIOS PRÓPRIOS, MUNICIPAL", + "4929904": + "ORGANIZAÇÃO DE EXCURSÕES EM VEÍCULOS RODOVIÁRIOS PRÓPRIOS, INTERMUNICIPAL, INTERESTADUAL E INTERNACIONAL", + "4929999": "OUTROS TRANSPORTES RODOVIÁRIOS DE PASSAGEIROS NÃO ESPECIFICADOS ANTERIORMENTE", + "4930201": "TRANSPORTE RODOVIÁRIO DE CARGA, EXCETO PRODUTOS PERIGOSOS E MUDANÇAS, MUNICIPAL", + "4930202": + "TRANSPORTE RODOVIÁRIO DE CARGA, EXCETO PRODUTOS PERIGOSOS E MUDANÇAS, INTERMUNICIPAL, INTERESTADUAL E INTERNACIONAL", + "4930203": "TRANSPORTE RODOVIÁRIO DE PRODUTOS PERIGOSOS", + "4930204": "TRANSPORTE RODOVIÁRIO DE MUDANÇAS", + "4940000": "TRANSPORTE DUTOVIÁRIO", + "4950700": "TRENS TURÍSTICOS, TELEFÉRICOS E SIMILARES", + "5011401": "TRANSPORTE MARÍTIMO DE CABOTAGEM - CARGA", + "5011402": "TRANSPORTE MARÍTIMO DE CABOTAGEM - PASSAGEIROS", + "5012201": "TRANSPORTE MARÍTIMO DE LONGO CURSO - CARGA", + "5012202": "TRANSPORTE MARÍTIMO DE LONGO CURSO - PASSAGEIROS", + "5021101": "TRANSPORTE POR NAVEGAÇÃO INTERIOR DE CARGA, MUNICIPAL, EXCETO TRAVESSIA", + "5021102": + "TRANSPORTE POR NAVEGAÇÃO INTERIOR DE CARGA, INTERMUNICIPAL, INTERESTADUAL E INTERNACIONAL, EXCETO TRAVESSIA", + "5022001": + "TRANSPORTE POR NAVEGAÇÃO INTERIOR DE PASSAGEIROS EM LINHAS REGULARES, MUNICIPAL, EXCETO TRAVESSIA", + "5022002": + "TRANSPORTE POR NAVEGAÇÃO INTERIOR DE PASSAGEIROS EM LINHAS REGULARES, INTERMUNICIPAL, INTERESTADUAL E INTERNACIONAL, EXCETO TRAVESSIA", + "5030101": "NAVEGAÇÃO DE APOIO MARÍTIMO", + "5030102": "NAVEGAÇÃO DE APOIO PORTUÁRIO", + "5030103": "SERVIÇO DE REBOCADORES E EMPURRADORES", + "5091201": "TRANSPORTE POR NAVEGAÇÃO DE TRAVESSIA, MUNICIPAL", + "5091202": "TRANSPORTE POR NAVEGAÇÃO DE TRAVESSIA INTERMUNICIPAL, INTERESTADUAL E INTERNACIONAL", + "5099801": "TRANSPORTE AQUAVIÁRIO PARA PASSEIOS TURÍSTICOS", + "5099899": "OUTROS TRANSPORTES AQUAVIÁRIOS NÃO ESPECIFICADOS ANTERIORMENTE", + "5111100": "TRANSPORTE AÉREO DE PASSAGEIROS REGULAR", + "5112901": "SERVIÇO DE TÁXI AÉREO E LOCAÇÃO DE AERONAVES COM TRIPULAÇÃO", + "5112999": "OUTROS SERVIÇOS DE TRANSPORTE AÉREO DE PASSAGEIROS NÃO REGULAR", + "5120000": "TRANSPORTE AÉREO DE CARGA", + "5130700": "TRANSPORTE ESPACIAL", + "5211701": "ARMAZÉNS GERAIS - EMISSÃO DE WARRANT", + "5211702": "GUARDA MÓVEIS", + "5211799": "DEPÓSITOS DE MERCADORIAS PARA TERCEIROS, EXCETO ARMAZÉNS GERAIS E GUARDA MÓVEIS", + "5212500": "CARGA E DESCARGA", + "5221400": "CONCESSIONÁRIAS DE RODOVIAS, PONTES, TÚNEIS E SERVIÇOS RELACIONADOS", + "5222200": "TERMINAIS RODOVIÁRIOS E FERROVIÁRIOS", + "5223100": "ESTACIONAMENTO DE VEÍCULOS", + "5229001": "SERVIÇOS DE APOIO AO TRANSPORTE POR TÁXI, INCLUSIVE CENTRAIS DE CHAMADA", + "5229002": "SERVIÇOS DE REBOQUE DE VEÍCULOS", + "5229099": + "OUTRAS ATIVIDADES AUXILIARES DOS TRANSPORTES TERRESTRES NÃO ESPECIFICADAS ANTERIORMENTE", + "5231101": "ADMINISTRAÇÃO DA INFRAESTRUTURA PORTUÁRIA", + "5231102": "ATIVIDADES DO OPERADOR PORTUÁRIO", + "5231103": "GESTÃO DE TERMINAIS AQUAVIÁRIOS", + "5232000": "ATIVIDADES DE AGENCIAMENTO MARÍTIMO", + "5239701": "SERVIÇOS DE PRATICAGEM", + "5239799": "ATIVIDADES AUXILIARES DOS TRANSPORTES AQUAVIÁRIOS NÃO ESPECIFICADAS ANTERIORMENTE", + "5240101": "OPERAÇÃO DOS AEROPORTOS E CAMPOS DE ATERRISSAGEM", + "5240199": + "ATIVIDADES AUXILIARES DOS TRANSPORTES AÉREOS, EXCETO OPERAÇÃO DOS AEROPORTOS E CAMPOS DE ATERRISSAGEM", + "5250801": "COMISSARIA DE DESPACHOS", + "5250802": "ATIVIDADES DE DESPACHANTES ADUANEIROS", + "5250803": "AGENCIAMENTO DE CARGAS, EXCETO PARA O TRANSPORTE MARÍTIMO", + "5250804": "ORGANIZAÇÃO LOGÍSTICA DO TRANSPORTE DE CARGA", + "5250805": "OPERADOR DE TRANSPORTE MULTIMODAL - OTM", + "5310501": "ATIVIDADES DO CORREIO NACIONAL", + "5310502": "ATIVIDADES DE FRANQUEADAS DO CORREIO NACIONAL", + "5320201": "SERVIÇOS DE MALOTE NÃO REALIZADOS PELO CORREIO NACIONAL", + "5320202": "SERVIÇOS DE ENTREGA RÁPIDA", + "5510801": "HOTÉIS", + "5510802": "APART HOTÉIS", + "5510803": "MOTÉIS", + "5590601": "ALBERGUES, EXCETO ASSISTENCIAIS", + "5590602": "CAMPINGS", + "5590603": "PENSÕES(ALOJAMENTO)", + "5590699": "OUTROS ALOJAMENTOS NÃO ESPECIFICADOS ANTERIORMENTE", + "5611201": "RESTAURANTES E SIMILARES", + "5611203": "LANCHONETES, CASAS DE CHÁ, DE SUCOS E SIMILARES", + "5611204": "BARES E OUTROS ESTABELECIMENTOS ESPECIALIZADOS EM SERVIR BEBIDAS, SEM ENTRETENIMENTO", + "5611205": + "BARES E OUTROS ESTABELECIMENTOS ESPECIALIZADOS EM SERVIR BEBIDAS, COM ENTRETENIMENTO ", + "5612100": "SERVIÇOS AMBULANTES DE ALIMENTAÇÃO", + "5620101": "FORNECIMENTO DE ALIMENTOS PREPARADOS PREPONDERANTEMENTE PARA EMPRESAS", + "5620102": "SERVIÇOS DE ALIMENTAÇÃO PARA EVENTOS E RECEPÇÕES - BUFÊ", + "5620103": "CANTINAS - SERVIÇOS DE ALIMENTAÇÃO PRIVATIVOS", + "5620104": "FORNECIMENTO DE ALIMENTOS PREPARADOS PREPONDERANTEMENTE PARA CONSUMO DOMICILIAR", + "5811500": "EDIÇÃO DE LIVROS", + "5812301": "EDIÇÃO DE JORNAIS DIÁRIOS", + "5812302": "EDIÇÃO DE JORNAIS NÃO DIÁRIOS", + "5813100": "EDIÇÃO DE REVISTAS", + "5819100": "EDIÇÃO DE CADASTROS, LISTAS E DE OUTROS PRODUTOS GRÁFICOS", + "5821200": "EDIÇÃO INTEGRADA À IMPRESSÃO DE LIVROS", + "5822101": "EDIÇÃO INTEGRADA À IMPRESSÃO DE JORNAIS DIÁRIOS", + "5822102": "EDIÇÃO INTEGRADA À IMPRESSÃO DE JORNAIS NÃO DIÁRIOS", + "5823900": "EDIÇÃO INTEGRADA À IMPRESSÃO DE REVISTAS", + "5829800": "EDIÇÃO INTEGRADA À IMPRESSÃO DE CADASTROS, LISTAS E DE OUTROS PRODUTOS GRÁFICOS", + "5911101": "ESTÚDIOS CINEMATOGRÁFICOS", + "5911102": "PRODUÇÃO DE FILMES PARA PUBLICIDADE", + "5911199": + "ATIVIDADES DE PRODUÇÃO CINEMATOGRÁFICA, DE VÍDEOS E DE PROGRAMAS DE TELEVISÃO NÃO ESPECIFICADAS ANTERIORMENTE", + "5912001": "SERVIÇOS DE DUBLAGEM", + "5912002": "SERVIÇOS DE MIXAGEM SONORA EM PRODUÇÃO AUDIOVISUAL", + "5912099": + "ATIVIDADES DE PÓS PRODUÇÃO CINEMATOGRÁFICA, DE VÍDEOS E DE PROGRAMAS DE TELEVISÃO NÃO ESPECIFICADAS ANTERIORMENTE", + "5913800": "DISTRIBUIÇÃO CINEMATOGRÁFICA, DE VÍDEO E DE PROGRAMAS DE TELEVISÃO", + "5914600": "ATIVIDADES DE EXIBIÇÃO CINEMATOGRÁFICA", + "5920100": "ATIVIDADES DE GRAVAÇÃO DE SOM E DE EDIÇÃO DE MÚSICA", + "6010100": "ATIVIDADES DE RÁDIO", + "6021700": "ATIVIDADES DE TELEVISÃO ABERTA", + "6022501": "PROGRAMADORAS", + "6022502": "ATIVIDADES RELACIONADAS À TELEVISÃO POR ASSINATURA, EXCETO PROGRAMADORAS", + "6110801": "SERVIÇOS DE TELEFONIA FIXA COMUTADA - STFC", + "6110802": "SERVIÇOS DE REDES DE TRANSPORTES DE TELECOMUNICAÇÕES - SRTT", + "6110803": "SERVIÇOS DE COMUNICAÇÃO MULTIMÍDIA - SCM", + "6110899": "SERVIÇOS DE TELECOMUNICAÇÕES POR FIO NÃO ESPECIFICADOS ANTERIORMENTE", + "6120501": "TELEFONIA MÓVEL CELULAR", + "6120502": "SERVIÇO MÓVEL ESPECIALIZADO - SME", + "6120599": "SERVIÇOS DE TELECOMUNICAÇÕES SEM FIO NÃO ESPECIFICADOS ANTERIORMENTE", + "6130200": "TELECOMUNICAÇÕES POR SATÉLITE", + "6141800": "OPERADORAS DE TELEVISÃO POR ASSINATURA POR CABO", + "6142600": "OPERADORAS DE TELEVISÃO POR ASSINATURA POR MICROONDAS", + "6143400": "OPERADORAS DE TELEVISÃO POR ASSINATURA POR SATÉLITE", + "6190601": "PROVEDORES DE ACESSO ÀS REDES DE COMUNICAÇÕES", + "6190602": "PROVEDORES DE VOZ SOBRE PROTOCOLO INTERNET - VOIP", + "6190699": "OUTRAS ATIVIDADES DE TELECOMUNICAÇÕES NÃO ESPECIFICADAS ANTERIORMENTE", + "6201501": "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA", + "6201502": "WEB DESIGN", + "6202300": "DESENVOLVIMENTO E LICENCIAMENTO DE PROGRAMAS DE COMPUTADOR CUSTOMIZÁVEIS", + "6203100": "DESENVOLVIMENTO E LICENCIAMENTO DE PROGRAMAS DE COMPUTADOR NÃO CUSTOMIZÁVEIS", + "6204000": "CONSULTORIA EM TECNOLOGIA DA INFORMAÇÃO", + "6209100": "SUPORTE TÉCNICO, MANUTENÇÃO E OUTROS SERVIÇOS EM TECNOLOGIA DA INFORMAÇÃO", + "6311900": + "TRATAMENTO DE DADOS, PROVEDORES DE SERVIÇOS DE APLICAÇÃO E SERVIÇOS DE HOSPEDAGEM NA INTERNET", + "6319400": "PORTAIS, PROVEDORES DE CONTEÚDO E OUTROS SERVIÇOS DE INFORMAÇÃO NA INTERNET", + "6391700": "AGÊNCIAS DE NOTÍCIAS", + "6399200": + "OUTRAS ATIVIDADES DE PRESTAÇÃO DE SERVIÇOS DE INFORMAÇÃO NÃO ESPECIFICADAS ANTERIORMENTE", + "6410700": "BANCO CENTRAL", + "6421200": "BANCOS COMERCIAIS", + "6422100": "BANCOS MÚLTIPLOS, COM CARTEIRA COMERCIAL", + "6423900": "CAIXAS ECONÔMICAS", + "6424701": "BANCOS COOPERATIVOS", + "6424702": "COOPERATIVAS CENTRAIS DE CRÉDITO", + "6424703": "COOPERATIVAS DE CRÉDITO MÚTUO", + "6424704": "COOPERATIVAS DE CRÉDITO RURAL", + "6431000": "BANCOS MÚLTIPLOS, SEM CARTEIRA COMERCIAL", + "6432800": "BANCOS DE INVESTIMENTO", + "6433600": "BANCOS DE DESENVOLVIMENTO", + "6434400": "AGÊNCIAS DE FOMENTO", + "6435201": "SOCIEDADES DE CRÉDITO IMOBILIÁRIO", + "6435202": "ASSOCIAÇÕES DE POUPANÇA E EMPRÉSTIMO", + "6435203": "COMPANHIAS HIPOTECÁRIAS", + "6436100": "SOCIEDADES DE CRÉDITO, FINANCIAMENTO E INVESTIMENTO - FINANCEIRAS", + "6437900": "SOCIEDADES DE CRÉDITO AO MICROEMPREENDEDOR", + "6438701": "BANCOS DE CÂMBIO", + "6438799": "OUTRAS INSTITUIÇÕES DE INTERMEDIAÇÃO NÃO MONETÁRIA", + "6440900": "ARRENDAMENTO MERCANTIL", + "6450600": "SOCIEDADES DE CAPITALIZAÇÃO", + "6461100": "HOLDINGS DE INSTITUIÇÕES FINANCEIRAS", + "6462000": "HOLDINGS DE INSTITUIÇÕES NÃO FINANCEIRAS", + "6463800": "OUTRAS SOCIEDADES DE PARTICIPAÇÃO, EXCETO HOLDINGS", + "6470101": "FUNDOS DE INVESTIMENTO, EXCETO PREVIDENCIÁRIOS E IMOBILIÁRIOS", + "6470102": "FUNDOS DE INVESTIMENTO PREVIDENCIÁRIOS", + "6470103": "FUNDOS DE INVESTIMENTO IMOBILIÁRIOS", + "6491300": "SOCIEDADES DE FOMENTO MERCANTIL - FACTORING", + "6492100": "SECURITIZAÇÃO DE CRÉDITOS", + "6493000": "ADMINISTRAÇÃO DE CONSÓRCIOS PARA AQUISIÇÃO DE BENS E DIREITOS", + "6499901": "CLUBES DE INVESTIMENTO", + "6499902": "SOCIEDADES DE INVESTIMENTO", + "6499903": "FUNDO GARANTIDOR DE CRÉDITO", + "6499904": "CAIXAS DE FINANCIAMENTO DE CORPORAÇÕES", + "6499905": "CONCESSÃO DE CRÉDITO PELAS OSCIP", + "6499999": "OUTRAS ATIVIDADES DE SERVIÇOS FINANCEIROS NÃO ESPECIFICADAS ANTERIORMENTE", + "6511101": "SOCIEDADE SEGURADORA DE SEGUROS VIDA", + "6511102": "PLANOS DE AUXÍLIO FUNERAL", + "6512000": "SOCIEDADE SEGURADORA DE SEGUROS NÃO VIDA", + "6520100": "SOCIEDADE SEGURADORA DE SEGUROS SAÚDE", + "6530800": "RESSEGUROS", + "6541300": "PREVIDÊNCIA COMPLEMENTAR FECHADA", + "6542100": "PREVIDÊNCIA COMPLEMENTAR ABERTA", + "6550200": "PLANOS DE SAÚDE", + "6611801": "BOLSA DE VALORES", + "6611802": "BOLSA DE MERCADORIAS", + "6611803": "BOLSA DE MERCADORIAS E FUTUROS", + "6611804": "ADMINISTRAÇÃO DE MERCADOS DE BALCÃO ORGANIZADOS", + "6612601": "CORRETORAS DE TÍTULOS E VALORES MOBILIÁRIOS", + "6612602": "DISTRIBUIDORAS DE TÍTULOS E VALORES MOBILIÁRIOS", + "6612603": "CORRETORAS DE CÂMBIO", + "6612604": "CORRETORAS DE CONTRATOS DE MERCADORIAS", + "6612605": "AGENTES DE INVESTIMENTOS EM APLICAÇÕES FINANCEIRAS", + "6613400": "ADMINISTRAÇÃO DE CARTÕES DE CRÉDITO", + "6619301": "SERVIÇOS DE LIQUIDAÇÃO E CUSTÓDIA", + "6619302": "CORRESPONDENTES DE INSTITUIÇÕES FINANCEIRAS", + "6619303": "REPRESENTAÇÕES DE BANCOS ESTRANGEIROS", + "6619304": "CAIXAS ELETRÔNICOS", + "6619305": "OPERADORAS DE CARTÕES DE DÉBITO", + "6619399": + "OUTRAS ATIVIDADES AUXILIARES DOS SERVIÇOS FINANCEIROS NÃO ESPECIFICADAS ANTERIORMENTE", + "6621501": "PERITOS E AVALIADORES DE SEGUROS", + "6621502": "AUDITORIA E CONSULTORIA ATUARIAL", + "6622300": "CORRETORES E AGENTES DE SEGUROS, DE PLANOS DE PREVIDÊNCIA COMPLEMENTAR E DE SAÚDE", + "6629100": + "ATIVIDADES AUXILIARES DOS SEGUROS, DA PREVIDÊNCIA COMPLEMENTAR E DOS PLANOS DE SAÚDE NÃO ESPECIFICADAS ANTERIORMENTE", + "6630400": "ATIVIDADES DE ADMINISTRAÇÃO DE FUNDOS POR CONTRATO OU COMISSÃO", + "6810201": "COMPRA E VENDA DE IMÓVEIS PRÓPRIOS", + "6810202": "ALUGUEL DE IMÓVEIS PRÓPRIOS", + "6810203": "LOTEAMENTO DE IMÓVEIS PRÓPRIOS", + "6821801": "CORRETAGEM NA COMPRA E VENDA E AVALIAÇÃO DE IMÓVEIS", + "6821802": "CORRETAGEM NO ALUGUEL DE IMÓVEIS", + "6822600": "GESTÃO E ADMINISTRAÇÃO DA PROPRIEDADE IMOBILIARIA", + "6911701": "SERVIÇOS ADVOCATÍCIOS", + "6911702": "ATIVIDADES AUXILIARES DA JUSTIÇA", + "6911703": "AGENTE DE PROPRIEDADE INDUSTRIAL", + "6912500": "CARTÓRIOS", + "6920601": "ATIVIDADES DE CONTABILIDADE", + "6920602": "ATIVIDADES DE CONSULTORIA E AUDITORIA CONTÁBIL E TRIBUTÁRIA", + "7020400": + "ATIVIDADES DE CONSULTORIA EM GESTÃO EMPRESARIAL, EXCETO CONSULTORIA TÉCNICA ESPECÍFICA", + "7111100": "SERVIÇOS DE ARQUITETURA", + "7112000": "SERVIÇOS DE ENGENHARIA", + "7119701": "SERVIÇOS DE CARTOGRAFIA, TOPOGRAFIA E GEODÉSIA", + "7119702": "ATIVIDADES DE ESTUDOS GEOLÓGICOS", + "7119703": "SERVIÇOS DE DESENHO TÉCNICO RELACIONADOS À ARQUITETURA E ENGENHARIA", + "7119704": "SERVIÇOS DE PERÍCIA TÉCNICA RELACIONADOS À SEGURANÇA DO TRABALHO", + "7119799": + "ATIVIDADES TÉCNICAS RELACIONADAS À ENGENHARIA E ARQUITETURA NÃO ESPECIFICADAS ANTERIORMENTE", + "7120100": "TESTES E ANÁLISES TÉCNICAS", + "7210000": "PESQUISA E DESENVOLVIMENTO EXPERIMENTAL EM CIÊNCIAS FÍSICAS E NATURAIS", + "7220700": "PESQUISA E DESENVOLVIMENTO EXPERIMENTAL EM CIÊNCIAS SOCIAIS E HUMANAS", + "7311400": "AGÊNCIAS DE PUBLICIDADE", + "7312200": "AGENCIAMENTO DE ESPAÇOS PARA PUBLICIDADE, EXCETO EM VEÍCULOS DE COMUNICAÇÃO", + "7319001": "CRIAÇÃO ESTANDES PARA FEIRAS E EXPOSIÇÕES", + "7319002": "PROMOÇÃO DE VENDAS", + "7319003": "MARKETING DIRETO", + "7319004": "CONSULTORIA EM PUBLICIDADE", + "7319099": "OUTRAS ATIVIDADES DE PUBLICIDADE NÃO ESPECIFICADAS ANTERIORMENTE", + "7320300": "PESQUISAS DE MERCADO E DE OPINIÃO PÚBLICA", + "7410202": "DESIGN DE INTERIORES", + "7410203": "DESIGN DE PRODUTO", + "7410299": "ATIVIDADES DE DESIGN NÃO ESPECIFICADAS ANTERIORMENTE", + "7420001": "ATIVIDADES DE PRODUÇÃO DE FOTOGRAFIAS, EXCETO AÉREA E SUBMARINA", + "7420002": "ATIVIDADES DE PRODUÇÃO DE FOTOGRAFIAS AÉREAS E SUBMARINAS", + "7420003": "LABORATÓRIOS FOTOGRÁFICOS", + "7420004": "FILMAGEM DE FESTAS E EVENTOS", + "7420005": "SERVIÇOS DE MICROFILMAGEM", + "7490101": "SERVIÇOS DE TRADUÇÃO, INTERPRETAÇÃO E SIMILARES", + "7490102": "ESCAFANDRIA E MERGULHO", + "7490103": "SERVIÇOS DE AGRONOMIA E DE CONSULTORIA ÀS ATIVIDADES AGRÍCOLAS E PECUÁRIAS", + "7490104": + "ATIVIDADES DE INTERMEDIAÇÃO E AGENCIAMENTO DE SERVIÇOS E NEGÓCIOS EM GERAL, EXCETO IMOBILIÁRIOS", + "7490105": "AGENCIAMENTO DE PROFISSIONAIS PARA ATIVIDADES ESPORTIVAS, CULTURAIS E ARTÍSTICAS", + "7490199": + "OUTRAS ATIVIDADES PROFISSIONAIS, CIENTÍFICAS E TÉCNICAS NÃO ESPECIFICADAS ANTERIORMENTE", + "7500100": "ATIVIDADES VETERINÁRIAS", + "7711000": "LOCAÇÃO DE AUTOMÓVEIS SEM CONDUTOR", + "7719501": "LOCAÇÃO DE EMBARCAÇÕES SEM TRIPULAÇÃO, EXCETO PARA FINS RECREATIVOS", + "7719502": "LOCAÇÃO DE AERONAVES SEM TRIPULAÇÃO", + "7719599": "LOCAÇÃO DE OUTROS MEIOS DE TRANSPORTE NÃO ESPECIFICADOS ANTERIORMENTE, SEM CONDUTOR", + "7721700": "ALUGUEL DE EQUIPAMENTOS RECREATIVOS E ESPORTIVOS", + "7722500": "ALUGUEL DE FITAS DE VÍDEO, DVDS E SIMILARES", + "7723300": "ALUGUEL DE OBJETOS DO VESTUÁRIO, JÓIAS E ACESSÓRIOS", + "7729201": "ALUGUEL DE APARELHOS DE JOGOS ELETRÔNICOS", + "7729202": + "ALUGUEL DE MÓVEIS, UTENSÍLIOS E APARELHOS DE USO DOMÉSTICO E PESSOAL; INSTRUMENTOS MUSICAIS", + "7729203": "ALUGUEL DE MATERIAL MÉDICO", + "7729299": "ALUGUEL DE OUTROS OBJETOS PESSOAIS E DOMÉSTICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "7731400": "ALUGUEL DE MÁQUINAS E EQUIPAMENTOS AGRÍCOLAS SEM OPERADOR", + "7732201": "ALUGUEL DE MÁQUINAS E EQUIPAMENTOS PARA CONSTRUÇÃO SEM OPERADOR, EXCETO ANDAIMES", + "7732202": "ALUGUEL DE ANDAIMES", + "7733100": "ALUGUEL DE MÁQUINAS E EQUIPAMENTOS PARA ESCRITÓRIOS", + "7739001": + "ALUGUEL DE MÁQUINAS E EQUIPAMENTOS PARA EXTRAÇÃO DE MINÉRIOS E PETRÓLEO, SEM OPERADOR", + "7739002": "ALUGUEL DE EQUIPAMENTOS CIENTÍFICOS, MÉDICOS E HOSPITALARES, SEM OPERADOR", + "7739003": "ALUGUEL DE PALCOS, COBERTURAS E OUTRAS ESTRUTURAS DE USO TEMPORÁRIO, EXCETO ANDAIMES", + "7739099": + "ALUGUEL DE OUTRAS MÁQUINAS E EQUIPAMENTOS COMERCIAIS E INDUSTRIAIS NÃO ESPECIFICADOS ANTERIORMENTE, SEM OPERADOR", + "7740300": "GESTÃO DE ATIVOS INTANGÍVEIS NÃO FINANCEIROS", + "7810800": "SELEÇÃO E AGENCIAMENTO DE MÃO DE OBRA", + "7820500": "LOCAÇÃO DE MÃO DE OBRA TEMPORÁRIA", + "7830200": "FORNECIMENTO E GESTÃO DE RECURSOS HUMANOS PARA TERCEIROS", + "7911200": "AGÊNCIAS DE VIAGENS", + "7912100": "OPERADORES TURÍSTICOS", + "7990200": "SERVIÇOS DE RESERVAS E OUTROS SERVIÇOS DE TURISMO NÃO ESPECIFICADOS ANTERIORMENTE", + "8011101": "ATIVIDADES DE VIGILÂNCIA E SEGURANÇA PRIVADA", + "8011102": "SERVIÇOS DE ADESTRAMENTO DE CÃES DE GUARDA", + "8012900": "ATIVIDADES DE TRANSPORTE DE VALORES", + "8020001": "ATIVIDADES DE MONITORAMENTO DE SISTEMAS DE SEGURANÇA ELETRÔNICO", + "8020002": "OUTRAS ATIVIDADES DE SERVIÇOS DE SEGURANÇA", + "8030700": "ATIVIDADES DE INVESTIGAÇÃO PARTICULAR", + "8111700": "SERVIÇOS COMBINADOS PARA APOIO A EDIFÍCIOS, EXCETO CONDOMÍNIOS PREDIAIS", + "8112500": "CONDOMÍNIOS PREDIAIS", + "8121400": "LIMPEZA EM PRÉDIOS E EM DOMICÍLIOS", + "8122200": "IMUNIZAÇÃO E CONTROLE DE PRAGAS URBANAS", + "8129000": "ATIVIDADES DE LIMPEZA NÃO ESPECIFICADAS ANTERIORMENTE", + "8130300": "ATIVIDADES PAISAGÍSTICAS", + "8211300": "SERVIÇOS COMBINADOS DE ESCRITÓRIO E APOIO ADMINISTRATIVO", + "8219901": "FOTOCÓPIAS", + "8219999": + "PREPARAÇÃO DE DOCUMENTOS E SERVIÇOS ESPECIALIZADOS DE APOIO ADMINISTRATIVO NÃO ESPECIFICADOS ANTERIORMENTE", + "8220200": "ATIVIDADES DE TELEATENDIMENTO", + "8230001": "SERVIÇOS DE ORGANIZAÇÃO DE FEIRAS, CONGRESSOS, EXPOSIÇÕES E FESTAS", + "8230002": "CASAS DE FESTAS E EVENTOS", + "8291100": "ATIVIDADES DE COBRANÇAS E INFORMAÇÕES CADASTRAIS", + "8292000": "ENVASAMENTO E EMPACOTAMENTO SOB CONTRATO", + "8299701": "MEDIÇÃO DE CONSUMO DE ENERGIA ELÉTRICA, GÁS E ÁGUA", + "8299702": "EMISSÃO DE VALES ALIMENTAÇÃO, VALES TRANSPORTE E SIMILARES", + "8299703": "SERVIÇOS DE GRAVAÇÃO DE CARIMBOS, EXCETO CONFECÇÃO", + "8299704": "LEILOEIROS INDEPENDENTES", + "8299705": "SERVIÇOS DE LEVANTAMENTO DE FUNDOS SOB CONTRATO", + "8299706": "CASAS LOTÉRICAS", + "8299707": "SALAS DE ACESSO À INTERNET", + "8299799": + "OUTRAS ATIVIDADES DE SERVIÇOS PRESTADOS PRINCIPALMENTE ÀS EMPRESAS NÃO ESPECIFICADAS ANTERIORMENTE", + "8411600": "ADMINISTRAÇÃO PÚBLICA EM GERAL", + "8412400": + "REGULAÇÃO DAS ATIVIDADES DE SAÚDE, EDUCAÇÃO, SERVIÇOS CULTURAIS E OUTROS SERVIÇOS SOCIAIS", + "8413200": "REGULAÇÃO DAS ATIVIDADES ECONÔMICAS", + "8421300": "RELAÇÕES EXTERIORES", + "8422100": "DEFESA", + "8423000": "JUSTIÇA", + "8424800": "SEGURANÇA E ORDEM PÚBLICA", + "8425600": "DEFESA CIVIL", + "8430200": "SEGURIDADE SOCIAL OBRIGATÓRIA", + "8511200": "EDUCAÇÃO INFANTIL - CRECHE", + "8512100": "EDUCAÇÃO INFANTIL - PRÉESCOLA", + "8513900": "ENSINO FUNDAMENTAL", + "8520100": "ENSINO MÉDIO", + "8531700": "EDUCAÇÃO SUPERIOR - GRADUAÇÃO", + "8532500": "EDUCAÇÃO SUPERIOR - GRADUAÇÃO E PÓS GRADUAÇÃO", + "8533300": "EDUCAÇÃO SUPERIOR - PÓS GRADUAÇÃO E EXTENSÃO", + "8541400": "EDUCAÇÃO PROFISSIONAL DE NÍVEL TÉCNICO", + "8542200": "EDUCAÇÃO PROFISSIONAL DE NÍVEL TECNOLÓGICO", + "8550301": "ADMINISTRAÇÃO DE CAIXAS ESCOLARES", + "8550302": "ATIVIDADES DE APOIO À EDUCAÇÃO, EXCETO CAIXAS ESCOLARES", + "8591100": "ENSINO DE ESPORTES", + "8592901": "ENSINO DE DANÇA", + "8592902": "ENSINO DE ARTES CÊNICAS, EXCETO DANÇA", + "8592903": "ENSINO DE MÚSICA", + "8592999": "ENSINO DE ARTE E CULTURA NÃO ESPECIFICADO ANTERIORMENTE", + "8593700": "ENSINO DE IDIOMAS", + "8599601": "FORMAÇÃO DE CONDUTORES", + "8599602": "CURSOS DE PILOTAGEM", + "8599603": "TREINAMENTO EM INFORMÁTICA", + "8599604": "TREINAMENTO EM DESENVOLVIMENTO PROFISSIONAL E GERENCIAL", + "8599605": "CURSOS PREPARATÓRIOS PARA CONCURSOS", + "8599699": "OUTRAS ATIVIDADES DE ENSINO NÃO ESPECIFICADAS ANTERIORMENTE", + "8610101": + "ATIVIDADES DE ATENDIMENTO HOSPITALAR, EXCETO PRONTO SOCORRO E UNIDADES PARA ATENDIMENTO A URGÊNCIAS", + "8610102": + "ATIVIDADES DE ATENDIMENTO EM PRONTO SOCORRO E UNIDADES HOSPITALARES PARA ATENDIMENTO A URGÊNCIAS", + "8621601": "UTI MÓVEL", + "8621602": "SERVIÇOS MÓVEIS DE ATENDIMENTO A URGÊNCIAS, EXCETO POR UTI MÓVEL", + "8622400": + "SERVIÇOS DE REMOÇÃO DE PACIENTES, EXCETO OS SERVIÇOS MÓVEIS DE ATENDIMENTO A URGÊNCIAS", + "8630501": + "ATIVIDADE MÉDICA AMBULATORIAL COM RECURSOS PARA REALIZAÇÃO DE PROCEDIMENTOS CIRÚRGICOS", + "8630502": "ATIVIDADE MÉDICA AMBULATORIAL COM RECURSOS PARA REALIZAÇÃO DE EXAMES COMPLEMENTARES", + "8630503": "ATIVIDADE MÉDICA AMBULATORIAL RESTRITA A CONSULTAS", + "8630504": "ATIVIDADE ODONTOLÓGICA", + "8630506": "SERVIÇOS DE VACINAÇÃO E IMUNIZAÇÃO HUMANA", + "8630507": "ATIVIDADES DE REPRODUÇÃO HUMANA ASSISTIDA", + "8630599": "ATIVIDADES DE ATENÇÃO AMBULATORIAL NÃO ESPECIFICADAS ANTERIORMENTE", + "8640201": "LABORATÓRIOS DE ANATOMIA PATOLÓGICA E CITOLÓGICA", + "8640202": "LABORATÓRIOS CLÍNICOS", + "8640203": "SERVIÇOS DE DIÁLISE E NEFROLOGIA", + "8640204": "SERVIÇOS DE TOMOGRAFIA", + "8640205": "SERVIÇOS DE DIAGNÓSTICO POR IMAGEM COM USO DE RADIAÇÃO IONIZANTE, EXCETO TOMOGRAFIA", + "8640206": "SERVIÇOS DE RESSONÂNCIA MAGNÉTICA", + "8640207": + "SERVIÇOS DE DIAGNÓSTICO POR IMAGEM SEM USO DE RADIAÇÃO IONIZANTE, EXCETO RESSONÂNCIA MAGNÉTICA", + "8640208": "SERVIÇOS DE DIAGNÓSTICO POR REGISTRO GRÁFICO - ECG, EEG E OUTROS EXAMES ANÁLOGOS", + "8640209": "SERVIÇOS DE DIAGNÓSTICO POR MÉTODOS ÓPTICOS - ENDOSCOPIA E OUTROS EXAMES ANÁLOGOS", + "8640210": "SERVIÇOS DE QUIMIOTERAPIA", + "8640211": "SERVIÇOS DE RADIOTERAPIA", + "8640212": "SERVIÇOS DE HEMOTERAPIA", + "8640213": "SERVIÇOS DE LITOTRIPCIA", + "8640214": "SERVIÇOS DE BANCOS DE CÉLULAS E TECIDOS HUMANOS", + "8640299": + "ATIVIDADES DE SERVIÇOS DE COMPLEMENTAÇÃO DIAGNÓSTICA E TERAPÊUTICA NÃO ESPECIFICADAS ANTERIORMENTE", + "8650001": "ATIVIDADES DE ENFERMAGEM", + "8650002": "ATIVIDADES DE PROFISSIONAIS DA NUTRIÇÃO", + "8650003": "ATIVIDADES DE PSICOLOGIA E PSICANÁLISE", + "8650004": "ATIVIDADES DE FISIOTERAPIA", + "8650005": "ATIVIDADES DE TERAPIA OCUPACIONAL", + "8650006": "ATIVIDADES DE FONOAUDIOLOGIA", + "8650007": "ATIVIDADES DE TERAPIA DE NUTRIÇÃO ENTERAL E PARENTERAL", + "8650099": "ATIVIDADES DE PROFISSIONAIS DA ÁREA DE SAÚDE NÃO ESPECIFICADAS ANTERIORMENTE", + "8660700": "ATIVIDADES DE APOIO À GESTÃO DE SAÚDE", + "8690901": "ATIVIDADES DE PRÁTICAS INTEGRATIVAS E COMPLEMENTARES EM SAÚDE HUMANA", + "8690902": "ATIVIDADES DE BANCO DE LEITE HUMANO", + "8690903": "ATIVIDADES DE ACUPUNTURA", + "8690904": "ATIVIDADES DE PODOLOGIA", + "8690999": "OUTRAS ATIVIDADES DE ATENÇÃO À SAÚDE HUMANA NÃO ESPECIFICADAS ANTERIORMENTE", + "8711501": "CLÍNICAS E RESIDÊNCIAS GERIÁTRICAS", + "8711502": "INSTITUIÇÕES DE LONGA PERMANÊNCIA PARA IDOSOS", + "8711503": "ATIVIDADES DE ASSISTÊNCIA A DEFICIENTES FÍSICOS, IMUNODEPRIMIDOS E CONVALESCENTES", + "8711504": "CENTROS DE APOIO A PACIENTES COM CÂNCER E COM AIDS", + "8711505": "CONDOMÍNIOS RESIDENCIAIS PARA IDOSOS", + "8712300": + "ATIVIDADES DE FORNECIMENTO DE INFRAESTRUTURA DE APOIO E ASSISTÊNCIA A PACIENTE NO DOMICÍLIO", + "8720401": "ATIVIDADES DE CENTROS DE ASSISTÊNCIA PSICOSSOCIAL", + "8720499": + "ATIVIDADES DE ASSISTÊNCIA PSICOSSOCIAL E À SAÚDE A PORTADORES DE DISTÚRBIOS PSÍQUICOS, DEFICIÊNCIA MENTAL E DEPENDÊNCIA QUÍMICA E GRUPOS SIMILARES NÃO ESPECIFICADAS ANTERIORMENTE", + "8730101": "ORFANATOS", + "8730102": "ALBERGUES ASSISTENCIAIS", + "8730199": + "ATIVIDADES DE ASSISTÊNCIA SOCIAL PRESTADAS EM RESIDÊNCIAS COLETIVAS E PARTICULARES NÃO ESPECIFICADAS ANTERIORMENTE", + "8800600": "SERVIÇOS DE ASSISTÊNCIA SOCIAL SEM ALOJAMENTO", + "9001901": "PRODUÇÃO TEATRAL", + "9001902": "PRODUÇÃO MUSICAL", + "9001903": "PRODUÇÃO DE ESPETÁCULOS DE DANÇA", + "9001904": "PRODUÇÃO DE ESPETÁCULOS CIRCENSES, DE MARIONETES E SIMILARES", + "9001905": "PRODUÇÃO DE ESPETÁCULOS DE RODEIOS, VAQUEJADAS E SIMILARES", + "9001906": "ATIVIDADES DE SONORIZAÇÃO E DE ILUMINAÇÃO", + "9001999": + "ARTES CÊNICAS, ESPETÁCULOS E ATIVIDADES COMPLEMENTARES NÃO ESPECIFICADAS ANTERIORMENTE", + "9002701": "ATIVIDADES DE ARTISTAS PLÁSTICOS, JORNALISTAS INDEPENDENTES E ESCRITORES", + "9002702": "RESTAURAÇÃO DE OBRAS DE ARTE", + "9003500": "GESTÃO DE ESPAÇOS PARA ARTES CÊNICAS, ESPETÁCULOS E OUTRAS ATIVIDADES ARTÍSTICAS", + "9101500": "ATIVIDADES DE BIBLIOTECAS E ARQUIVOS", + "9102301": + "ATIVIDADES DE MUSEUS E DE EXPLORAÇÃO DE LUGARES E PRÉDIOS HISTÓRICOS E ATRAÇÕES SIMILARES", + "9102302": "RESTAURAÇÃO E CONSERVAÇÃO DE LUGARES E PRÉDIOS HISTÓRICOS", + "9103100": + "ATIVIDADES DE JARDINS BOTÂNICOS, ZOOLÓGICOS, PARQUES NACIONAIS, RESERVAS ECOLÓGICAS E ÁREAS DE PROTEÇÃO AMBIENTAL", + "9200301": "CASAS DE BINGO", + "9200302": "EXPLORAÇÃO DE APOSTAS EM CORRIDAS DE CAVALOS", + "9200399": "EXPLORAÇÃO DE JOGOS DE AZAR E APOSTAS NÃO ESPECIFICADOS ANTERIORMENTE", + "9311500": "GESTÃO DE INSTALAÇÕES DE ESPORTES", + "9312300": "CLUBES SOCIAIS, ESPORTIVOS E SIMILARES", + "9313100": "ATIVIDADES DE CONDICIONAMENTO FÍSICO", + "9319101": "PRODUÇÃO E PROMOÇÃO DE EVENTOS ESPORTIVOS", + "9319199": "OUTRAS ATIVIDADES ESPORTIVAS NÃO ESPECIFICADAS ANTERIORMENTE", + "9321200": "PARQUES DE DIVERSÃO E PARQUES TEMÁTICOS", + "9329801": "DISCOTECAS, DANCETERIAS, SALÕES DE DANÇA E SIMILARES", + "9329802": "EXPLORAÇÃO DE BOLICHES", + "9329803": "EXPLORAÇÃO DE JOGOS DE SINUCA, BILHAR E SIMILARES", + "9329804": "EXPLORAÇÃO DE JOGOS ELETRÔNICOS RECREATIVOS", + "9329899": "OUTRAS ATIVIDADES DE RECREAÇÃO E LAZER NÃO ESPECIFICADAS ANTERIORMENTE", + "9411100": "ATIVIDADES DE ORGANIZAÇÕES ASSOCIATIVAS PATRONAIS E EMPRESARIAIS", + "9412001": "ATIVIDADES DE FISCALIZAÇÃO PROFISSIONAL", + "9412099": "OUTRAS ATIVIDADES ASSOCIATIVAS PROFISSIONAIS", + "9420100": "ATIVIDADES DE ORGANIZAÇÕES SINDICAIS", + "9430800": "ATIVIDADES DE ASSOCIAÇÕES DE DEFESA DE DIREITOS SOCIAIS", + "9491000": "ATIVIDADES DE ORGANIZAÇÕES RELIGIOSAS OU FILOSÓFICAS", + "9492800": "ATIVIDADES DE ORGANIZAÇÕES POLÍTICAS", + "9493600": "ATIVIDADES DE ORGANIZAÇÕES ASSOCIATIVAS LIGADAS À CULTURA E À ARTE", + "9499500": "ATIVIDADES ASSOCIATIVAS NÃO ESPECIFICADAS ANTERIORMENTE", + "9511800": "REPARAÇÃO E MANUTENÇÃO DE COMPUTADORES E DE EQUIPAMENTOS PERIFÉRICOS", + "9512600": "REPARAÇÃO E MANUTENÇÃO DE EQUIPAMENTOS DE COMUNICAÇÃO", + "9521500": "REPARAÇÃO E MANUTENÇÃO DE EQUIPAMENTOS ELETROELETRÔNICOS DE USO PESSOAL E DOMÉSTICO", + "9529101": "REPARAÇÃO DE CALÇADOS, DE BOLSAS E ARTIGOS DE VIAGEM", + "9529102": "CHAVEIROS", + "9529103": "REPARAÇÃO DE RELÓGIOS", + "9529104": "REPARAÇÃO DE BICICLETAS, TRICICLOS E OUTROS VEÍCULOS NÃO MOTORIZADOS", + "9529105": "REPARAÇÃO DE ARTIGOS DO MOBILIÁRIO", + "9529106": "REPARAÇÃO DE JÓIAS", + "9529199": + "REPARAÇÃO E MANUTENÇÃO DE OUTROS OBJETOS E EQUIPAMENTOS PESSOAIS E DOMÉSTICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "9601701": "LAVANDERIAS", + "9601702": "TINTURARIAS", + "9601703": "TOALHEIROS", + "9602501": "CABELEIREIROS, MANICURE E PEDICURE", + "9602502": "ATIVIDADES DE ESTÉTICA E OUTROS SERVIÇOS DE CUIDADOS COM A BELEZA", + "9603301": "GESTÃO E MANUTENÇÃO DE CEMITÉRIOS", + "9603302": "SERVIÇOS DE CREMAÇÃO", + "9603303": "SERVIÇOS DE SEPULTAMENTO", + "9603304": "SERVIÇOS DE FUNERÁRIAS", + "9603305": "SERVIÇOS DE SOMATOCONSERVAÇÃO", + "9603399": "ATIVIDADES FUNERÁRIAS E SERVIÇOS RELACIONADOS NÃO ESPECIFICADOS ANTERIORMENTE", + "9609202": "AGÊNCIAS MATRIMONIAIS", + "9609204": "EXPLORAÇÃO DE MÁQUINAS DE SERVIÇOS PESSOAIS ACIONADAS POR MOEDA", + "9609205": "ATIVIDADES DE SAUNA E BANHOS", + "9609206": "SERVIÇOS DE TATUAGEM E COLOCAÇÃO DE PIERCING", + "9609207": "ALOJAMENTO DE ANIMAIS DOMÉSTICOS", + "9609208": "HIGIENE E EMBELEZAMENTO DE ANIMAIS DOMÉSTICOS", + "9609299": "OUTRAS ATIVIDADES DE SERVIÇOS PESSOAIS NÃO ESPECIFICADAS ANTERIORMENTE", + "9700500": "SERVIÇOS DOMÉSTICOS", + "9900800": "ORGANISMOS INTERNACIONAIS E OUTRAS INSTITUIÇÕES EXTRATERRITORIAIS", + "0111301": "CULTIVO DE ARROZ", + "0111302": "CULTIVO DE MILHO", + "0111303": "CULTIVO DE TRIGO", + "0111399": "CULTIVO DE OUTROS CEREAIS NÃO ESPECIFICADOS ANTERIORMENTE", + "0112101": "CULTIVO DE ALGODÃO HERBÁCEO", + "0112102": "CULTIVO DE JUTA", + "0112199": "CULTIVO DE OUTRAS FIBRAS DE LAVOURA TEMPORÁRIA NÃO ESPECIFICADAS ANTERIORMENTE", + "0113000": "CULTIVO DE CANA DE AÇÚCAR", + "0114800": "CULTIVO DE FUMO", + "0115600": "CULTIVO DE SOJA", + "0116401": "CULTIVO DE AMENDOIM", + "0116402": "CULTIVO DE GIRASSOL", + "0116403": "CULTIVO DE MAMONA", + "0116499": "CULTIVO DE OUTRAS OLEAGINOSAS DE LAVOURA TEMPORÁRIA NÃO ESPECIFICADAS ANTERIORMENTE", + "0119901": "CULTIVO DE ABACAXI", + "0119902": "CULTIVO DE ALHO", + "0119903": "CULTIVO DE BATATA INGLESA", + "0119904": "CULTIVO DE CEBOLA", + "0119905": "CULTIVO DE FEIJÃO", + "0119906": "CULTIVO DE MANDIOCA", + "0119907": "CULTIVO DE MELÃO", + "0119908": "CULTIVO DE MELANCIA", + "0119909": "CULTIVO DE TOMATE RASTEIRO", + "0119999": "CULTIVO DE OUTRAS PLANTAS DE LAVOURA TEMPORÁRIA NÃO ESPECIFICADAS ANTERIORMENTE", + "0121101": "HORTICULTURA, EXCETO MORANGO", + "0121102": "CULTIVO DE MORANGO", + "0122900": "CULTIVO DE FLORES E PLANTAS ORNAMENTAIS", + "0131800": "CULTIVO DE LARANJA", + "0132600": "CULTIVO DE UVA", + "0133401": "CULTIVO DE AÇAÍ", + "0133402": "CULTIVO DE BANANA", + "0133403": "CULTIVO DE CAJU", + "0133404": "CULTIVO DE CÍTRICOS, EXCETO LARANJA", + "0133405": "CULTIVO DE COCO DA BAÍA", + "0133406": "CULTIVO DE GUARANÁ", + "0133407": "CULTIVO DE MAÇÃ", + "0133408": "CULTIVO DE MAMÃO", + "0133409": "CULTIVO DE MARACUJÁ", + "0133410": "CULTIVO DE MANGA", + "0133411": "CULTIVO DE PÊSSEGO", + "0133499": "CULTIVO DE FRUTAS DE LAVOURA PERMANENTE NÃO ESPECIFICADAS ANTERIORMENTE", + "0134200": "CULTIVO DE CAFÉ", + "0135100": "CULTIVO DE CACAU", + "0139301": "CULTIVO DE CHÁ DA ÍNDIA", + "0139302": "CULTIVO DE ERVA MATE", + "0139303": "CULTIVO DE PIMENTA DO REINO", + "0139304": "CULTIVO DE PLANTAS PARA CONDIMENTO, EXCETO PIMENTA DO REINO", + "0139305": "CULTIVO DE DENDÊ", + "0139306": "CULTIVO DE SERINGUEIRA", + "0139399": "CULTIVO DE OUTRAS PLANTAS DE LAVOURA PERMANENTE NÃO ESPECIFICADAS ANTERIORMENTE", + "0141501": "PRODUÇÃO DE SEMENTES CERTIFICADAS, EXCETO DE FORRAGEIRAS PARA PASTO", + "0141502": "PRODUÇÃO DE SEMENTES CERTIFICADAS DE FORRAGEIRAS PARA FORMAÇÃO DE PASTO", + "0142300": "PRODUÇÃO DE MUDAS E OUTRAS FORMAS DE PROPAGAÇÃO VEGETAL, CERTIFICADAS", + "0151201": "CRIAÇÃO DE BOVINOS PARA CORTE", + "0151202": "CRIAÇÃO DE BOVINOS PARA LEITE", + "0151203": "CRIAÇÃO DE BOVINOS, EXCETO PARA CORTE E LEITE", + "0152101": "CRIAÇÃO DE BUFALINOS", + "0152102": "CRIAÇÃO DE EQÜINOS", + "0152103": "CRIAÇÃO DE ASININOS E MUARES", + "0153901": "CRIAÇÃO DE CAPRINOS", + "0153902": "CRIAÇÃO DE OVINOS, INCLUSIVE PARA PRODUÇÃO DE LÃ", + "0154700": "CRIAÇÃO DE SUÍNOS", + "0155501": "CRIAÇÃO DE FRANGOS PARA CORTE", + "0155502": "PRODUÇÃO DE PINTOS DE UM DIA", + "0155503": "CRIAÇÃO DE OUTROS GALINÁCEOS, EXCETO PARA CORTE", + "0155504": "CRIAÇÃO DE AVES, EXCETO GALINÁCEOS", + "0155505": "PRODUÇÃO DE OVOS", + "0159801": "APICULTURA", + "0159802": "CRIAÇÃO DE ANIMAIS DE ESTIMAÇÃO", + "0159803": "CRIAÇÃO DE ESCARGÔ", + "0159804": "CRIAÇÃO DE BICHO DA SEDA", + "0159899": "CRIAÇÃO DE OUTROS ANIMAIS NÃO ESPECIFICADOS ANTERIORMENTE", + "0161001": "SERVIÇO DE PULVERIZAÇÃO E CONTROLE DE PRAGAS AGRÍCOLAS", + "0161002": "SERVIÇO DE PODA DE ÁRVORES PARA LAVOURAS", + "0161003": "SERVIÇO DE PREPARAÇÃO DE TERRENO, CULTIVO E COLHEITA", + "0161099": "ATIVIDADES DE APOIO À AGRICULTURA NÃO ESPECIFICADAS ANTERIORMENTE", + "0162801": "SERVIÇO DE INSEMINAÇÃO ARTIFICIAL DE ANIMAIS", + "0162802": "SERVIÇO DE TOSQUIAMENTO DE OVINOS", + "0162803": "SERVIÇO DE MANEJO DE ANIMAIS", + "0162899": "ATIVIDADES DE APOIO À PECUÁRIA NÃO ESPECIFICADAS ANTERIORMENTE", + "0163600": "ATIVIDADES DE PÓS COLHEITA", + "0170900": "CAÇA E SERVIÇOS RELACIONADOS", + "0210101": "CULTIVO DE EUCALIPTO", + "0210102": "CULTIVO DE ACÁCIA NEGRA", + "0210103": "CULTIVO DE PINUS", + "0210104": "CULTIVO DE TECA", + "0210105": "CULTIVO DE ESPÉCIES MADEIREIRAS, EXCETO EUCALIPTO, ACÁCIA NEGRA, PINUS E TECA", + "0210106": "CULTIVO DE MUDAS EM VIVEIROS FLORESTAIS", + "0210107": "EXTRAÇÃO DE MADEIRA EM FLORESTAS PLANTADAS", + "0210108": "PRODUÇÃO DE CARVÃO VEGETAL - FLORESTAS PLANTADAS", + "0210109": "PRODUÇÃO DE CASCA DE ACÁCIA NEGRA - FLORESTAS PLANTADAS", + "0210199": + "PRODUÇÃO DE PRODUTOS NÃO MADEIREIROS NÃO ESPECIFICADOS ANTERIORMENTE EM FLORESTAS PLANTADAS", + "0220901": "EXTRAÇÃO DE MADEIRA EM FLORESTAS NATIVAS", + "0220902": "PRODUÇÃO DE CARVÃO VEGETAL - FLORESTAS NATIVAS", + "0220903": "COLETA DE CASTANHA DO PARÁ EM FLORESTAS NATIVAS", + "0220904": "COLETA DE LÁTEX EM FLORESTAS NATIVAS", + "0220905": "COLETA DE PALMITO EM FLORESTAS NATIVAS", + "0220906": "CONSERVAÇÃO DE FLORESTAS NATIVAS", + "0220999": + "COLETA DE PRODUTOS NÃO MADEIREIROS NÃO ESPECIFICADOS ANTERIORMENTE EM FLORESTAS NATIVAS", + "0230600": "ATIVIDADES DE APOIO À PRODUÇÃO FLORESTAL", + "0311601": "PESCA DE PEIXES EM ÁGUA SALGADA", + "0311602": "PESCA DE CRUSTÁCEOS E MOLUSCOS EM ÁGUA SALGADA", + "0311603": "COLETA DE OUTROS PRODUTOS MARINHOS", + "0311604": "ATIVIDADES DE APOIO À PESCA EM ÁGUA SALGADA", + "0312401": "PESCA DE PEIXES EM ÁGUA DOCE", + "0312402": "PESCA DE CRUSTÁCEOS E MOLUSCOS EM ÁGUA DOCE", + "0312403": "COLETA DE OUTROS PRODUTOS AQUÁTICOS DE ÁGUA DOCE", + "0312404": "ATIVIDADES DE APOIO À PESCA EM ÁGUA DOCE", + "0321301": "CRIAÇÃO DE PEIXES EM ÁGUA SALGADA E SALOBRA", + "0321302": "CRIAÇÃO DE CAMARÕES EM ÁGUA SALGADA E SALOBRA", + "0321303": "CRIAÇÃO DE OSTRAS E MEXILHÕES EM ÁGUA SALGADA E SALOBRA", + "0321304": "CRIAÇÃO DE PEIXES ORNAMENTAIS EM ÁGUA SALGADA E SALOBRA", + "0321305": "ATIVIDADES DE APOIO À AQÜICULTURA EM ÁGUA SALGADA E SALOBRA", + "0321399": + "CULTIVOS E SEMICULTIVOS DA AQÜICULTURA EM ÁGUA SALGADA E SALOBRA NÃO ESPECIFICADOS ANTERIORMENTE", + "0322101": "CRIAÇÃO DE PEIXES EM ÁGUA DOCE", + "0322102": "CRIAÇÃO DE CAMARÕES EM ÁGUA DOCE", + "0322103": "CRIAÇÃO DE OSTRAS E MEXILHÕES EM ÁGUA DOCE", + "0322104": "CRIAÇÃO DE PEIXES ORNAMENTAIS EM ÁGUA DOCE", + "0322105": "RANICULTURA", + "0322106": "CRIAÇÃO DE JACARÉ", + "0322107": "ATIVIDADES DE APOIO À AQÜICULTURA EM ÁGUA DOCE", + "0322199": "CULTIVOS E SEMICULTIVOS DA AQÜICULTURA EM ÁGUA DOCE NÃO ESPECIFICADOS ANTERIORMENTE", + "0500301": "EXTRAÇÃO DE CARVÃO MINERAL", + "0500302": "BENEFICIAMENTO DE CARVÃO MINERAL", + "0600001": "EXTRAÇÃO DE PETRÓLEO E GÁS NATURAL", + "0600002": "EXTRAÇÃO E BENEFICIAMENTO DE XISTO", + "0600003": "EXTRAÇÃO E BENEFICIAMENTO DE AREIAS BETUMINOSAS", + "0710301": "EXTRAÇÃO DE MINÉRIO DE FERRO", + "0710302": "PELOTIZAÇÃO, SINTERIZAÇÃO E OUTROS BENEFICIAMENTOS DE MINÉRIO DE FERRO", + "0721901": "EXTRAÇÃO DE MINÉRIO DE ALUMÍNIO", + "0721902": "BENEFICIAMENTO DE MINÉRIO DE ALUMÍNIO", + "0722701": "EXTRAÇÃO DE MINÉRIO DE ESTANHO", + "0722702": "BENEFICIAMENTO DE MINÉRIO DE ESTANHO", + "0723501": "EXTRAÇÃO DE MINÉRIO DE MANGANÊS", + "0723502": "BENEFICIAMENTO DE MINÉRIO DE MANGANÊS", + "0724301": "EXTRAÇÃO DE MINÉRIO DE METAIS PRECIOSOS", + "0724302": "BENEFICIAMENTO DE MINÉRIO DE METAIS PRECIOSOS", + "0725100": "EXTRAÇÃO DE MINERAIS RADIOATIVOS", + "0729401": "EXTRAÇÃO DE MINÉRIOS DE NIÓBIO E TITÂNIO", + "0729402": "EXTRAÇÃO DE MINÉRIO DE TUNGSTÊNIO", + "0729403": "EXTRAÇÃO DE MINÉRIO DE NÍQUEL", + "0729404": + "EXTRAÇÃO DE MINÉRIOS DE COBRE, CHUMBO, ZINCO E OUTROS MINERAIS METÁLICOS NÃO FERROSOS NÃO ESPECIFICADOS ANTERIORMENTE", + "0729405": + "BENEFICIAMENTO DE MINÉRIOS DE COBRE, CHUMBO, ZINCO E OUTROS MINERAIS METÁLICOS NÃO FERROSOS NÃO ESPECIFICADOS ANTERIORMENTE", + "0810001": "EXTRAÇÃO DE ARDÓSIA E BENEFICIAMENTO ASSOCIADO", + "0810002": "EXTRAÇÃO DE GRANITO E BENEFICIAMENTO ASSOCIADO", + "0810003": "EXTRAÇÃO DE MÁRMORE E BENEFICIAMENTO ASSOCIADO", + "0810004": "EXTRAÇÃO DE CALCÁRIO E DOLOMITA E BENEFICIAMENTO ASSOCIADO", + "0810005": "EXTRAÇÃO DE GESSO E CAULIM", + "0810006": "EXTRAÇÃO DE AREIA, CASCALHO OU PEDREGULHO E BENEFICIAMENTO ASSOCIADO", + "0810007": "EXTRAÇÃO DE ARGILA E BENEFICIAMENTO ASSOCIADO", + "0810008": "EXTRAÇÃO DE SAIBRO E BENEFICIAMENTO ASSOCIADO", + "0810009": "EXTRAÇÃO DE BASALTO E BENEFICIAMENTO ASSOCIADO", + "0810010": "BENEFICIAMENTO DE GESSO E CAULIM ASSOCIADO À EXTRAÇÃO", + "0810099": + "EXTRAÇÃO E BRITAMENTO DE PEDRAS E OUTROS MATERIAIS PARA CONSTRUÇÃO E BENEFICIAMENTO ASSOCIADO", + "0891600": + "EXTRAÇÃO DE MINERAIS PARA FABRICAÇÃO DE ADUBOS, FERTILIZANTES E OUTROS PRODUTOS QUÍMICOS", + "0892401": "EXTRAÇÃO DE SAL MARINHO", + "0892402": "EXTRAÇÃO DE SAL GEMA", + "0892403": "REFINO E OUTROS TRATAMENTOS DO SAL", + "0893200": "EXTRAÇÃO DE GEMAS (PEDRAS PRECIOSAS E SEMIPRECIOSAS)", + "0899101": "EXTRAÇÃO DE GRAFITA", + "0899102": "EXTRAÇÃO DE QUARTZO", + "0899103": "EXTRAÇÃO DE AMIANTO", + "0899199": "EXTRAÇÃO DE OUTROS MINERAIS NÃO METÁLICOS NÃO ESPECIFICADOS ANTERIORMENTE", + "0910600": "ATIVIDADES DE APOIO À EXTRAÇÃO DE PETRÓLEO E GÁS NATURAL", + "0990401": "ATIVIDADES DE APOIO À EXTRAÇÃO DE MINÉRIO DE FERRO", + "0990402": "ATIVIDADES DE APOIO À EXTRAÇÃO DE MINERAIS METÁLICOS NÃO FERROSOS", + "0990403": "ATIVIDADES DE APOIO À EXTRAÇÃO DE MINERAIS NÃO METÁLICOS", +}; diff --git a/src/format-cnae/format-cnae.test.ts b/src/format-cnae/format-cnae.test.ts new file mode 100644 index 00000000..061d236d --- /dev/null +++ b/src/format-cnae/format-cnae.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { formatCnae } from "./format-cnae"; + +describe("formatCnae", () => { + it("should format a CNAE code given as digits", () => { + expect(formatCnae("6201501")).toBe("6201-5/01"); + }); + + it("should format a CNAE code given as a number", () => { + expect(formatCnae(6201501)).toBe("6201-5/01"); + }); + + it("should format a CNAE code that already has the mask", () => { + expect(formatCnae("6201-5/01")).toBe("6201-5/01"); + }); + + it("should not validate whether the code exists in the official table", () => { + expect(formatCnae("0000000")).toBe("0000-0/00"); + }); + + it("should return an empty string for an empty value", () => { + expect(formatCnae("")).toBe(""); + }); + + it("should return an empty string for null and undefined", () => { + // @ts-expect-error not a string or number + expect(formatCnae(null)).toBe(""); + // @ts-expect-error not a string or number + expect(formatCnae(undefined)).toBe(""); + }); +}); diff --git a/src/format-cnae/format-cnae.ts b/src/format-cnae/format-cnae.ts new file mode 100644 index 00000000..989348a6 --- /dev/null +++ b/src/format-cnae/format-cnae.ts @@ -0,0 +1,30 @@ +import { format } from "../_internals/format/format"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Formats a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. + * + * This is a purely structural transformation, it does not check the code against the + * official table, use `isValidCnae` for that. + * + * @param {string|number} value - The CNAE code to be formatted. + * @returns {string} The formatted code in the `NNNN-N/NN` pattern, or an empty string + * when there is nothing to format. + * + * @example + * ```typescript + * formatCnae("6201501"); // "6201-5/01" + * formatCnae(6201501); // "6201-5/01" + * ``` + * + * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + */ +export const formatCnae = (value: string | number): string => + isNullish(value) || value === "" + ? "" + : format({ + value: sanitizeToDigits(value), + pattern: "0000-0/00", + pad: true, + }); diff --git a/src/get-cnae/get-cnae.test.ts b/src/get-cnae/get-cnae.test.ts new file mode 100644 index 00000000..99b4cba3 --- /dev/null +++ b/src/get-cnae/get-cnae.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getCnae } from "./get-cnae"; + +describe("getCnae", () => { + it("should return the CNAE entry for a known code as a string", () => { + expect(getCnae("6201501")).toEqual({ + code: "6201-5/01", + description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA", + }); + }); + + it("should return the CNAE entry for a known code as a number", () => { + expect(getCnae(6201501)).toEqual({ + code: "6201-5/01", + description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA", + }); + }); + + it("should return the CNAE entry for a masked code", () => { + expect(getCnae("6201-5/01")).toEqual({ + code: "6201-5/01", + description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA", + }); + }); + + it("should return a fresh object on every call", () => { + const first = getCnae("6201501"); + const second = getCnae("6201501"); + expect(first).not.toBe(second); + }); + + it("should return null for an unknown seven digit code", () => { + expect(getCnae("0000000")).toBeNull(); + }); + + it("should return null for a code with a digit count different from seven", () => { + expect(getCnae("620150")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getCnae("")).toBeNull(); + }); + + it("should return null for null and undefined", () => { + // @ts-expect-error not a string or number + expect(getCnae(null)).toBeNull(); + // @ts-expect-error not a string or number + expect(getCnae(undefined)).toBeNull(); + }); +}); diff --git a/src/get-cnae/get-cnae.ts b/src/get-cnae/get-cnae.ts new file mode 100644 index 00000000..4e8c02d8 --- /dev/null +++ b/src/get-cnae/get-cnae.ts @@ -0,0 +1,40 @@ +import { CNAE_SUBCLASSES } from "../_internals/constants/cnae"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { formatCnae } from "../format-cnae/format-cnae"; + +/** + * A CNAE (Classificação Nacional de Atividades Econômicas) subclass. + */ +export type Cnae = { + /** The subclass code formatted as `NNNN-N/NN`. */ + code: string; + /** The official subclass description. */ + description: string; +}; + +/** + * Looks a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up in the + * official CNAE 2.3 table. + * + * @param {string|number} value - The CNAE code to look up, with or without the + * `NNNN-N/NN` mask. + * @returns {Cnae|null} The matching subclass, or null when the code is unknown or invalid. + * + * @example + * ```typescript + * getCnae("6201501"); // { code: "6201-5/01", description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA" } + * getCnae("0000000"); // null + * ``` + * + * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + */ +export const getCnae = (value: string | number): Cnae | null => { + if (isNullish(value) || value === "") return null; + + const digits = sanitizeToDigits(value); + + if (digits.length !== 7 || !(digits in CNAE_SUBCLASSES)) return null; + + return { code: formatCnae(digits), description: CNAE_SUBCLASSES[digits] }; +}; diff --git a/src/is-valid-cnae/is-valid-cnae.test.ts b/src/is-valid-cnae/is-valid-cnae.test.ts new file mode 100644 index 00000000..7764876f --- /dev/null +++ b/src/is-valid-cnae/is-valid-cnae.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidCnae } from "./is-valid-cnae"; + +describe("isValidCnae", () => { + it("should validate a CNAE code without a mask", () => { + expect(isValidCnae("6201501")).toBe(true); + }); + + it("should validate a CNAE code with the NNNN-N/NN mask", () => { + expect(isValidCnae("6201-5/01")).toBe(true); + }); + + it("should validate a CNAE code given as a number", () => { + expect(isValidCnae(6201501)).toBe(true); + }); + + it("should validate a CNAE code with surrounding whitespace", () => { + expect(isValidCnae(" 6201501 ")).toBe(true); + }); + + it("should return false for an unknown seven digit code", () => { + expect(isValidCnae("0000000")).toBe(false); + }); + + it("should return false when the digit count is not seven", () => { + expect(isValidCnae("620150")).toBe(false); + expect(isValidCnae("62015011")).toBe(false); + }); + + it("should return false for an empty string", () => { + expect(isValidCnae("")).toBe(false); + }); + + it("should return false for null and undefined", () => { + // @ts-expect-error not a string or number + expect(isValidCnae(null)).toBe(false); + // @ts-expect-error not a string or number + expect(isValidCnae(undefined)).toBe(false); + }); + + it("should return false for whitespace only", () => { + expect(isValidCnae(" ")).toBe(false); + }); + + it("should return false for a non numeric string", () => { + expect(isValidCnae("abcdefg")).toBe(false); + }); +}); diff --git a/src/is-valid-cnae/is-valid-cnae.ts b/src/is-valid-cnae/is-valid-cnae.ts new file mode 100644 index 00000000..75972b2d --- /dev/null +++ b/src/is-valid-cnae/is-valid-cnae.ts @@ -0,0 +1,29 @@ +import { CNAE_SUBCLASSES } from "../_internals/constants/cnae"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Validates if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code + * exists in the official CNAE 2.3 table. + * + * @param {string|number} value - The CNAE code to be validated, with or without the + * `NNNN-N/NN` mask, e.g. `"6201-5/01"`, `"6201501"` or `6201501`. + * @returns {boolean} True when the code is a known 7 digit subclass, false otherwise. + * + * @example + * ```typescript + * isValidCnae("6201-5/01"); // true + * isValidCnae("6201501"); // true + * isValidCnae(6201501); // true + * isValidCnae("0000000"); // false + * ``` + * + * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + */ +export const isValidCnae = (value: string | number): boolean => { + if (isNullish(value) || value === "") return false; + + const digits = sanitizeToDigits(value); + + return digits.length === 7 && digits in CNAE_SUBCLASSES; +}; From 1d10864c17b5fab4a6a4b73bee79a57b67bc2af9 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:55 -0300 Subject: [PATCH 18/22] feat(ncm): add formatNcm and isValidNcm NCM (MDIC/SINIEF/CONFAZ) formatting/validation against a table with no check digit, generated by scripts/ncm.ts. --- scripts/ncm.ts | 56 + src/format-ncm/format-ncm.test.ts | 41 + src/format-ncm/format-ncm.ts | 30 + src/is-valid-ncm/constants.ts | 10523 ++++++++++++++++++++++++ src/is-valid-ncm/is-valid-ncm.test.ts | 57 + src/is-valid-ncm/is-valid-ncm.ts | 38 + 6 files changed, 10745 insertions(+) create mode 100644 scripts/ncm.ts create mode 100644 src/format-ncm/format-ncm.test.ts create mode 100644 src/format-ncm/format-ncm.ts create mode 100644 src/is-valid-ncm/constants.ts create mode 100644 src/is-valid-ncm/is-valid-ncm.test.ts create mode 100644 src/is-valid-ncm/is-valid-ncm.ts diff --git a/scripts/ncm.ts b/scripts/ncm.ts new file mode 100644 index 00000000..b74db86e --- /dev/null +++ b/scripts/ncm.ts @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +import { writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; + +const scriptsDir = dirname(fileURLToPath(import.meta.url)); + +type NcmEntry = { + Codigo: string; + Data_Fim: string; +}; + +type NcmResponse = { + Nomenclaturas: NcmEntry[]; +}; + +const main = async () => { + const response = await fetchWithRetry( + "https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json?perfil=PUBLICO", + ); + + if (!response.ok) { + throw new Error(`Siscomex NCM request failed with status ${response.status}`); + } + + const json: NcmResponse = await response.json(); + + const codes = json.Nomenclaturas.filter( + (entry) => entry.Data_Fim === "31/12/9999" && /^[\d.]{10}$/.test(entry.Codigo), + ) + .map((entry) => entry.Codigo.replace(/\D/g, "")) + .filter((code) => code.length === 8); + + const uniqueSortedCodes = Array.from(new Set(codes)).sort(); + + await writeFile( + resolve(scriptsDir, "..", "./src/is-valid-ncm/constants.ts"), + `/** + * Currently valid NCM (Nomenclatura Comum do Mercosul) 8 digit codes, sorted ascending. + * + * Generated by \`node ./scripts/ncm.ts\`. Do not edit by hand. + * + * @see https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json + */ +export const NCM_CODES: readonly string[] = ${JSON.stringify(uniqueSortedCodes)}; +`, + ); +}; + +await main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +}); diff --git a/src/format-ncm/format-ncm.test.ts b/src/format-ncm/format-ncm.test.ts new file mode 100644 index 00000000..6bd6aa0a --- /dev/null +++ b/src/format-ncm/format-ncm.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { formatNcm } from "./format-ncm"; + +describe("formatNcm", () => { + it("should format an NCM code given as digits", () => { + expect(formatNcm("84713012")).toBe("8471.30.12"); + }); + + it("should format an NCM code given as a number", () => { + expect(formatNcm(84713012)).toBe("8471.30.12"); + }); + + it("should format an NCM code that already has the mask", () => { + expect(formatNcm("8471.30.12")).toBe("8471.30.12"); + }); + + it("should format a partial value progressively", () => { + expect(formatNcm("8")).toBe("8"); + expect(formatNcm("84")).toBe("84"); + expect(formatNcm("847")).toBe("847"); + expect(formatNcm("8471")).toBe("8471"); + expect(formatNcm("84713")).toBe("8471.3"); + expect(formatNcm("847130")).toBe("8471.30"); + expect(formatNcm("8471301")).toBe("8471.30.1"); + }); + + it("should not validate whether the code exists in the official table", () => { + expect(formatNcm("00000000")).toBe("0000.00.00"); + }); + + it("should return an empty string for an empty value", () => { + expect(formatNcm("")).toBe(""); + }); + + it("should return an empty string for null and undefined", () => { + // @ts-expect-error not a string or number + expect(formatNcm(null)).toBe(""); + // @ts-expect-error not a string or number + expect(formatNcm(undefined)).toBe(""); + }); +}); diff --git a/src/format-ncm/format-ncm.ts b/src/format-ncm/format-ncm.ts new file mode 100644 index 00000000..80a39e1d --- /dev/null +++ b/src/format-ncm/format-ncm.ts @@ -0,0 +1,30 @@ +import { format } from "../_internals/format/format"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Formats a NCM (Nomenclatura Comum do Mercosul) code. + * + * This is a purely structural transformation, it does not check the code against the + * official table, use `isValidNcm` for that. + * + * @param {string|number} value - The NCM code to be formatted. + * @returns {string} The formatted code in the `NNNN.NN.NN` pattern, or an empty string + * when there is nothing to format. + * + * @example + * ```typescript + * formatNcm("84713012"); // "8471.30.12" + * formatNcm(84713012); // "8471.30.12" + * formatNcm("8471"); // "8471" (partial values are formatted progressively) + * ``` + * + * @see Official: https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json + */ +export const formatNcm = (value: string | number): string => + isNullish(value) || value === "" + ? "" + : format({ + value: sanitizeToDigits(value), + pattern: "0000.00.00", + }); diff --git a/src/is-valid-ncm/constants.ts b/src/is-valid-ncm/constants.ts new file mode 100644 index 00000000..cec2446a --- /dev/null +++ b/src/is-valid-ncm/constants.ts @@ -0,0 +1,10523 @@ +/** + * Currently valid NCM (Nomenclatura Comum do Mercosul) 8 digit codes, sorted ascending. + * + * Generated by `node ./scripts/ncm.ts`. Do not edit by hand. + * + * @see https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json + */ +export const NCM_CODES: readonly string[] = [ + "01012100", + "01012900", + "01013000", + "01019000", + "01022110", + "01022190", + "01022911", + "01022919", + "01022990", + "01023110", + "01023190", + "01023911", + "01023919", + "01023990", + "01029000", + "01031000", + "01039100", + "01039200", + "01041011", + "01041019", + "01041090", + "01042010", + "01042090", + "01051110", + "01051190", + "01051200", + "01051300", + "01051400", + "01051500", + "01059400", + "01059900", + "01061100", + "01061200", + "01061300", + "01061400", + "01061900", + "01062000", + "01063100", + "01063200", + "01063310", + "01063390", + "01063900", + "01064100", + "01064900", + "01069000", + "02011000", + "02012010", + "02012020", + "02012090", + "02013000", + "02021000", + "02022010", + "02022020", + "02022090", + "02023000", + "02031100", + "02031200", + "02031900", + "02032100", + "02032200", + "02032900", + "02041000", + "02042100", + "02042200", + "02042300", + "02043000", + "02044100", + "02044200", + "02044300", + "02045000", + "02050000", + "02061000", + "02062100", + "02062200", + "02062910", + "02062990", + "02063000", + "02064100", + "02064900", + "02068000", + "02069000", + "02071100", + "02071210", + "02071220", + "02071300", + "02071411", + "02071412", + "02071413", + "02071419", + "02071421", + "02071422", + "02071423", + "02071424", + "02071429", + "02071431", + "02071432", + "02071433", + "02071434", + "02071439", + "02072400", + "02072500", + "02072600", + "02072700", + "02074100", + "02074200", + "02074300", + "02074400", + "02074500", + "02075100", + "02075200", + "02075300", + "02075400", + "02075500", + "02076000", + "02081000", + "02083000", + "02084000", + "02085000", + "02086000", + "02089000", + "02091011", + "02091019", + "02091021", + "02091029", + "02099000", + "02101100", + "02101200", + "02101900", + "02102000", + "02109100", + "02109200", + "02109300", + "02109911", + "02109919", + "02109920", + "02109930", + "02109940", + "02109990", + "03011110", + "03011190", + "03011900", + "03019110", + "03019190", + "03019210", + "03019290", + "03019310", + "03019390", + "03019410", + "03019490", + "03019510", + "03019590", + "03019911", + "03019912", + "03019919", + "03019991", + "03019992", + "03019999", + "03021100", + "03021300", + "03021400", + "03021900", + "03022100", + "03022200", + "03022300", + "03022400", + "03022900", + "03023100", + "03023200", + "03023300", + "03023400", + "03023500", + "03023600", + "03023900", + "03024100", + "03024210", + "03024290", + "03024300", + "03024400", + "03024500", + "03024600", + "03024700", + "03024910", + "03024990", + "03025100", + "03025200", + "03025300", + "03025400", + "03025500", + "03025600", + "03025900", + "03027100", + "03027210", + "03027290", + "03027300", + "03027400", + "03027900", + "03028100", + "03028200", + "03028310", + "03028320", + "03028400", + "03028500", + "03028910", + "03028921", + "03028922", + "03028923", + "03028924", + "03028931", + "03028932", + "03028933", + "03028934", + "03028935", + "03028936", + "03028937", + "03028938", + "03028941", + "03028942", + "03028943", + "03028944", + "03028945", + "03028990", + "03029110", + "03029190", + "03029200", + "03029900", + "03031100", + "03031200", + "03031300", + "03031400", + "03031900", + "03032300", + "03032410", + "03032490", + "03032500", + "03032600", + "03032900", + "03033100", + "03033200", + "03033300", + "03033400", + "03033900", + "03034100", + "03034200", + "03034300", + "03034400", + "03034500", + "03034600", + "03034900", + "03035100", + "03035300", + "03035400", + "03035500", + "03035600", + "03035700", + "03035910", + "03035920", + "03035990", + "03036300", + "03036400", + "03036500", + "03036600", + "03036700", + "03036800", + "03036910", + "03036990", + "03038111", + "03038112", + "03038113", + "03038114", + "03038119", + "03038190", + "03038200", + "03038311", + "03038319", + "03038321", + "03038329", + "03038400", + "03038910", + "03038920", + "03038932", + "03038933", + "03038941", + "03038942", + "03038943", + "03038944", + "03038945", + "03038946", + "03038951", + "03038952", + "03038953", + "03038954", + "03038955", + "03038956", + "03038961", + "03038962", + "03038963", + "03038964", + "03038965", + "03038990", + "03039110", + "03039190", + "03039200", + "03039910", + "03039920", + "03039990", + "03043100", + "03043210", + "03043290", + "03043300", + "03043900", + "03044100", + "03044200", + "03044300", + "03044400", + "03044500", + "03044600", + "03044700", + "03044800", + "03044910", + "03044920", + "03044990", + "03045100", + "03045200", + "03045300", + "03045400", + "03045500", + "03045600", + "03045700", + "03045900", + "03046100", + "03046210", + "03046290", + "03046300", + "03046900", + "03047100", + "03047200", + "03047300", + "03047400", + "03047500", + "03047900", + "03048100", + "03048200", + "03048300", + "03048400", + "03048510", + "03048520", + "03048600", + "03048700", + "03048810", + "03048890", + "03048910", + "03048920", + "03048930", + "03048990", + "03049100", + "03049211", + "03049212", + "03049219", + "03049221", + "03049222", + "03049229", + "03049300", + "03049400", + "03049500", + "03049600", + "03049700", + "03049900", + "03052010", + "03052090", + "03053100", + "03053210", + "03053220", + "03053230", + "03053290", + "03053900", + "03054100", + "03054200", + "03054300", + "03054400", + "03054910", + "03054920", + "03054990", + "03055100", + "03055200", + "03055310", + "03055390", + "03055400", + "03055900", + "03056100", + "03056200", + "03056300", + "03056400", + "03056910", + "03056990", + "03057100", + "03057200", + "03057900", + "03061110", + "03061190", + "03061200", + "03061400", + "03061500", + "03061610", + "03061690", + "03061710", + "03061790", + "03061910", + "03061990", + "03063100", + "03063200", + "03063300", + "03063400", + "03063500", + "03063600", + "03063910", + "03063990", + "03069100", + "03069200", + "03069300", + "03069400", + "03069500", + "03069910", + "03069990", + "03071100", + "03071200", + "03071900", + "03072100", + "03072200", + "03072900", + "03073100", + "03073200", + "03073900", + "03074200", + "03074310", + "03074320", + "03074900", + "03075100", + "03075200", + "03075900", + "03076000", + "03077100", + "03077200", + "03077900", + "03078100", + "03078200", + "03078300", + "03078400", + "03078700", + "03078800", + "03079100", + "03079200", + "03079900", + "03081100", + "03081200", + "03081900", + "03082100", + "03082200", + "03082900", + "03083000", + "03089000", + "03091000", + "03099000", + "04011010", + "04011090", + "04012010", + "04012090", + "04014010", + "04014021", + "04014029", + "04015010", + "04015021", + "04015029", + "04021010", + "04021090", + "04022110", + "04022120", + "04022130", + "04022910", + "04022920", + "04022930", + "04029100", + "04029900", + "04032000", + "04039000", + "04041000", + "04049000", + "04051000", + "04052000", + "04059010", + "04059090", + "04061010", + "04061090", + "04062000", + "04063000", + "04064000", + "04069010", + "04069020", + "04069030", + "04069090", + "04071100", + "04071900", + "04072100", + "04072900", + "04079000", + "04081100", + "04081900", + "04089100", + "04089900", + "04090000", + "04101000", + "04109000", + "05010000", + "05021011", + "05021019", + "05021090", + "05029010", + "05029020", + "05040011", + "05040012", + "05040013", + "05040019", + "05040090", + "05051000", + "05059000", + "05061000", + "05069000", + "05071000", + "05079000", + "05080000", + "05100010", + "05100090", + "05111000", + "05119110", + "05119190", + "05119910", + "05119920", + "05119930", + "05119991", + "05119999", + "06011000", + "06012000", + "06021000", + "06022000", + "06023000", + "06024000", + "06029010", + "06029021", + "06029029", + "06029081", + "06029082", + "06029083", + "06029089", + "06029090", + "06031100", + "06031200", + "06031300", + "06031400", + "06031500", + "06031900", + "06039000", + "06042000", + "06049000", + "07011000", + "07019000", + "07020000", + "07031011", + "07031019", + "07031021", + "07031029", + "07032010", + "07032090", + "07039010", + "07039090", + "07041000", + "07042000", + "07049000", + "07051100", + "07051900", + "07052100", + "07052900", + "07061000", + "07069000", + "07070000", + "07081000", + "07082000", + "07089000", + "07092000", + "07093000", + "07094000", + "07095100", + "07095200", + "07095300", + "07095400", + "07095500", + "07095600", + "07095900", + "07096000", + "07097000", + "07099100", + "07099200", + "07099300", + "07099911", + "07099919", + "07099990", + "07101000", + "07102100", + "07102200", + "07102900", + "07103000", + "07104000", + "07108000", + "07109000", + "07112010", + "07112020", + "07112090", + "07114000", + "07115100", + "07115900", + "07119000", + "07122000", + "07123100", + "07123200", + "07123300", + "07123400", + "07123900", + "07129020", + "07129090", + "07131010", + "07131090", + "07132010", + "07132090", + "07133110", + "07133190", + "07133210", + "07133290", + "07133311", + "07133319", + "07133321", + "07133329", + "07133391", + "07133399", + "07133410", + "07133490", + "07133510", + "07133590", + "07133910", + "07133990", + "07134010", + "07134090", + "07135010", + "07135090", + "07136010", + "07136090", + "07139010", + "07139090", + "07141000", + "07142000", + "07143000", + "07144000", + "07145000", + "07149000", + "08011100", + "08011200", + "08011900", + "08012100", + "08012200", + "08013100", + "08013200", + "08021100", + "08021200", + "08022100", + "08022200", + "08023100", + "08023200", + "08024100", + "08024200", + "08025100", + "08025200", + "08026100", + "08026200", + "08027000", + "08028000", + "08029100", + "08029200", + "08029900", + "08031000", + "08039000", + "08041010", + "08041020", + "08042010", + "08042020", + "08043000", + "08044000", + "08045010", + "08045020", + "08045030", + "08051000", + "08052100", + "08052200", + "08052900", + "08054000", + "08055000", + "08059000", + "08061000", + "08062000", + "08071100", + "08071900", + "08072000", + "08081000", + "08083000", + "08084000", + "08091000", + "08092100", + "08092900", + "08093010", + "08093020", + "08094000", + "08101000", + "08102000", + "08103000", + "08104000", + "08105000", + "08106000", + "08107000", + "08109011", + "08109012", + "08109013", + "08109014", + "08109015", + "08109016", + "08109017", + "08109090", + "08111000", + "08112000", + "08119000", + "08121000", + "08129000", + "08131000", + "08132010", + "08132020", + "08133000", + "08134010", + "08134090", + "08135000", + "08140000", + "09011110", + "09011190", + "09011200", + "09012100", + "09012200", + "09019000", + "09021000", + "09022000", + "09023000", + "09024000", + "09030010", + "09030090", + "09041100", + "09041200", + "09042100", + "09042200", + "09051000", + "09052000", + "09061100", + "09061900", + "09062000", + "09071000", + "09072000", + "09081100", + "09081200", + "09082100", + "09082200", + "09083100", + "09083200", + "09092100", + "09092200", + "09093100", + "09093200", + "09096110", + "09096120", + "09096190", + "09096210", + "09096220", + "09096290", + "09101100", + "09101200", + "09102000", + "09103000", + "09109100", + "09109900", + "10011100", + "10011900", + "10019100", + "10019900", + "10021000", + "10029000", + "10031000", + "10039010", + "10039080", + "10039090", + "10041000", + "10049000", + "10051000", + "10059010", + "10059090", + "10061010", + "10061091", + "10061092", + "10062010", + "10062020", + "10063011", + "10063019", + "10063021", + "10063029", + "10064000", + "10071000", + "10079000", + "10081010", + "10081090", + "10082110", + "10082190", + "10082910", + "10082990", + "10083010", + "10083090", + "10084010", + "10084090", + "10085010", + "10085090", + "10086010", + "10086090", + "10089010", + "10089090", + "11010010", + "11010020", + "11022000", + "11029000", + "11031100", + "11031300", + "11031900", + "11032000", + "11041200", + "11041900", + "11042200", + "11042300", + "11042900", + "11043000", + "11051000", + "11052000", + "11061000", + "11062000", + "11063000", + "11071010", + "11071020", + "11072010", + "11072020", + "11081100", + "11081200", + "11081300", + "11081400", + "11081900", + "11082000", + "11090000", + "12011000", + "12019000", + "12023000", + "12024100", + "12024200", + "12030000", + "12040010", + "12040090", + "12051010", + "12051090", + "12059010", + "12059090", + "12060010", + "12060090", + "12071010", + "12071090", + "12072100", + "12072900", + "12073010", + "12073090", + "12074010", + "12074090", + "12075010", + "12075090", + "12076010", + "12076090", + "12077010", + "12077090", + "12079110", + "12079190", + "12079910", + "12079990", + "12081000", + "12089000", + "12091000", + "12092100", + "12092200", + "12092300", + "12092400", + "12092500", + "12092900", + "12093000", + "12099100", + "12099900", + "12101000", + "12102010", + "12102020", + "12112000", + "12113000", + "12114000", + "12115000", + "12116000", + "12119010", + "12119090", + "12122100", + "12122900", + "12129100", + "12129200", + "12129300", + "12129400", + "12129910", + "12129990", + "12130000", + "12141000", + "12149000", + "13012000", + "13019010", + "13019090", + "13021110", + "13021190", + "13021200", + "13021300", + "13021400", + "13021910", + "13021920", + "13021930", + "13021940", + "13021950", + "13021960", + "13021991", + "13021999", + "13022010", + "13022090", + "13023100", + "13023211", + "13023219", + "13023220", + "13023910", + "13023990", + "14011000", + "14012000", + "14019000", + "14042010", + "14042090", + "14049010", + "14049090", + "15011000", + "15012000", + "15019000", + "15021011", + "15021012", + "15021019", + "15021090", + "15029000", + "15030000", + "15041011", + "15041019", + "15041090", + "15042000", + "15043000", + "15050010", + "15050090", + "15060000", + "15071000", + "15079011", + "15079019", + "15079090", + "15081000", + "15089000", + "15092000", + "15093000", + "15094000", + "15099010", + "15099090", + "15101000", + "15109000", + "15111000", + "15119000", + "15121110", + "15121120", + "15121911", + "15121919", + "15121920", + "15122100", + "15122910", + "15122990", + "15131100", + "15131900", + "15132111", + "15132119", + "15132120", + "15132911", + "15132919", + "15132920", + "15141100", + "15141910", + "15141990", + "15149100", + "15149910", + "15149990", + "15151100", + "15151900", + "15152100", + "15152910", + "15152990", + "15153000", + "15155000", + "15156000", + "15159010", + "15159021", + "15159022", + "15159090", + "15161000", + "15162000", + "15163000", + "15171000", + "15179010", + "15179090", + "15180010", + "15180090", + "15200010", + "15200020", + "15211000", + "15219011", + "15219019", + "15219090", + "15220000", + "16010000", + "16021000", + "16022000", + "16023100", + "16023210", + "16023220", + "16023230", + "16023290", + "16023900", + "16024100", + "16024200", + "16024900", + "16025000", + "16029000", + "16030000", + "16041100", + "16041200", + "16041310", + "16041390", + "16041410", + "16041420", + "16041430", + "16041500", + "16041600", + "16041700", + "16041800", + "16041900", + "16042010", + "16042020", + "16042030", + "16042090", + "16043100", + "16043200", + "16051000", + "16052100", + "16052900", + "16053000", + "16054000", + "16055100", + "16055200", + "16055300", + "16055400", + "16055500", + "16055600", + "16055700", + "16055800", + "16055900", + "16056100", + "16056200", + "16056300", + "16056900", + "17011200", + "17011300", + "17011400", + "17019100", + "17019900", + "17021100", + "17021900", + "17022000", + "17023011", + "17023019", + "17023020", + "17024010", + "17024020", + "17025000", + "17026010", + "17026020", + "17029000", + "17031000", + "17039000", + "17041000", + "17049010", + "17049020", + "17049090", + "18010000", + "18020000", + "18031000", + "18032000", + "18040000", + "18050000", + "18061000", + "18062000", + "18063110", + "18063120", + "18063210", + "18063220", + "18069000", + "19011010", + "19011020", + "19011030", + "19011090", + "19012010", + "19012020", + "19012090", + "19019010", + "19019020", + "19019090", + "19021100", + "19021900", + "19022000", + "19023000", + "19024000", + "19030000", + "19041000", + "19042000", + "19043000", + "19049000", + "19051000", + "19052010", + "19052090", + "19053100", + "19053200", + "19054000", + "19059010", + "19059020", + "19059090", + "20011000", + "20019000", + "20021000", + "20029000", + "20031000", + "20039000", + "20041000", + "20049000", + "20051000", + "20052000", + "20054000", + "20055100", + "20055900", + "20056000", + "20057000", + "20058000", + "20059100", + "20059900", + "20060000", + "20071000", + "20079100", + "20079910", + "20079921", + "20079922", + "20079923", + "20079924", + "20079925", + "20079926", + "20079927", + "20079929", + "20079990", + "20081100", + "20081900", + "20082010", + "20082090", + "20083000", + "20084010", + "20084090", + "20085000", + "20086010", + "20086090", + "20087010", + "20087020", + "20087090", + "20088000", + "20089100", + "20089300", + "20089710", + "20089790", + "20089900", + "20091100", + "20091200", + "20091900", + "20092100", + "20092900", + "20093100", + "20093900", + "20094100", + "20094900", + "20095000", + "20096100", + "20096900", + "20097100", + "20097900", + "20098100", + "20098911", + "20098912", + "20098913", + "20098919", + "20098921", + "20098922", + "20098990", + "20099000", + "21011110", + "21011190", + "21011200", + "21012010", + "21012020", + "21013000", + "21021010", + "21021090", + "21022000", + "21023000", + "21031010", + "21031090", + "21032010", + "21032090", + "21033010", + "21033021", + "21033029", + "21039011", + "21039019", + "21039021", + "21039029", + "21039091", + "21039099", + "21041011", + "21041019", + "21041021", + "21041029", + "21042000", + "21050010", + "21050090", + "21061000", + "21069010", + "21069021", + "21069029", + "21069030", + "21069040", + "21069050", + "21069060", + "21069090", + "22011000", + "22019000", + "22021000", + "22029100", + "22029900", + "22030000", + "22041010", + "22041090", + "22042100", + "22042211", + "22042219", + "22042220", + "22042910", + "22042920", + "22043000", + "22051000", + "22059000", + "22060010", + "22060090", + "22071010", + "22071090", + "22072011", + "22072019", + "22072020", + "22082000", + "22083010", + "22083020", + "22083090", + "22084000", + "22085000", + "22086000", + "22087000", + "22089000", + "22090000", + "23011010", + "23011090", + "23012010", + "23012090", + "23021000", + "23023010", + "23023090", + "23024000", + "23025000", + "23031000", + "23032000", + "23033000", + "23040010", + "23040090", + "23050000", + "23061000", + "23062000", + "23063010", + "23063090", + "23064100", + "23064900", + "23065000", + "23066000", + "23069010", + "23069090", + "23070000", + "23080000", + "23091000", + "23099010", + "23099020", + "23099030", + "23099040", + "23099050", + "23099060", + "23099070", + "23099090", + "24011010", + "24011020", + "24011030", + "24011040", + "24011090", + "24012010", + "24012020", + "24012030", + "24012040", + "24012090", + "24013000", + "24021000", + "24022000", + "24029000", + "24031100", + "24031900", + "24039100", + "24039910", + "24039990", + "24041100", + "24041200", + "24041900", + "24049100", + "24049200", + "24049900", + "25010011", + "25010019", + "25010020", + "25010090", + "25020000", + "25030010", + "25030090", + "25041000", + "25049000", + "25051000", + "25059000", + "25061000", + "25062000", + "25070010", + "25070090", + "25081000", + "25083000", + "25084010", + "25084090", + "25085000", + "25086000", + "25087000", + "25090000", + "25101010", + "25101090", + "25102010", + "25102090", + "25111000", + "25112000", + "25120000", + "25131000", + "25132000", + "25140000", + "25151100", + "25151210", + "25151220", + "25152000", + "25161100", + "25161200", + "25162000", + "25169000", + "25171000", + "25172000", + "25173000", + "25174100", + "25174900", + "25181000", + "25182000", + "25191000", + "25199010", + "25199090", + "25201011", + "25201019", + "25201020", + "25202010", + "25202090", + "25210000", + "25221000", + "25222000", + "25223000", + "25231000", + "25232100", + "25232910", + "25232990", + "25233000", + "25239000", + "25241000", + "25249000", + "25251000", + "25252000", + "25253000", + "25261000", + "25262000", + "25280000", + "25291000", + "25292100", + "25292200", + "25293000", + "25301010", + "25301090", + "25302000", + "25309010", + "25309020", + "25309030", + "25309040", + "25309090", + "26011100", + "26011210", + "26011220", + "26011290", + "26012000", + "26020010", + "26020090", + "26030010", + "26030090", + "26040000", + "26050000", + "26060011", + "26060012", + "26060090", + "26070000", + "26080010", + "26080090", + "26090000", + "26100010", + "26100090", + "26110000", + "26121000", + "26122000", + "26131010", + "26131090", + "26139010", + "26139090", + "26140010", + "26140090", + "26151010", + "26151020", + "26151090", + "26159000", + "26161000", + "26169000", + "26171000", + "26179000", + "26180000", + "26190000", + "26201100", + "26201900", + "26202100", + "26202900", + "26203000", + "26204000", + "26206000", + "26209100", + "26209910", + "26209990", + "26211000", + "26219010", + "26219090", + "27011100", + "27011200", + "27011900", + "27012000", + "27021000", + "27022000", + "27030000", + "27040011", + "27040012", + "27040090", + "27050000", + "27060000", + "27071000", + "27072000", + "27073000", + "27074000", + "27075010", + "27075090", + "27079100", + "27079910", + "27079990", + "27081000", + "27082000", + "27090010", + "27090090", + "27101210", + "27101221", + "27101229", + "27101230", + "27101241", + "27101249", + "27101251", + "27101259", + "27101260", + "27101290", + "27101911", + "27101919", + "27101921", + "27101922", + "27101929", + "27101931", + "27101932", + "27101991", + "27101992", + "27101993", + "27101994", + "27101999", + "27102000", + "27109110", + "27109120", + "27109190", + "27109900", + "27111100", + "27111210", + "27111290", + "27111300", + "27111400", + "27111910", + "27111990", + "27112100", + "27112910", + "27112990", + "27121000", + "27122000", + "27129000", + "27131100", + "27131200", + "27132000", + "27139000", + "27141000", + "27149000", + "27150000", + "27160000", + "28011000", + "28012010", + "28012090", + "28013000", + "28020000", + "28030011", + "28030019", + "28030090", + "28041000", + "28042100", + "28042910", + "28042990", + "28043000", + "28044000", + "28045000", + "28046100", + "28046900", + "28047010", + "28047020", + "28047030", + "28048000", + "28049000", + "28051100", + "28051200", + "28051910", + "28051920", + "28051990", + "28053010", + "28053090", + "28054000", + "28061010", + "28061020", + "28062000", + "28070010", + "28070020", + "28080010", + "28080020", + "28091000", + "28092011", + "28092019", + "28092020", + "28092030", + "28092090", + "28100010", + "28100090", + "28111100", + "28111200", + "28111910", + "28111920", + "28111930", + "28111940", + "28111990", + "28112100", + "28112210", + "28112220", + "28112230", + "28112290", + "28112910", + "28112990", + "28121100", + "28121200", + "28121300", + "28121400", + "28121500", + "28121600", + "28121700", + "28121911", + "28121919", + "28121920", + "28129000", + "28131000", + "28139010", + "28139090", + "28141000", + "28142000", + "28151100", + "28151200", + "28152000", + "28153000", + "28161010", + "28161020", + "28164010", + "28164090", + "28170010", + "28170020", + "28181010", + "28181090", + "28182010", + "28182090", + "28183000", + "28191000", + "28199010", + "28199020", + "28201010", + "28201090", + "28209010", + "28209020", + "28209030", + "28209040", + "28211011", + "28211019", + "28211020", + "28211030", + "28211090", + "28212000", + "28220010", + "28220090", + "28230010", + "28230090", + "28241000", + "28249010", + "28249090", + "28251010", + "28251020", + "28252010", + "28252020", + "28253010", + "28253090", + "28254010", + "28254090", + "28255010", + "28255090", + "28256010", + "28256020", + "28257010", + "28257090", + "28258010", + "28258090", + "28259010", + "28259020", + "28259090", + "28261200", + "28261910", + "28261920", + "28261990", + "28263000", + "28269010", + "28269020", + "28269090", + "28271000", + "28272010", + "28272090", + "28273110", + "28273190", + "28273200", + "28273500", + "28273910", + "28273920", + "28273931", + "28273939", + "28273940", + "28273950", + "28273960", + "28273970", + "28273991", + "28273992", + "28273993", + "28273994", + "28273995", + "28273996", + "28273997", + "28273999", + "28274110", + "28274120", + "28274911", + "28274912", + "28274919", + "28274921", + "28274929", + "28275100", + "28275900", + "28276011", + "28276012", + "28276019", + "28276021", + "28276029", + "28281000", + "28289011", + "28289019", + "28289020", + "28289090", + "28291100", + "28291910", + "28291920", + "28291990", + "28299011", + "28299012", + "28299019", + "28299021", + "28299022", + "28299029", + "28299031", + "28299032", + "28299039", + "28299040", + "28299050", + "28301010", + "28301020", + "28309011", + "28309012", + "28309013", + "28309014", + "28309015", + "28309016", + "28309019", + "28309020", + "28311011", + "28311019", + "28311021", + "28311029", + "28319010", + "28319090", + "28321010", + "28321090", + "28322000", + "28323010", + "28323020", + "28323090", + "28331110", + "28331190", + "28331900", + "28332100", + "28332200", + "28332400", + "28332510", + "28332520", + "28332710", + "28332790", + "28332910", + "28332920", + "28332930", + "28332940", + "28332950", + "28332960", + "28332970", + "28332990", + "28333000", + "28334010", + "28334020", + "28334090", + "28341010", + "28341090", + "28342110", + "28342190", + "28342910", + "28342930", + "28342940", + "28342990", + "28351011", + "28351019", + "28351021", + "28351029", + "28352200", + "28352400", + "28352500", + "28352600", + "28352910", + "28352920", + "28352930", + "28352940", + "28352950", + "28352960", + "28352970", + "28352980", + "28352990", + "28353110", + "28353190", + "28353910", + "28353920", + "28353930", + "28353990", + "28362010", + "28362090", + "28363000", + "28364000", + "28365000", + "28366010", + "28366090", + "28369100", + "28369200", + "28369911", + "28369912", + "28369913", + "28369919", + "28369920", + "28371100", + "28371911", + "28371912", + "28371914", + "28371915", + "28371919", + "28371920", + "28372011", + "28372012", + "28372019", + "28372021", + "28372022", + "28372023", + "28372029", + "28372090", + "28391100", + "28391900", + "28399010", + "28399020", + "28399030", + "28399040", + "28399050", + "28399090", + "28401100", + "28401900", + "28402000", + "28403000", + "28413000", + "28415011", + "28415012", + "28415013", + "28415014", + "28415015", + "28415016", + "28415019", + "28415020", + "28416100", + "28416910", + "28416920", + "28416930", + "28417010", + "28417020", + "28417090", + "28418010", + "28418020", + "28418090", + "28419011", + "28419012", + "28419013", + "28419014", + "28419015", + "28419019", + "28419021", + "28419022", + "28419029", + "28419030", + "28419041", + "28419042", + "28419043", + "28419049", + "28419050", + "28419060", + "28419070", + "28419081", + "28419082", + "28419083", + "28419089", + "28419090", + "28421010", + "28421090", + "28429000", + "28431000", + "28432100", + "28432910", + "28432990", + "28433010", + "28433090", + "28439011", + "28439019", + "28439020", + "28439030", + "28439040", + "28439090", + "28441000", + "28442000", + "28443000", + "28444100", + "28444200", + "28444310", + "28444320", + "28444330", + "28444390", + "28444400", + "28445000", + "28451000", + "28452000", + "28453000", + "28454000", + "28459000", + "28461010", + "28461090", + "28469010", + "28469020", + "28469030", + "28469090", + "28470000", + "28491000", + "28492000", + "28499010", + "28499020", + "28499030", + "28499090", + "28500010", + "28500020", + "28500090", + "28521011", + "28521012", + "28521013", + "28521014", + "28521019", + "28521021", + "28521022", + "28521023", + "28521024", + "28521025", + "28521029", + "28529000", + "28531000", + "28539011", + "28539012", + "28539013", + "28539019", + "28539020", + "28539030", + "28539090", + "29011000", + "29012100", + "29012200", + "29012300", + "29012410", + "29012420", + "29012900", + "29021100", + "29021910", + "29021990", + "29022000", + "29023000", + "29024100", + "29024200", + "29024300", + "29024400", + "29025000", + "29026000", + "29027000", + "29029010", + "29029020", + "29029030", + "29029040", + "29029090", + "29031110", + "29031120", + "29031200", + "29031300", + "29031400", + "29031500", + "29031910", + "29031920", + "29031990", + "29032100", + "29032200", + "29032300", + "29032910", + "29032990", + "29034100", + "29034200", + "29034300", + "29034400", + "29034510", + "29034520", + "29034600", + "29034700", + "29034800", + "29034900", + "29035100", + "29035910", + "29035990", + "29036100", + "29036200", + "29036910", + "29036920", + "29036990", + "29037100", + "29037200", + "29037300", + "29037400", + "29037500", + "29037600", + "29037711", + "29037712", + "29037713", + "29037721", + "29037722", + "29037723", + "29037724", + "29037731", + "29037732", + "29037733", + "29037734", + "29037735", + "29037736", + "29037737", + "29037790", + "29037800", + "29037911", + "29037912", + "29037919", + "29037920", + "29037931", + "29037939", + "29037990", + "29038110", + "29038120", + "29038130", + "29038190", + "29038210", + "29038220", + "29038230", + "29038300", + "29038910", + "29038990", + "29039110", + "29039120", + "29039130", + "29039210", + "29039220", + "29039300", + "29039400", + "29039911", + "29039912", + "29039913", + "29039914", + "29039915", + "29039916", + "29039917", + "29039918", + "29039919", + "29039921", + "29039922", + "29039923", + "29039924", + "29039929", + "29039931", + "29039939", + "29039990", + "29041011", + "29041012", + "29041013", + "29041019", + "29041020", + "29041030", + "29041040", + "29041051", + "29041052", + "29041053", + "29041059", + "29041060", + "29041090", + "29042010", + "29042020", + "29042030", + "29042041", + "29042049", + "29042051", + "29042052", + "29042059", + "29042060", + "29042070", + "29042090", + "29043100", + "29043200", + "29043300", + "29043400", + "29043500", + "29043600", + "29049100", + "29049911", + "29049912", + "29049913", + "29049914", + "29049915", + "29049916", + "29049919", + "29049921", + "29049929", + "29049930", + "29049940", + "29049990", + "29051100", + "29051210", + "29051220", + "29051300", + "29051410", + "29051420", + "29051430", + "29051600", + "29051710", + "29051720", + "29051730", + "29051911", + "29051912", + "29051919", + "29051921", + "29051922", + "29051923", + "29051929", + "29051991", + "29051992", + "29051993", + "29051994", + "29051995", + "29051996", + "29051999", + "29052210", + "29052220", + "29052230", + "29052290", + "29052910", + "29052990", + "29053100", + "29053200", + "29053910", + "29053920", + "29053930", + "29053990", + "29054100", + "29054200", + "29054300", + "29054400", + "29054500", + "29054900", + "29055100", + "29055910", + "29055990", + "29061100", + "29061200", + "29061300", + "29061910", + "29061920", + "29061930", + "29061940", + "29061950", + "29061990", + "29062100", + "29062910", + "29062920", + "29062990", + "29071100", + "29071200", + "29071300", + "29071510", + "29071590", + "29071910", + "29071920", + "29071930", + "29071940", + "29071990", + "29072100", + "29072200", + "29072300", + "29072900", + "29081100", + "29081911", + "29081912", + "29081913", + "29081914", + "29081915", + "29081916", + "29081919", + "29081921", + "29081929", + "29081990", + "29089100", + "29089200", + "29089912", + "29089913", + "29089919", + "29089921", + "29089929", + "29089930", + "29089990", + "29091100", + "29091910", + "29091920", + "29091990", + "29092000", + "29093011", + "29093012", + "29093013", + "29093014", + "29093019", + "29093021", + "29093022", + "29093023", + "29093024", + "29093025", + "29093029", + "29094100", + "29094310", + "29094320", + "29094411", + "29094412", + "29094413", + "29094419", + "29094421", + "29094429", + "29094910", + "29094921", + "29094922", + "29094923", + "29094924", + "29094929", + "29094931", + "29094932", + "29094939", + "29094941", + "29094949", + "29094950", + "29094990", + "29095011", + "29095012", + "29095013", + "29095019", + "29095090", + "29096011", + "29096012", + "29096013", + "29096019", + "29096090", + "29101000", + "29102000", + "29103000", + "29104000", + "29105000", + "29109010", + "29109090", + "29110010", + "29110090", + "29121100", + "29121200", + "29121911", + "29121912", + "29121919", + "29121921", + "29121922", + "29121923", + "29121929", + "29121991", + "29121999", + "29122100", + "29122910", + "29122920", + "29122990", + "29124100", + "29124200", + "29124910", + "29124920", + "29124930", + "29124941", + "29124949", + "29124990", + "29125000", + "29126000", + "29130010", + "29130090", + "29141100", + "29141200", + "29141300", + "29141910", + "29141921", + "29141922", + "29141923", + "29141929", + "29141930", + "29141940", + "29141950", + "29141990", + "29142210", + "29142220", + "29142310", + "29142320", + "29142910", + "29142920", + "29142990", + "29143100", + "29143910", + "29143990", + "29144010", + "29144091", + "29144099", + "29145010", + "29145020", + "29145090", + "29146100", + "29146200", + "29146910", + "29146920", + "29146990", + "29147100", + "29147911", + "29147919", + "29147921", + "29147922", + "29147929", + "29147990", + "29151100", + "29151210", + "29151290", + "29151310", + "29151390", + "29152100", + "29152400", + "29152910", + "29152920", + "29152990", + "29153100", + "29153200", + "29153300", + "29153600", + "29153910", + "29153921", + "29153929", + "29153931", + "29153932", + "29153939", + "29153941", + "29153942", + "29153951", + "29153952", + "29153953", + "29153954", + "29153955", + "29153961", + "29153962", + "29153963", + "29153991", + "29153992", + "29153993", + "29153994", + "29153999", + "29154010", + "29154020", + "29154090", + "29155010", + "29155020", + "29155030", + "29156011", + "29156012", + "29156019", + "29156021", + "29156029", + "29157011", + "29157019", + "29157020", + "29157031", + "29157039", + "29157040", + "29159010", + "29159021", + "29159022", + "29159023", + "29159024", + "29159029", + "29159031", + "29159032", + "29159033", + "29159039", + "29159041", + "29159043", + "29159049", + "29159050", + "29159060", + "29159070", + "29159090", + "29161110", + "29161120", + "29161210", + "29161220", + "29161230", + "29161240", + "29161290", + "29161310", + "29161320", + "29161410", + "29161420", + "29161430", + "29161490", + "29161511", + "29161519", + "29161520", + "29161600", + "29161911", + "29161919", + "29161921", + "29161922", + "29161923", + "29161929", + "29161990", + "29162011", + "29162012", + "29162013", + "29162014", + "29162015", + "29162019", + "29162090", + "29163110", + "29163121", + "29163122", + "29163129", + "29163131", + "29163132", + "29163139", + "29163210", + "29163220", + "29163400", + "29163910", + "29163920", + "29163930", + "29163940", + "29163990", + "29171110", + "29171120", + "29171210", + "29171220", + "29171310", + "29171321", + "29171322", + "29171323", + "29171329", + "29171400", + "29171910", + "29171921", + "29171922", + "29171930", + "29171990", + "29172000", + "29173200", + "29173300", + "29173400", + "29173500", + "29173600", + "29173700", + "29173911", + "29173919", + "29173920", + "29173931", + "29173939", + "29173940", + "29173950", + "29173990", + "29181100", + "29181200", + "29181310", + "29181320", + "29181400", + "29181500", + "29181610", + "29181690", + "29181700", + "29181800", + "29181910", + "29181921", + "29181922", + "29181929", + "29181930", + "29181942", + "29181943", + "29181990", + "29182110", + "29182120", + "29182211", + "29182212", + "29182219", + "29182220", + "29182300", + "29182910", + "29182921", + "29182922", + "29182923", + "29182929", + "29182930", + "29182940", + "29182950", + "29182990", + "29183010", + "29183020", + "29183031", + "29183032", + "29183033", + "29183039", + "29183040", + "29183090", + "29189100", + "29189911", + "29189912", + "29189919", + "29189921", + "29189929", + "29189930", + "29189940", + "29189950", + "29189960", + "29189991", + "29189992", + "29189993", + "29189994", + "29189999", + "29191000", + "29199010", + "29199020", + "29199030", + "29199040", + "29199050", + "29199060", + "29199090", + "29201110", + "29201120", + "29201910", + "29201920", + "29201990", + "29202100", + "29202200", + "29202300", + "29202400", + "29202910", + "29202920", + "29202930", + "29202940", + "29202950", + "29202990", + "29203000", + "29209022", + "29209029", + "29209031", + "29209032", + "29209033", + "29209039", + "29209041", + "29209042", + "29209049", + "29209051", + "29209059", + "29209090", + "29211111", + "29211112", + "29211121", + "29211122", + "29211123", + "29211129", + "29211131", + "29211132", + "29211139", + "29211200", + "29211300", + "29211400", + "29211911", + "29211912", + "29211913", + "29211914", + "29211915", + "29211919", + "29211921", + "29211922", + "29211923", + "29211924", + "29211929", + "29211931", + "29211939", + "29211941", + "29211949", + "29211991", + "29211992", + "29211993", + "29211994", + "29211999", + "29212100", + "29212200", + "29212910", + "29212920", + "29212990", + "29213011", + "29213012", + "29213019", + "29213020", + "29213090", + "29214100", + "29214211", + "29214219", + "29214221", + "29214229", + "29214231", + "29214239", + "29214241", + "29214249", + "29214290", + "29214311", + "29214319", + "29214321", + "29214322", + "29214323", + "29214329", + "29214410", + "29214421", + "29214422", + "29214429", + "29214500", + "29214610", + "29214620", + "29214630", + "29214640", + "29214650", + "29214660", + "29214670", + "29214680", + "29214690", + "29214910", + "29214921", + "29214922", + "29214929", + "29214931", + "29214939", + "29214990", + "29215111", + "29215112", + "29215119", + "29215120", + "29215131", + "29215132", + "29215133", + "29215134", + "29215135", + "29215139", + "29215190", + "29215911", + "29215919", + "29215921", + "29215929", + "29215931", + "29215932", + "29215939", + "29215990", + "29221100", + "29221200", + "29221400", + "29221500", + "29221600", + "29221700", + "29221800", + "29221911", + "29221912", + "29221913", + "29221919", + "29221921", + "29221929", + "29221931", + "29221939", + "29221941", + "29221949", + "29221951", + "29221952", + "29221959", + "29221971", + "29221979", + "29221981", + "29221989", + "29221991", + "29221992", + "29221993", + "29221994", + "29221995", + "29221996", + "29221999", + "29222100", + "29222911", + "29222919", + "29222920", + "29222990", + "29223111", + "29223112", + "29223120", + "29223130", + "29223910", + "29223921", + "29223929", + "29223990", + "29224110", + "29224190", + "29224210", + "29224220", + "29224300", + "29224410", + "29224420", + "29224910", + "29224920", + "29224931", + "29224932", + "29224940", + "29224951", + "29224952", + "29224959", + "29224961", + "29224962", + "29224963", + "29224964", + "29224969", + "29224990", + "29225011", + "29225019", + "29225031", + "29225032", + "29225039", + "29225091", + "29225099", + "29231000", + "29232000", + "29233000", + "29234000", + "29239010", + "29239020", + "29239030", + "29239040", + "29239050", + "29239060", + "29239090", + "29241100", + "29241210", + "29241220", + "29241230", + "29241911", + "29241919", + "29241921", + "29241922", + "29241929", + "29241931", + "29241932", + "29241939", + "29241942", + "29241949", + "29241991", + "29241992", + "29241993", + "29241994", + "29241999", + "29242111", + "29242119", + "29242120", + "29242190", + "29242300", + "29242400", + "29242500", + "29242911", + "29242912", + "29242913", + "29242914", + "29242915", + "29242919", + "29242920", + "29242931", + "29242932", + "29242939", + "29242941", + "29242943", + "29242944", + "29242945", + "29242946", + "29242947", + "29242949", + "29242951", + "29242952", + "29242959", + "29242961", + "29242962", + "29242963", + "29242964", + "29242969", + "29242991", + "29242992", + "29242993", + "29242994", + "29242995", + "29242996", + "29242999", + "29251100", + "29251200", + "29251910", + "29251990", + "29252100", + "29252911", + "29252919", + "29252921", + "29252922", + "29252923", + "29252929", + "29252930", + "29252940", + "29252950", + "29252990", + "29261000", + "29262000", + "29263011", + "29263012", + "29263020", + "29264000", + "29269011", + "29269012", + "29269019", + "29269021", + "29269022", + "29269023", + "29269024", + "29269025", + "29269026", + "29269029", + "29269030", + "29269091", + "29269092", + "29269093", + "29269095", + "29269096", + "29269099", + "29270010", + "29270021", + "29270029", + "29270030", + "29280011", + "29280019", + "29280020", + "29280030", + "29280041", + "29280042", + "29280090", + "29291010", + "29291021", + "29291029", + "29291030", + "29291090", + "29299011", + "29299012", + "29299019", + "29299031", + "29299039", + "29299040", + "29299050", + "29299060", + "29299090", + "29301000", + "29302011", + "29302012", + "29302013", + "29302019", + "29302021", + "29302022", + "29302023", + "29302024", + "29302029", + "29303011", + "29303012", + "29303019", + "29303021", + "29303022", + "29303029", + "29303090", + "29304010", + "29304090", + "29306000", + "29307000", + "29308010", + "29308020", + "29308030", + "29309011", + "29309012", + "29309013", + "29309019", + "29309021", + "29309022", + "29309023", + "29309029", + "29309031", + "29309032", + "29309033", + "29309034", + "29309035", + "29309036", + "29309037", + "29309039", + "29309041", + "29309042", + "29309043", + "29309049", + "29309051", + "29309052", + "29309053", + "29309054", + "29309057", + "29309059", + "29309061", + "29309069", + "29309071", + "29309072", + "29309079", + "29309081", + "29309082", + "29309083", + "29309084", + "29309085", + "29309086", + "29309087", + "29309088", + "29309089", + "29309091", + "29309093", + "29309094", + "29309095", + "29309096", + "29309097", + "29309098", + "29309099", + "29311000", + "29312000", + "29314100", + "29314200", + "29314300", + "29314400", + "29314500", + "29314600", + "29314700", + "29314800", + "29314911", + "29314912", + "29314913", + "29314914", + "29314915", + "29314916", + "29314920", + "29314931", + "29314932", + "29314939", + "29314940", + "29314990", + "29315100", + "29315200", + "29315300", + "29315400", + "29315911", + "29315912", + "29315913", + "29315991", + "29315992", + "29315993", + "29315994", + "29315995", + "29315996", + "29315998", + "29315999", + "29319021", + "29319029", + "29319041", + "29319042", + "29319043", + "29319044", + "29319045", + "29319046", + "29319049", + "29319051", + "29319052", + "29319053", + "29319054", + "29319059", + "29319061", + "29319062", + "29319069", + "29319090", + "29321100", + "29321200", + "29321310", + "29321320", + "29321400", + "29321910", + "29321920", + "29321930", + "29321940", + "29321950", + "29321990", + "29322000", + "29329100", + "29329200", + "29329300", + "29329400", + "29329500", + "29329600", + "29329911", + "29329912", + "29329913", + "29329991", + "29329992", + "29329993", + "29329994", + "29329999", + "29331111", + "29331112", + "29331119", + "29331120", + "29331190", + "29331911", + "29331919", + "29331990", + "29332110", + "29332121", + "29332129", + "29332190", + "29332911", + "29332912", + "29332913", + "29332919", + "29332921", + "29332922", + "29332923", + "29332924", + "29332925", + "29332929", + "29332930", + "29332940", + "29332991", + "29332992", + "29332993", + "29332994", + "29332995", + "29332999", + "29333110", + "29333120", + "29333200", + "29333311", + "29333312", + "29333319", + "29333321", + "29333322", + "29333329", + "29333331", + "29333332", + "29333339", + "29333341", + "29333342", + "29333349", + "29333351", + "29333352", + "29333359", + "29333361", + "29333362", + "29333363", + "29333369", + "29333371", + "29333372", + "29333379", + "29333381", + "29333382", + "29333383", + "29333384", + "29333389", + "29333391", + "29333392", + "29333393", + "29333394", + "29333399", + "29333400", + "29333500", + "29333600", + "29333700", + "29333912", + "29333913", + "29333914", + "29333915", + "29333919", + "29333921", + "29333922", + "29333923", + "29333924", + "29333925", + "29333929", + "29333931", + "29333932", + "29333933", + "29333934", + "29333935", + "29333936", + "29333937", + "29333939", + "29333941", + "29333942", + "29333943", + "29333944", + "29333945", + "29333946", + "29333948", + "29333949", + "29333981", + "29333982", + "29333983", + "29333984", + "29333989", + "29333991", + "29333992", + "29333993", + "29333994", + "29333999", + "29334110", + "29334120", + "29334911", + "29334912", + "29334913", + "29334919", + "29334920", + "29334930", + "29334940", + "29334990", + "29335200", + "29335311", + "29335312", + "29335321", + "29335322", + "29335323", + "29335330", + "29335340", + "29335350", + "29335360", + "29335371", + "29335372", + "29335380", + "29335400", + "29335510", + "29335520", + "29335530", + "29335540", + "29335911", + "29335912", + "29335913", + "29335914", + "29335915", + "29335916", + "29335919", + "29335921", + "29335922", + "29335923", + "29335929", + "29335931", + "29335932", + "29335933", + "29335934", + "29335935", + "29335939", + "29335941", + "29335942", + "29335943", + "29335944", + "29335945", + "29335949", + "29335991", + "29335992", + "29335999", + "29336100", + "29336911", + "29336912", + "29336913", + "29336914", + "29336915", + "29336916", + "29336919", + "29336921", + "29336922", + "29336923", + "29336929", + "29336991", + "29336992", + "29336999", + "29337100", + "29337210", + "29337220", + "29337910", + "29337990", + "29339111", + "29339112", + "29339113", + "29339114", + "29339115", + "29339119", + "29339121", + "29339122", + "29339123", + "29339129", + "29339131", + "29339132", + "29339133", + "29339134", + "29339139", + "29339141", + "29339142", + "29339143", + "29339149", + "29339151", + "29339152", + "29339153", + "29339159", + "29339161", + "29339162", + "29339163", + "29339164", + "29339169", + "29339171", + "29339172", + "29339173", + "29339179", + "29339181", + "29339182", + "29339183", + "29339189", + "29339200", + "29339911", + "29339912", + "29339913", + "29339919", + "29339920", + "29339931", + "29339932", + "29339933", + "29339934", + "29339935", + "29339939", + "29339941", + "29339942", + "29339943", + "29339944", + "29339945", + "29339946", + "29339947", + "29339949", + "29339951", + "29339952", + "29339953", + "29339954", + "29339955", + "29339956", + "29339959", + "29339961", + "29339962", + "29339963", + "29339969", + "29339991", + "29339992", + "29339993", + "29339995", + "29339996", + "29339999", + "29341010", + "29341020", + "29341030", + "29341090", + "29342010", + "29342020", + "29342031", + "29342032", + "29342033", + "29342034", + "29342039", + "29342040", + "29342090", + "29343010", + "29343020", + "29343030", + "29343090", + "29349111", + "29349112", + "29349121", + "29349122", + "29349123", + "29349129", + "29349131", + "29349132", + "29349133", + "29349141", + "29349142", + "29349149", + "29349150", + "29349160", + "29349170", + "29349200", + "29349911", + "29349912", + "29349913", + "29349914", + "29349915", + "29349919", + "29349922", + "29349923", + "29349924", + "29349925", + "29349926", + "29349927", + "29349929", + "29349931", + "29349932", + "29349933", + "29349934", + "29349935", + "29349939", + "29349941", + "29349942", + "29349943", + "29349944", + "29349945", + "29349946", + "29349949", + "29349951", + "29349952", + "29349953", + "29349954", + "29349959", + "29349961", + "29349969", + "29349991", + "29349992", + "29349993", + "29349999", + "29351000", + "29352000", + "29353000", + "29354000", + "29355000", + "29359011", + "29359012", + "29359013", + "29359014", + "29359015", + "29359019", + "29359021", + "29359022", + "29359023", + "29359024", + "29359025", + "29359029", + "29359091", + "29359092", + "29359093", + "29359094", + "29359095", + "29359096", + "29359099", + "29362111", + "29362112", + "29362113", + "29362119", + "29362190", + "29362210", + "29362220", + "29362290", + "29362310", + "29362320", + "29362390", + "29362410", + "29362490", + "29362510", + "29362520", + "29362590", + "29362610", + "29362620", + "29362630", + "29362690", + "29362710", + "29362720", + "29362790", + "29362811", + "29362812", + "29362819", + "29362890", + "29362911", + "29362919", + "29362921", + "29362929", + "29362931", + "29362939", + "29362940", + "29362951", + "29362952", + "29362953", + "29362959", + "29362990", + "29369000", + "29371100", + "29371200", + "29371910", + "29371920", + "29371930", + "29371940", + "29371950", + "29371990", + "29372110", + "29372120", + "29372130", + "29372140", + "29372210", + "29372221", + "29372229", + "29372231", + "29372239", + "29372290", + "29372310", + "29372321", + "29372322", + "29372329", + "29372331", + "29372339", + "29372341", + "29372342", + "29372349", + "29372351", + "29372359", + "29372360", + "29372370", + "29372391", + "29372392", + "29372399", + "29372910", + "29372920", + "29372931", + "29372939", + "29372940", + "29372950", + "29372960", + "29372990", + "29375000", + "29379010", + "29379030", + "29379040", + "29379090", + "29381000", + "29389010", + "29389020", + "29389090", + "29391110", + "29391121", + "29391122", + "29391123", + "29391131", + "29391132", + "29391140", + "29391151", + "29391152", + "29391153", + "29391161", + "29391162", + "29391169", + "29391170", + "29391181", + "29391182", + "29391191", + "29391192", + "29391900", + "29392000", + "29393010", + "29393020", + "29394100", + "29394200", + "29394300", + "29394400", + "29394510", + "29394520", + "29394530", + "29394900", + "29395100", + "29395910", + "29395920", + "29395990", + "29396100", + "29396200", + "29396300", + "29396911", + "29396919", + "29396921", + "29396929", + "29396931", + "29396939", + "29396941", + "29396942", + "29396949", + "29396951", + "29396952", + "29396959", + "29396990", + "29397210", + "29397220", + "29397290", + "29397911", + "29397919", + "29397920", + "29397931", + "29397939", + "29397940", + "29397990", + "29398010", + "29398090", + "29400011", + "29400012", + "29400013", + "29400019", + "29400021", + "29400022", + "29400023", + "29400029", + "29400092", + "29400093", + "29400094", + "29400099", + "29411010", + "29411020", + "29411031", + "29411039", + "29411041", + "29411042", + "29411043", + "29411049", + "29411090", + "29412010", + "29412090", + "29413010", + "29413020", + "29413031", + "29413032", + "29413090", + "29414011", + "29414019", + "29414020", + "29414090", + "29415010", + "29415020", + "29415090", + "29419011", + "29419012", + "29419013", + "29419019", + "29419021", + "29419022", + "29419029", + "29419031", + "29419032", + "29419033", + "29419034", + "29419035", + "29419036", + "29419037", + "29419039", + "29419041", + "29419042", + "29419043", + "29419049", + "29419051", + "29419059", + "29419061", + "29419062", + "29419069", + "29419071", + "29419072", + "29419073", + "29419079", + "29419081", + "29419082", + "29419083", + "29419089", + "29419091", + "29419092", + "29419099", + "29420000", + "30012010", + "30012090", + "30019010", + "30019020", + "30019031", + "30019039", + "30019090", + "30021211", + "30021212", + "30021213", + "30021214", + "30021215", + "30021216", + "30021219", + "30021221", + "30021222", + "30021223", + "30021224", + "30021229", + "30021231", + "30021232", + "30021233", + "30021234", + "30021235", + "30021236", + "30021239", + "30021300", + "30021400", + "30021510", + "30021520", + "30021590", + "30024111", + "30024112", + "30024113", + "30024114", + "30024115", + "30024116", + "30024117", + "30024118", + "30024119", + "30024121", + "30024122", + "30024123", + "30024124", + "30024125", + "30024126", + "30024127", + "30024128", + "30024129", + "30024210", + "30024220", + "30024230", + "30024240", + "30024250", + "30024260", + "30024270", + "30024280", + "30024290", + "30024910", + "30024920", + "30024991", + "30024992", + "30024994", + "30024999", + "30025100", + "30025900", + "30029000", + "30031011", + "30031012", + "30031013", + "30031014", + "30031015", + "30031019", + "30031020", + "30032011", + "30032019", + "30032021", + "30032029", + "30032031", + "30032032", + "30032039", + "30032041", + "30032049", + "30032051", + "30032052", + "30032059", + "30032061", + "30032062", + "30032063", + "30032069", + "30032071", + "30032072", + "30032073", + "30032079", + "30032091", + "30032092", + "30032093", + "30032094", + "30032095", + "30032099", + "30033100", + "30033911", + "30033912", + "30033913", + "30033914", + "30033915", + "30033916", + "30033917", + "30033918", + "30033919", + "30033921", + "30033922", + "30033923", + "30033924", + "30033925", + "30033926", + "30033927", + "30033929", + "30033931", + "30033932", + "30033933", + "30033934", + "30033935", + "30033936", + "30033937", + "30033939", + "30033981", + "30033982", + "30033991", + "30033992", + "30033994", + "30033995", + "30033999", + "30034100", + "30034200", + "30034300", + "30034910", + "30034920", + "30034930", + "30034940", + "30034950", + "30034990", + "30036000", + "30039011", + "30039012", + "30039013", + "30039014", + "30039015", + "30039016", + "30039017", + "30039019", + "30039021", + "30039022", + "30039023", + "30039024", + "30039025", + "30039029", + "30039031", + "30039032", + "30039033", + "30039034", + "30039035", + "30039036", + "30039037", + "30039038", + "30039039", + "30039041", + "30039042", + "30039043", + "30039044", + "30039045", + "30039046", + "30039047", + "30039048", + "30039049", + "30039051", + "30039052", + "30039053", + "30039054", + "30039055", + "30039056", + "30039057", + "30039058", + "30039059", + "30039061", + "30039062", + "30039063", + "30039064", + "30039065", + "30039066", + "30039067", + "30039069", + "30039071", + "30039072", + "30039073", + "30039074", + "30039075", + "30039076", + "30039077", + "30039078", + "30039079", + "30039081", + "30039082", + "30039083", + "30039084", + "30039085", + "30039086", + "30039087", + "30039088", + "30039089", + "30039091", + "30039092", + "30039093", + "30039094", + "30039095", + "30039096", + "30039097", + "30039099", + "30041011", + "30041012", + "30041013", + "30041014", + "30041015", + "30041019", + "30041020", + "30042011", + "30042019", + "30042021", + "30042029", + "30042031", + "30042032", + "30042039", + "30042041", + "30042049", + "30042051", + "30042052", + "30042059", + "30042061", + "30042062", + "30042063", + "30042069", + "30042071", + "30042072", + "30042073", + "30042079", + "30042091", + "30042092", + "30042093", + "30042094", + "30042095", + "30042099", + "30043100", + "30043210", + "30043220", + "30043290", + "30043911", + "30043912", + "30043913", + "30043914", + "30043915", + "30043916", + "30043917", + "30043918", + "30043919", + "30043921", + "30043922", + "30043923", + "30043924", + "30043925", + "30043926", + "30043927", + "30043928", + "30043929", + "30043931", + "30043932", + "30043933", + "30043934", + "30043935", + "30043936", + "30043937", + "30043939", + "30043981", + "30043982", + "30043991", + "30043992", + "30043994", + "30043999", + "30044100", + "30044200", + "30044300", + "30044910", + "30044920", + "30044930", + "30044940", + "30044950", + "30044990", + "30045010", + "30045020", + "30045030", + "30045040", + "30045050", + "30045060", + "30045090", + "30046000", + "30049011", + "30049012", + "30049013", + "30049014", + "30049015", + "30049019", + "30049021", + "30049022", + "30049023", + "30049024", + "30049025", + "30049026", + "30049027", + "30049028", + "30049029", + "30049031", + "30049032", + "30049033", + "30049034", + "30049035", + "30049036", + "30049037", + "30049038", + "30049039", + "30049041", + "30049042", + "30049043", + "30049044", + "30049045", + "30049046", + "30049047", + "30049048", + "30049049", + "30049051", + "30049052", + "30049053", + "30049054", + "30049055", + "30049057", + "30049058", + "30049059", + "30049061", + "30049062", + "30049063", + "30049064", + "30049065", + "30049066", + "30049067", + "30049068", + "30049069", + "30049071", + "30049072", + "30049073", + "30049074", + "30049075", + "30049076", + "30049077", + "30049078", + "30049079", + "30049091", + "30049092", + "30049093", + "30049094", + "30049095", + "30049096", + "30049097", + "30049098", + "30049099", + "30051010", + "30051020", + "30051030", + "30051040", + "30051050", + "30051090", + "30059011", + "30059012", + "30059019", + "30059020", + "30059090", + "30061010", + "30061020", + "30061090", + "30063011", + "30063012", + "30063013", + "30063015", + "30063016", + "30063017", + "30063018", + "30063019", + "30063021", + "30063029", + "30064011", + "30064012", + "30064020", + "30065000", + "30066000", + "30067000", + "30069110", + "30069190", + "30069200", + "30069300", + "31010000", + "31021010", + "31021090", + "31022100", + "31022910", + "31022990", + "31023000", + "31024000", + "31025011", + "31025019", + "31025090", + "31026000", + "31028000", + "31029000", + "31031100", + "31031900", + "31039011", + "31039019", + "31039090", + "31042010", + "31042090", + "31043010", + "31043090", + "31049010", + "31049090", + "31051000", + "31052000", + "31053000", + "31054000", + "31055100", + "31055900", + "31056000", + "31059011", + "31059019", + "31059090", + "32011000", + "32012000", + "32019011", + "32019012", + "32019019", + "32019020", + "32019090", + "32021000", + "32029011", + "32029012", + "32029013", + "32029019", + "32029021", + "32029029", + "32029030", + "32030011", + "32030012", + "32030013", + "32030019", + "32030021", + "32030029", + "32030030", + "32041100", + "32041210", + "32041220", + "32041300", + "32041400", + "32041510", + "32041520", + "32041530", + "32041590", + "32041600", + "32041700", + "32041810", + "32041820", + "32041830", + "32041890", + "32041920", + "32041930", + "32041990", + "32042011", + "32042019", + "32042090", + "32049000", + "32050000", + "32061110", + "32061120", + "32061130", + "32061910", + "32061990", + "32062000", + "32064100", + "32064210", + "32064290", + "32064910", + "32064920", + "32064990", + "32065011", + "32065019", + "32065021", + "32065029", + "32071020", + "32071030", + "32071090", + "32072010", + "32072091", + "32072099", + "32073000", + "32074010", + "32074090", + "32081010", + "32081020", + "32081030", + "32082011", + "32082019", + "32082020", + "32082030", + "32089010", + "32089021", + "32089029", + "32089031", + "32089039", + "32091010", + "32091020", + "32099011", + "32099019", + "32099020", + "32100010", + "32100020", + "32100030", + "32110000", + "32121000", + "32129010", + "32129090", + "32131000", + "32139000", + "32141010", + "32141020", + "32149000", + "32151100", + "32151900", + "32159000", + "33011210", + "33011290", + "33011300", + "33011910", + "33011990", + "33012400", + "33012510", + "33012520", + "33012590", + "33012911", + "33012912", + "33012913", + "33012914", + "33012915", + "33012916", + "33012917", + "33012918", + "33012919", + "33012921", + "33012922", + "33012990", + "33013000", + "33019010", + "33019020", + "33019030", + "33019040", + "33021000", + "33029011", + "33029019", + "33029091", + "33029099", + "33030010", + "33030020", + "33041000", + "33042010", + "33042090", + "33043000", + "33049100", + "33049910", + "33049990", + "33051000", + "33052000", + "33053000", + "33059000", + "33061000", + "33062000", + "33069000", + "33071000", + "33072010", + "33072090", + "33073000", + "33074100", + "33074900", + "33079000", + "34011110", + "34011190", + "34011900", + "34012010", + "34012090", + "34013000", + "34023100", + "34023910", + "34023920", + "34023930", + "34023990", + "34024110", + "34024190", + "34024200", + "34024900", + "34025000", + "34029011", + "34029019", + "34029021", + "34029022", + "34029023", + "34029029", + "34029031", + "34029039", + "34029090", + "34031110", + "34031120", + "34031190", + "34031900", + "34039110", + "34039120", + "34039190", + "34039900", + "34042010", + "34042020", + "34049011", + "34049012", + "34049013", + "34049014", + "34049019", + "34049021", + "34049022", + "34049029", + "34051000", + "34052000", + "34053000", + "34054000", + "34059000", + "34060000", + "34070010", + "34070020", + "34070090", + "35011000", + "35019011", + "35019019", + "35019020", + "35021100", + "35021900", + "35022000", + "35029010", + "35029090", + "35030011", + "35030012", + "35030019", + "35030090", + "35040011", + "35040019", + "35040020", + "35040030", + "35040090", + "35051000", + "35052000", + "35061010", + "35061090", + "35069110", + "35069120", + "35069190", + "35069900", + "35071000", + "35079011", + "35079019", + "35079021", + "35079022", + "35079023", + "35079024", + "35079025", + "35079026", + "35079029", + "35079031", + "35079032", + "35079039", + "35079041", + "35079042", + "35079049", + "36010000", + "36020000", + "36031000", + "36032000", + "36033000", + "36034000", + "36035000", + "36036000", + "36041000", + "36049010", + "36049090", + "36050000", + "36061000", + "36069000", + "37011010", + "37011021", + "37011029", + "37012010", + "37012020", + "37013010", + "37013021", + "37013022", + "37013029", + "37013031", + "37013039", + "37013040", + "37013050", + "37013090", + "37019100", + "37019900", + "37021010", + "37021020", + "37023100", + "37023200", + "37023900", + "37024100", + "37024210", + "37024290", + "37024310", + "37024320", + "37024390", + "37024410", + "37024421", + "37024422", + "37024429", + "37025200", + "37025300", + "37025411", + "37025412", + "37025419", + "37025491", + "37025499", + "37025510", + "37025590", + "37025600", + "37029600", + "37029700", + "37029800", + "37031010", + "37031021", + "37031029", + "37032000", + "37039010", + "37039090", + "37040000", + "37050010", + "37050090", + "37061000", + "37069000", + "37071000", + "37079010", + "37079021", + "37079029", + "37079030", + "37079090", + "38011000", + "38012010", + "38012090", + "38013010", + "38013090", + "38019000", + "38021000", + "38029010", + "38029020", + "38029030", + "38029040", + "38029050", + "38029090", + "38030010", + "38030090", + "38040011", + "38040012", + "38040020", + "38051000", + "38059010", + "38059090", + "38061000", + "38062000", + "38063000", + "38069011", + "38069012", + "38069019", + "38069090", + "38070000", + "38085200", + "38085910", + "38085921", + "38085922", + "38085923", + "38085924", + "38085925", + "38085926", + "38085929", + "38086100", + "38086210", + "38086290", + "38086910", + "38086990", + "38089111", + "38089119", + "38089120", + "38089191", + "38089192", + "38089193", + "38089194", + "38089195", + "38089196", + "38089197", + "38089199", + "38089211", + "38089219", + "38089220", + "38089291", + "38089292", + "38089293", + "38089294", + "38089295", + "38089296", + "38089297", + "38089299", + "38089311", + "38089319", + "38089321", + "38089322", + "38089323", + "38089324", + "38089325", + "38089326", + "38089327", + "38089328", + "38089329", + "38089331", + "38089332", + "38089333", + "38089341", + "38089349", + "38089351", + "38089352", + "38089359", + "38089411", + "38089419", + "38089421", + "38089422", + "38089429", + "38089911", + "38089919", + "38089920", + "38089991", + "38089992", + "38089993", + "38089994", + "38089995", + "38089996", + "38089999", + "38091010", + "38091090", + "38099110", + "38099120", + "38099130", + "38099141", + "38099149", + "38099190", + "38099211", + "38099219", + "38099290", + "38099311", + "38099319", + "38099390", + "38101010", + "38101020", + "38109000", + "38111100", + "38111900", + "38112110", + "38112120", + "38112130", + "38112140", + "38112150", + "38112190", + "38112910", + "38112920", + "38112990", + "38119010", + "38119090", + "38121000", + "38122000", + "38123100", + "38123911", + "38123912", + "38123919", + "38123921", + "38123929", + "38130010", + "38130020", + "38130030", + "38130040", + "38130090", + "38140010", + "38140020", + "38140030", + "38140090", + "38151100", + "38151210", + "38151220", + "38151290", + "38151900", + "38159010", + "38159091", + "38159092", + "38159093", + "38159099", + "38160011", + "38160012", + "38160019", + "38160021", + "38160029", + "38160090", + "38170010", + "38170020", + "38180010", + "38180090", + "38190000", + "38200000", + "38210000", + "38221100", + "38221200", + "38221300", + "38221910", + "38221920", + "38221930", + "38221940", + "38221990", + "38229000", + "38231100", + "38231200", + "38231300", + "38231910", + "38231990", + "38237010", + "38237020", + "38237040", + "38237090", + "38241000", + "38243000", + "38244000", + "38245000", + "38246000", + "38248110", + "38248190", + "38248210", + "38248290", + "38248300", + "38248400", + "38248500", + "38248600", + "38248700", + "38248810", + "38248820", + "38248900", + "38249100", + "38249200", + "38249911", + "38249912", + "38249913", + "38249914", + "38249915", + "38249919", + "38249921", + "38249922", + "38249923", + "38249924", + "38249925", + "38249929", + "38249931", + "38249932", + "38249933", + "38249934", + "38249935", + "38249936", + "38249939", + "38249941", + "38249942", + "38249943", + "38249949", + "38249951", + "38249952", + "38249953", + "38249954", + "38249959", + "38249961", + "38249962", + "38249969", + "38249971", + "38249972", + "38249973", + "38249974", + "38249975", + "38249976", + "38249977", + "38249978", + "38249979", + "38249981", + "38249982", + "38249983", + "38249984", + "38249985", + "38249986", + "38249987", + "38249988", + "38249989", + "38251000", + "38252000", + "38253000", + "38254100", + "38254900", + "38255000", + "38256100", + "38256900", + "38259000", + "38260000", + "38271110", + "38271190", + "38271200", + "38271300", + "38271400", + "38272000", + "38273110", + "38273190", + "38273210", + "38273290", + "38273900", + "38274000", + "38275100", + "38275900", + "38276100", + "38276200", + "38276300", + "38276400", + "38276500", + "38276800", + "38276900", + "38279000", + "39011020", + "39011030", + "39012011", + "39012019", + "39012021", + "39012029", + "39013010", + "39013090", + "39014000", + "39019010", + "39019020", + "39019030", + "39019040", + "39019050", + "39019090", + "39021010", + "39021020", + "39022000", + "39023000", + "39029000", + "39031110", + "39031120", + "39031900", + "39032000", + "39033010", + "39033020", + "39039010", + "39039020", + "39039090", + "39041010", + "39041020", + "39041090", + "39042100", + "39042200", + "39043000", + "39044010", + "39044090", + "39045010", + "39045090", + "39046110", + "39046190", + "39046910", + "39046990", + "39049010", + "39049090", + "39051200", + "39051910", + "39051990", + "39052100", + "39052900", + "39053000", + "39059130", + "39059190", + "39059910", + "39059920", + "39059930", + "39059990", + "39061000", + "39069011", + "39069012", + "39069019", + "39069021", + "39069022", + "39069029", + "39069031", + "39069032", + "39069039", + "39069051", + "39069052", + "39069053", + "39069054", + "39069059", + "39069061", + "39069062", + "39069063", + "39069064", + "39069065", + "39069069", + "39071010", + "39071020", + "39071031", + "39071039", + "39071041", + "39071042", + "39071049", + "39071091", + "39071099", + "39072100", + "39072911", + "39072912", + "39072920", + "39072931", + "39072939", + "39072941", + "39072942", + "39072949", + "39072991", + "39072992", + "39072999", + "39073011", + "39073019", + "39073021", + "39073022", + "39073029", + "39074010", + "39074020", + "39074090", + "39075010", + "39075090", + "39076100", + "39076900", + "39077000", + "39079100", + "39079911", + "39079912", + "39079919", + "39079991", + "39079992", + "39079993", + "39079994", + "39079995", + "39079999", + "39081011", + "39081012", + "39081013", + "39081014", + "39081019", + "39081021", + "39081022", + "39081023", + "39081025", + "39081026", + "39081029", + "39089010", + "39089020", + "39089090", + "39091000", + "39092011", + "39092019", + "39092021", + "39092029", + "39093100", + "39093900", + "39094011", + "39094019", + "39094091", + "39094099", + "39095011", + "39095012", + "39095019", + "39095021", + "39095029", + "39100011", + "39100012", + "39100013", + "39100019", + "39100021", + "39100029", + "39100030", + "39100090", + "39111010", + "39111021", + "39111029", + "39112000", + "39119011", + "39119012", + "39119013", + "39119014", + "39119019", + "39119021", + "39119022", + "39119023", + "39119024", + "39119025", + "39119026", + "39119027", + "39119029", + "39121110", + "39121120", + "39121200", + "39122010", + "39122021", + "39122029", + "39123111", + "39123119", + "39123121", + "39123129", + "39123910", + "39123920", + "39123930", + "39123990", + "39129010", + "39129020", + "39129031", + "39129039", + "39129040", + "39129090", + "39131000", + "39139011", + "39139012", + "39139019", + "39139020", + "39139030", + "39139040", + "39139060", + "39139090", + "39140011", + "39140019", + "39140090", + "39151000", + "39152000", + "39153000", + "39159000", + "39161000", + "39162000", + "39169010", + "39169090", + "39171010", + "39171021", + "39171029", + "39172100", + "39172210", + "39172290", + "39172300", + "39172900", + "39173100", + "39173210", + "39173221", + "39173229", + "39173230", + "39173240", + "39173251", + "39173259", + "39173290", + "39173300", + "39173900", + "39174010", + "39174090", + "39181000", + "39189000", + "39191010", + "39191020", + "39191090", + "39199010", + "39199020", + "39199090", + "39201010", + "39201091", + "39201099", + "39202011", + "39202012", + "39202019", + "39202090", + "39203000", + "39204310", + "39204390", + "39204900", + "39205100", + "39205900", + "39206100", + "39206211", + "39206219", + "39206291", + "39206299", + "39206300", + "39206900", + "39207100", + "39207310", + "39207390", + "39207910", + "39207990", + "39209100", + "39209200", + "39209300", + "39209400", + "39209910", + "39209920", + "39209930", + "39209940", + "39209950", + "39209990", + "39211100", + "39211200", + "39211310", + "39211390", + "39211400", + "39211900", + "39219011", + "39219012", + "39219013", + "39219019", + "39219020", + "39219090", + "39221000", + "39222000", + "39229000", + "39231010", + "39231090", + "39232110", + "39232190", + "39232910", + "39232990", + "39233010", + "39233090", + "39234000", + "39235000", + "39239010", + "39239090", + "39241000", + "39249000", + "39251000", + "39252000", + "39253000", + "39259010", + "39259090", + "39261000", + "39262000", + "39263000", + "39264000", + "39269010", + "39269021", + "39269022", + "39269030", + "39269040", + "39269050", + "39269061", + "39269069", + "39269090", + "40011000", + "40012100", + "40012200", + "40012910", + "40012920", + "40012990", + "40013000", + "40021110", + "40021120", + "40021911", + "40021912", + "40021919", + "40021920", + "40022010", + "40022091", + "40022099", + "40023100", + "40023900", + "40024100", + "40024900", + "40025100", + "40025900", + "40026000", + "40027000", + "40028000", + "40029100", + "40029910", + "40029920", + "40029930", + "40029990", + "40030000", + "40040000", + "40051010", + "40051090", + "40052000", + "40059110", + "40059190", + "40059910", + "40059990", + "40061000", + "40069000", + "40070011", + "40070019", + "40070020", + "40081100", + "40081900", + "40082100", + "40082900", + "40091100", + "40091210", + "40091290", + "40092110", + "40092190", + "40092210", + "40092290", + "40093100", + "40093210", + "40093290", + "40094100", + "40094210", + "40094290", + "40101100", + "40101200", + "40101900", + "40103100", + "40103200", + "40103300", + "40103400", + "40103500", + "40103600", + "40103900", + "40111000", + "40112010", + "40112090", + "40113000", + "40114000", + "40115000", + "40117010", + "40117090", + "40118010", + "40118020", + "40118090", + "40119010", + "40119090", + "40121100", + "40121200", + "40121300", + "40121900", + "40122000", + "40129010", + "40129090", + "40131010", + "40131090", + "40132000", + "40139000", + "40141000", + "40149010", + "40149090", + "40151200", + "40151900", + "40159000", + "40161010", + "40161090", + "40169100", + "40169200", + "40169300", + "40169400", + "40169510", + "40169590", + "40169910", + "40169990", + "40170000", + "41012000", + "41015010", + "41015020", + "41015030", + "41019010", + "41019020", + "41019030", + "41021000", + "41022100", + "41022900", + "41032000", + "41033000", + "41039000", + "41041111", + "41041112", + "41041113", + "41041114", + "41041119", + "41041121", + "41041122", + "41041123", + "41041124", + "41041129", + "41041910", + "41041920", + "41041930", + "41041940", + "41041990", + "41044110", + "41044120", + "41044130", + "41044190", + "41044910", + "41044920", + "41044990", + "41051010", + "41051021", + "41051029", + "41051090", + "41053000", + "41062110", + "41062121", + "41062129", + "41062190", + "41062200", + "41063110", + "41063190", + "41063200", + "41064000", + "41069100", + "41069200", + "41071110", + "41071120", + "41071190", + "41071210", + "41071220", + "41071290", + "41071910", + "41071920", + "41071990", + "41079110", + "41079190", + "41079210", + "41079290", + "41079910", + "41079990", + "41120000", + "41131010", + "41131090", + "41132000", + "41133000", + "41139000", + "41141000", + "41142010", + "41142020", + "41151000", + "41152000", + "42010010", + "42010090", + "42021100", + "42021210", + "42021220", + "42021900", + "42022100", + "42022210", + "42022220", + "42022900", + "42023100", + "42023200", + "42023900", + "42029100", + "42029200", + "42029900", + "42031000", + "42032100", + "42032900", + "42033000", + "42034000", + "42050000", + "42060000", + "43011000", + "43013000", + "43016000", + "43018000", + "43019000", + "43021100", + "43021910", + "43021990", + "43022000", + "43023000", + "43031000", + "43039000", + "43040000", + "44011100", + "44011200", + "44012100", + "44012200", + "44013100", + "44013200", + "44013900", + "44014100", + "44014900", + "44021000", + "44022000", + "44029000", + "44031100", + "44031200", + "44032100", + "44032200", + "44032300", + "44032400", + "44032500", + "44032600", + "44034100", + "44034200", + "44034900", + "44039100", + "44039300", + "44039400", + "44039500", + "44039600", + "44039700", + "44039800", + "44039900", + "44041000", + "44042000", + "44050000", + "44061100", + "44061200", + "44069100", + "44069200", + "44071100", + "44071200", + "44071300", + "44071400", + "44071900", + "44072100", + "44072200", + "44072300", + "44072500", + "44072600", + "44072700", + "44072800", + "44072910", + "44072920", + "44072930", + "44072940", + "44072950", + "44072960", + "44072970", + "44072990", + "44079100", + "44079200", + "44079300", + "44079400", + "44079500", + "44079600", + "44079700", + "44079920", + "44079930", + "44079960", + "44079970", + "44079990", + "44081010", + "44081091", + "44081099", + "44083110", + "44083190", + "44083910", + "44083991", + "44083992", + "44083999", + "44089010", + "44089090", + "44091000", + "44092100", + "44092200", + "44092900", + "44101110", + "44101121", + "44101129", + "44101190", + "44101210", + "44101290", + "44101911", + "44101919", + "44101991", + "44101992", + "44101999", + "44109000", + "44111210", + "44111290", + "44111310", + "44111391", + "44111399", + "44111410", + "44111490", + "44119210", + "44119290", + "44119310", + "44119390", + "44119410", + "44119490", + "44121000", + "44123100", + "44123300", + "44123400", + "44123900", + "44124100", + "44124200", + "44124900", + "44125100", + "44125200", + "44125900", + "44129100", + "44129200", + "44129900", + "44130000", + "44141000", + "44149000", + "44151000", + "44152000", + "44160010", + "44160090", + "44170010", + "44170020", + "44170090", + "44181100", + "44181900", + "44182100", + "44182900", + "44183000", + "44184000", + "44185000", + "44187300", + "44187400", + "44187500", + "44187900", + "44188100", + "44188200", + "44188300", + "44188900", + "44189100", + "44189200", + "44189900", + "44191100", + "44191200", + "44191900", + "44192000", + "44199000", + "44201100", + "44201900", + "44209000", + "44211000", + "44212000", + "44219100", + "44219900", + "45011000", + "45019000", + "45020000", + "45031000", + "45039000", + "45041000", + "45049000", + "46012100", + "46012200", + "46012900", + "46019200", + "46019300", + "46019400", + "46019900", + "46021100", + "46021200", + "46021900", + "46029000", + "47010000", + "47020000", + "47031100", + "47031900", + "47032110", + "47032190", + "47032900", + "47041100", + "47041900", + "47042100", + "47042900", + "47050000", + "47061000", + "47062000", + "47063000", + "47069100", + "47069200", + "47069300", + "47071000", + "47072000", + "47073000", + "47079000", + "48010020", + "48010030", + "48010090", + "48021000", + "48022010", + "48022090", + "48024010", + "48024090", + "48025410", + "48025491", + "48025499", + "48025510", + "48025591", + "48025592", + "48025599", + "48025610", + "48025691", + "48025692", + "48025693", + "48025699", + "48025710", + "48025791", + "48025792", + "48025793", + "48025799", + "48025810", + "48025891", + "48025892", + "48025899", + "48026110", + "48026191", + "48026192", + "48026199", + "48026210", + "48026291", + "48026292", + "48026299", + "48026910", + "48026991", + "48026992", + "48026999", + "48030010", + "48030090", + "48041100", + "48041900", + "48042100", + "48042900", + "48043110", + "48043190", + "48043910", + "48043990", + "48044100", + "48044200", + "48044900", + "48045100", + "48045200", + "48045910", + "48045990", + "48051100", + "48051200", + "48051900", + "48052400", + "48052500", + "48053000", + "48054010", + "48054090", + "48055000", + "48059100", + "48059210", + "48059290", + "48059300", + "48061000", + "48062000", + "48063000", + "48064000", + "48070000", + "48081000", + "48084000", + "48089000", + "48092000", + "48099000", + "48101310", + "48101381", + "48101382", + "48101389", + "48101391", + "48101399", + "48101410", + "48101481", + "48101482", + "48101489", + "48101490", + "48101910", + "48101981", + "48101982", + "48101989", + "48101991", + "48101999", + "48102210", + "48102290", + "48102910", + "48102990", + "48103110", + "48103190", + "48103210", + "48103290", + "48103910", + "48103990", + "48109210", + "48109290", + "48109910", + "48109990", + "48111010", + "48111090", + "48114110", + "48114190", + "48114910", + "48114990", + "48115110", + "48115121", + "48115122", + "48115123", + "48115128", + "48115129", + "48115130", + "48115910", + "48115921", + "48115922", + "48115923", + "48115929", + "48115930", + "48116010", + "48116090", + "48119011", + "48119019", + "48119020", + "48119090", + "48120000", + "48131000", + "48132000", + "48139000", + "48142000", + "48149000", + "48162000", + "48169010", + "48169090", + "48171000", + "48172000", + "48173000", + "48181000", + "48182000", + "48183000", + "48185000", + "48189010", + "48189090", + "48191000", + "48192000", + "48193000", + "48194000", + "48195000", + "48196000", + "48201000", + "48202000", + "48203000", + "48204000", + "48205000", + "48209000", + "48211000", + "48219000", + "48221000", + "48229000", + "48232010", + "48232091", + "48232099", + "48234000", + "48236100", + "48236900", + "48237000", + "48239010", + "48239020", + "48239091", + "48239099", + "49011000", + "49019100", + "49019900", + "49021000", + "49029000", + "49030000", + "49040000", + "49052000", + "49059000", + "49060000", + "49070010", + "49070020", + "49070030", + "49070090", + "49081000", + "49089000", + "49090000", + "49100000", + "49111010", + "49111090", + "49119100", + "49119900", + "50010000", + "50020000", + "50030010", + "50030090", + "50040000", + "50050000", + "50060000", + "50071010", + "50071090", + "50072010", + "50072090", + "50079000", + "51011110", + "51011190", + "51011900", + "51012100", + "51012900", + "51013000", + "51021100", + "51021900", + "51022000", + "51031000", + "51032000", + "51033000", + "51040000", + "51051000", + "51052100", + "51052910", + "51052991", + "51052999", + "51053100", + "51053900", + "51054000", + "51061000", + "51062000", + "51071011", + "51071019", + "51071090", + "51072000", + "51081000", + "51082000", + "51091000", + "51099000", + "51100000", + "51111110", + "51111120", + "51111900", + "51112000", + "51113010", + "51113090", + "51119000", + "51121100", + "51121910", + "51121920", + "51122010", + "51122020", + "51123010", + "51123020", + "51129000", + "51130011", + "51130012", + "51130013", + "51130020", + "52010010", + "52010020", + "52010090", + "52021000", + "52029100", + "52029900", + "52030000", + "52041111", + "52041112", + "52041120", + "52041131", + "52041132", + "52041140", + "52041911", + "52041912", + "52041920", + "52041931", + "52041932", + "52041940", + "52042000", + "52051100", + "52051200", + "52051310", + "52051390", + "52051400", + "52051500", + "52052100", + "52052200", + "52052310", + "52052390", + "52052400", + "52052600", + "52052700", + "52052800", + "52053100", + "52053200", + "52053300", + "52053400", + "52053500", + "52054100", + "52054200", + "52054300", + "52054400", + "52054600", + "52054700", + "52054800", + "52061100", + "52061200", + "52061300", + "52061400", + "52061500", + "52062100", + "52062200", + "52062300", + "52062400", + "52062500", + "52063100", + "52063200", + "52063300", + "52063400", + "52063500", + "52064100", + "52064200", + "52064300", + "52064400", + "52064500", + "52071000", + "52079000", + "52081100", + "52081200", + "52081300", + "52081900", + "52082100", + "52082200", + "52082300", + "52082900", + "52083100", + "52083200", + "52083300", + "52083900", + "52084100", + "52084200", + "52084300", + "52084900", + "52085100", + "52085200", + "52085910", + "52085990", + "52091100", + "52091200", + "52091900", + "52092100", + "52092200", + "52092900", + "52093100", + "52093200", + "52093900", + "52094100", + "52094210", + "52094290", + "52094300", + "52094900", + "52095100", + "52095200", + "52095900", + "52101100", + "52101910", + "52101990", + "52102100", + "52102910", + "52102990", + "52103100", + "52103200", + "52103900", + "52104100", + "52104910", + "52104990", + "52105100", + "52105910", + "52105990", + "52111100", + "52111200", + "52111900", + "52112010", + "52112020", + "52112090", + "52113100", + "52113200", + "52113900", + "52114100", + "52114210", + "52114290", + "52114300", + "52114900", + "52115100", + "52115200", + "52115900", + "52121100", + "52121200", + "52121300", + "52121400", + "52121500", + "52122100", + "52122200", + "52122300", + "52122400", + "52122500", + "53011000", + "53012110", + "53012120", + "53012910", + "53012990", + "53013000", + "53021000", + "53029000", + "53031010", + "53031090", + "53039010", + "53039090", + "53050010", + "53050090", + "53061000", + "53062000", + "53071010", + "53071090", + "53072010", + "53072090", + "53081000", + "53082000", + "53089000", + "53091100", + "53091900", + "53092100", + "53092900", + "53101010", + "53101090", + "53109000", + "53110000", + "54011011", + "54011012", + "54011090", + "54012011", + "54012012", + "54012090", + "54021100", + "54021910", + "54021990", + "54022010", + "54022090", + "54023111", + "54023119", + "54023190", + "54023211", + "54023219", + "54023290", + "54023310", + "54023320", + "54023390", + "54023400", + "54023900", + "54024400", + "54024510", + "54024520", + "54024590", + "54024600", + "54024710", + "54024720", + "54024790", + "54024800", + "54024910", + "54024990", + "54025110", + "54025190", + "54025200", + "54025300", + "54025900", + "54026110", + "54026190", + "54026200", + "54026300", + "54026900", + "54031000", + "54033110", + "54033190", + "54033200", + "54033300", + "54033900", + "54034100", + "54034200", + "54034900", + "54041100", + "54041200", + "54041911", + "54041919", + "54041990", + "54049000", + "54050000", + "54060010", + "54060020", + "54071011", + "54071019", + "54071021", + "54071029", + "54072000", + "54073000", + "54074100", + "54074200", + "54074300", + "54074400", + "54075100", + "54075210", + "54075220", + "54075300", + "54075400", + "54076100", + "54076900", + "54077100", + "54077200", + "54077300", + "54077400", + "54078100", + "54078200", + "54078300", + "54078400", + "54079100", + "54079200", + "54079300", + "54079400", + "54081000", + "54082100", + "54082200", + "54082300", + "54082400", + "54083100", + "54083200", + "54083300", + "54083400", + "55011100", + "55011900", + "55012000", + "55013000", + "55014000", + "55019000", + "55021000", + "55029010", + "55029090", + "55031100", + "55031910", + "55031990", + "55032010", + "55032090", + "55033000", + "55034000", + "55039010", + "55039020", + "55039090", + "55041000", + "55049010", + "55049090", + "55051000", + "55052000", + "55061000", + "55062000", + "55063000", + "55064000", + "55069000", + "55070000", + "55081000", + "55082000", + "55091100", + "55091210", + "55091290", + "55092100", + "55092200", + "55093100", + "55093200", + "55094100", + "55094200", + "55095100", + "55095200", + "55095300", + "55095900", + "55096100", + "55096200", + "55096900", + "55099100", + "55099200", + "55099900", + "55101111", + "55101112", + "55101113", + "55101119", + "55101190", + "55101211", + "55101212", + "55101213", + "55101219", + "55101290", + "55102011", + "55102012", + "55102013", + "55102019", + "55102090", + "55103011", + "55103012", + "55103013", + "55103019", + "55103090", + "55109011", + "55109012", + "55109013", + "55109019", + "55109090", + "55111000", + "55112000", + "55113000", + "55121100", + "55121900", + "55122100", + "55122900", + "55129110", + "55129190", + "55129910", + "55129990", + "55131100", + "55131200", + "55131300", + "55131900", + "55132100", + "55132310", + "55132390", + "55132900", + "55133100", + "55133911", + "55133919", + "55133990", + "55134100", + "55134911", + "55134919", + "55134990", + "55141100", + "55141200", + "55141910", + "55141990", + "55142100", + "55142200", + "55142300", + "55142900", + "55143011", + "55143012", + "55143019", + "55143090", + "55144100", + "55144200", + "55144300", + "55144900", + "55151100", + "55151200", + "55151300", + "55151900", + "55152100", + "55152200", + "55152900", + "55159100", + "55159910", + "55159990", + "55161100", + "55161200", + "55161300", + "55161400", + "55162100", + "55162200", + "55162300", + "55162400", + "55163100", + "55163200", + "55163300", + "55163400", + "55164100", + "55164200", + "55164300", + "55164400", + "55169100", + "55169200", + "55169300", + "55169400", + "56012110", + "56012190", + "56012211", + "56012219", + "56012291", + "56012299", + "56012900", + "56013010", + "56013090", + "56021000", + "56022100", + "56022900", + "56029000", + "56031110", + "56031120", + "56031130", + "56031140", + "56031190", + "56031210", + "56031220", + "56031230", + "56031240", + "56031250", + "56031290", + "56031310", + "56031320", + "56031330", + "56031340", + "56031350", + "56031390", + "56031410", + "56031420", + "56031430", + "56031440", + "56031490", + "56039110", + "56039120", + "56039130", + "56039190", + "56039210", + "56039220", + "56039230", + "56039240", + "56039290", + "56039310", + "56039320", + "56039330", + "56039340", + "56039390", + "56039410", + "56039420", + "56039430", + "56039490", + "56041000", + "56049010", + "56049021", + "56049022", + "56049090", + "56050010", + "56050020", + "56050090", + "56060000", + "56072100", + "56072900", + "56074100", + "56074900", + "56075011", + "56075019", + "56075090", + "56079010", + "56079020", + "56079090", + "56081100", + "56081900", + "56089000", + "56090010", + "56090090", + "57011011", + "57011012", + "57011020", + "57019000", + "57021000", + "57022000", + "57023100", + "57023200", + "57023900", + "57024100", + "57024200", + "57024900", + "57025010", + "57025020", + "57025090", + "57029100", + "57029200", + "57029900", + "57031000", + "57032100", + "57032900", + "57033100", + "57033900", + "57039000", + "57041000", + "57042000", + "57049000", + "57050000", + "58011000", + "58012100", + "58012200", + "58012300", + "58012600", + "58012700", + "58013100", + "58013200", + "58013300", + "58013600", + "58013700", + "58019000", + "58021000", + "58022000", + "58023000", + "58030010", + "58030090", + "58041010", + "58041090", + "58042100", + "58042910", + "58042990", + "58043010", + "58043090", + "58050010", + "58050020", + "58050090", + "58061000", + "58062000", + "58063100", + "58063200", + "58063900", + "58064000", + "58071000", + "58079000", + "58081000", + "58089000", + "58090000", + "58101000", + "58109100", + "58109200", + "58109900", + "58110000", + "59011000", + "59019000", + "59021010", + "59021090", + "59022000", + "59029000", + "59031000", + "59032000", + "59039010", + "59039090", + "59041000", + "59049000", + "59050000", + "59061000", + "59069100", + "59069900", + "59070000", + "59080000", + "59090000", + "59100000", + "59111000", + "59112010", + "59112090", + "59113100", + "59113200", + "59114000", + "59119000", + "60011010", + "60011020", + "60011090", + "60012100", + "60012200", + "60012900", + "60019100", + "60019200", + "60019900", + "60024010", + "60024020", + "60024090", + "60029010", + "60029020", + "60029090", + "60031000", + "60032000", + "60033000", + "60034000", + "60039000", + "60041011", + "60041012", + "60041013", + "60041014", + "60041031", + "60041032", + "60041033", + "60041034", + "60041041", + "60041042", + "60041043", + "60041044", + "60041091", + "60041092", + "60041093", + "60041094", + "60049010", + "60049030", + "60049040", + "60049090", + "60052100", + "60052200", + "60052300", + "60052400", + "60053500", + "60053600", + "60053700", + "60053800", + "60053900", + "60054100", + "60054200", + "60054300", + "60054400", + "60059010", + "60059090", + "60061000", + "60062100", + "60062200", + "60062300", + "60062400", + "60063110", + "60063120", + "60063130", + "60063190", + "60063210", + "60063220", + "60063230", + "60063290", + "60063310", + "60063320", + "60063330", + "60063390", + "60063410", + "60063420", + "60063430", + "60063490", + "60064100", + "60064200", + "60064300", + "60064400", + "60069000", + "61012000", + "61013000", + "61019010", + "61019090", + "61021000", + "61022000", + "61023000", + "61029000", + "61031010", + "61031020", + "61031090", + "61032200", + "61032300", + "61032910", + "61032990", + "61033100", + "61033200", + "61033300", + "61033900", + "61034100", + "61034200", + "61034300", + "61034900", + "61041300", + "61041910", + "61041920", + "61041990", + "61042200", + "61042300", + "61042910", + "61042990", + "61043100", + "61043200", + "61043300", + "61043900", + "61044100", + "61044200", + "61044300", + "61044400", + "61044900", + "61045100", + "61045200", + "61045300", + "61045900", + "61046100", + "61046200", + "61046300", + "61046900", + "61051000", + "61052000", + "61059000", + "61061000", + "61062000", + "61069000", + "61071100", + "61071200", + "61071900", + "61072100", + "61072200", + "61072900", + "61079100", + "61079910", + "61079990", + "61081100", + "61081900", + "61082100", + "61082200", + "61082900", + "61083100", + "61083200", + "61083900", + "61089100", + "61089200", + "61089900", + "61091000", + "61099000", + "61101100", + "61101200", + "61101900", + "61102000", + "61103000", + "61109000", + "61112000", + "61113000", + "61119010", + "61119090", + "61121100", + "61121200", + "61121900", + "61122000", + "61123100", + "61123900", + "61124100", + "61124900", + "61130000", + "61142000", + "61143000", + "61149010", + "61149090", + "61151011", + "61151012", + "61151013", + "61151014", + "61151019", + "61151021", + "61151022", + "61151029", + "61151091", + "61151092", + "61151093", + "61151099", + "61152100", + "61152200", + "61152910", + "61152920", + "61152990", + "61153010", + "61153020", + "61153090", + "61159400", + "61159500", + "61159600", + "61159900", + "61161000", + "61169100", + "61169200", + "61169300", + "61169900", + "61171000", + "61178010", + "61178090", + "61179000", + "62012000", + "62013000", + "62014000", + "62019000", + "62022000", + "62023000", + "62024000", + "62029000", + "62031100", + "62031200", + "62031900", + "62032200", + "62032300", + "62032910", + "62032990", + "62033100", + "62033200", + "62033300", + "62033900", + "62034100", + "62034200", + "62034300", + "62034900", + "62041100", + "62041200", + "62041300", + "62041900", + "62042100", + "62042200", + "62042300", + "62042900", + "62043100", + "62043200", + "62043300", + "62043900", + "62044100", + "62044200", + "62044300", + "62044400", + "62044900", + "62045100", + "62045200", + "62045300", + "62045900", + "62046100", + "62046200", + "62046300", + "62046900", + "62052000", + "62053000", + "62059010", + "62059090", + "62061000", + "62062000", + "62063000", + "62064000", + "62069000", + "62071100", + "62071900", + "62072100", + "62072200", + "62072900", + "62079100", + "62079910", + "62079990", + "62081100", + "62081900", + "62082100", + "62082200", + "62082900", + "62089100", + "62089200", + "62089900", + "62092000", + "62093000", + "62099010", + "62099090", + "62101000", + "62102000", + "62103000", + "62104000", + "62105000", + "62111100", + "62111200", + "62112000", + "62113200", + "62113300", + "62113910", + "62113990", + "62114200", + "62114300", + "62114900", + "62121000", + "62122000", + "62123000", + "62129000", + "62132000", + "62139010", + "62139090", + "62141000", + "62142000", + "62143000", + "62144000", + "62149010", + "62149090", + "62151000", + "62152000", + "62159000", + "62160000", + "62171000", + "62179000", + "63011000", + "63012000", + "63013000", + "63014000", + "63019000", + "63021000", + "63022100", + "63022200", + "63022900", + "63023100", + "63023200", + "63023900", + "63024000", + "63025100", + "63025300", + "63025910", + "63025990", + "63026000", + "63029100", + "63029300", + "63029910", + "63029990", + "63031200", + "63031910", + "63031990", + "63039100", + "63039200", + "63039900", + "63041100", + "63041910", + "63041990", + "63042000", + "63049100", + "63049200", + "63049300", + "63049900", + "63051000", + "63052000", + "63053200", + "63053310", + "63053390", + "63053900", + "63059000", + "63061200", + "63061910", + "63061990", + "63062200", + "63062910", + "63062990", + "63063010", + "63063090", + "63064010", + "63064090", + "63069000", + "63071000", + "63072000", + "63079010", + "63079020", + "63079090", + "63080000", + "63090010", + "63090090", + "63101000", + "63109000", + "64011000", + "64019200", + "64019910", + "64019990", + "64021200", + "64021900", + "64022000", + "64029110", + "64029190", + "64029910", + "64029990", + "64031200", + "64031900", + "64032000", + "64034000", + "64035110", + "64035190", + "64035910", + "64035990", + "64039110", + "64039190", + "64039910", + "64039990", + "64041100", + "64041900", + "64042000", + "64051010", + "64051020", + "64051090", + "64052000", + "64059000", + "64061000", + "64062000", + "64069010", + "64069020", + "64069090", + "65010000", + "65020010", + "65020090", + "65040010", + "65040090", + "65050011", + "65050012", + "65050019", + "65050021", + "65050022", + "65050029", + "65050031", + "65050032", + "65050039", + "65050090", + "65061010", + "65061090", + "65069100", + "65069900", + "65070000", + "66011000", + "66019110", + "66019190", + "66019900", + "66020000", + "66032000", + "66039000", + "67010000", + "67021000", + "67029000", + "67030000", + "67041100", + "67041900", + "67042000", + "67049000", + "68010000", + "68021000", + "68022100", + "68022300", + "68022900", + "68029100", + "68029200", + "68029310", + "68029390", + "68029910", + "68029990", + "68030000", + "68041000", + "68042111", + "68042119", + "68042190", + "68042211", + "68042219", + "68042290", + "68042300", + "68043000", + "68051000", + "68052000", + "68053010", + "68053020", + "68053090", + "68061000", + "68062000", + "68069010", + "68069090", + "68071000", + "68079000", + "68080000", + "68091100", + "68091900", + "68099000", + "68101100", + "68101900", + "68109100", + "68109900", + "68114000", + "68118100", + "68118200", + "68118900", + "68128000", + "68129100", + "68129910", + "68129920", + "68129930", + "68129990", + "68132000", + "68138110", + "68138190", + "68138910", + "68138990", + "68141000", + "68149000", + "68151100", + "68151200", + "68151300", + "68151900", + "68152000", + "68159110", + "68159190", + "68159911", + "68159912", + "68159913", + "68159914", + "68159919", + "68159990", + "69010000", + "69021011", + "69021018", + "69021019", + "69021090", + "69022010", + "69022091", + "69022092", + "69022093", + "69022099", + "69029010", + "69029020", + "69029030", + "69029040", + "69029090", + "69031011", + "69031012", + "69031019", + "69031020", + "69031030", + "69031040", + "69031090", + "69032010", + "69032020", + "69032030", + "69032090", + "69039011", + "69039012", + "69039019", + "69039091", + "69039092", + "69039099", + "69041000", + "69049000", + "69051000", + "69059000", + "69060000", + "69072100", + "69072200", + "69072300", + "69073000", + "69074000", + "69091100", + "69091210", + "69091220", + "69091230", + "69091290", + "69091910", + "69091920", + "69091930", + "69091990", + "69099000", + "69101000", + "69109000", + "69111010", + "69111090", + "69119000", + "69120000", + "69131000", + "69139000", + "69141000", + "69149000", + "70010000", + "70021000", + "70022000", + "70023100", + "70023200", + "70023900", + "70031200", + "70031900", + "70032000", + "70033000", + "70042000", + "70049000", + "70051000", + "70052100", + "70052900", + "70053000", + "70060000", + "70071100", + "70071900", + "70072100", + "70072900", + "70080000", + "70091000", + "70099100", + "70099200", + "70101000", + "70102000", + "70109011", + "70109012", + "70109021", + "70109022", + "70109090", + "70111010", + "70111021", + "70111029", + "70111090", + "70112000", + "70119000", + "70131000", + "70132200", + "70132800", + "70133300", + "70133700", + "70134100", + "70134210", + "70134290", + "70134900", + "70139110", + "70139190", + "70139900", + "70140000", + "70151010", + "70151091", + "70151092", + "70159010", + "70159020", + "70159030", + "70159090", + "70161000", + "70169000", + "70171000", + "70172000", + "70179000", + "70181010", + "70181020", + "70181090", + "70182000", + "70189000", + "70191100", + "70191210", + "70191290", + "70191300", + "70191400", + "70191500", + "70191900", + "70196100", + "70196200", + "70196300", + "70196400", + "70196500", + "70196600", + "70196900", + "70197100", + "70197200", + "70197310", + "70197390", + "70198000", + "70199000", + "70200010", + "70200090", + "71011000", + "71012100", + "71012200", + "71021000", + "71022100", + "71022900", + "71023100", + "71023900", + "71031000", + "71039100", + "71039900", + "71041000", + "71042100", + "71042900", + "71049100", + "71049900", + "71051000", + "71059000", + "71061000", + "71069100", + "71069210", + "71069220", + "71069290", + "71070000", + "71081100", + "71081210", + "71081290", + "71081310", + "71081390", + "71082000", + "71090000", + "71101100", + "71101910", + "71101990", + "71102100", + "71102900", + "71103100", + "71103900", + "71104100", + "71104900", + "71110000", + "71123010", + "71123020", + "71123090", + "71129100", + "71129200", + "71129900", + "71131100", + "71131900", + "71132000", + "71141100", + "71141900", + "71142000", + "71151000", + "71159000", + "71161000", + "71162010", + "71162020", + "71162090", + "71171100", + "71171900", + "71179000", + "71181010", + "71181090", + "71189000", + "72011000", + "72012000", + "72015000", + "72021100", + "72021900", + "72022100", + "72022900", + "72023000", + "72024100", + "72024900", + "72025000", + "72026000", + "72027000", + "72028000", + "72029100", + "72029200", + "72029300", + "72029910", + "72029990", + "72031000", + "72039000", + "72041000", + "72042100", + "72042900", + "72043000", + "72044100", + "72044900", + "72045000", + "72051000", + "72052100", + "72052910", + "72052920", + "72052990", + "72061000", + "72069000", + "72071110", + "72071190", + "72071200", + "72071900", + "72072000", + "72081000", + "72082500", + "72082610", + "72082690", + "72082710", + "72082790", + "72083610", + "72083690", + "72083700", + "72083810", + "72083890", + "72083910", + "72083990", + "72084000", + "72085100", + "72085200", + "72085300", + "72085400", + "72089000", + "72091500", + "72091600", + "72091700", + "72091800", + "72092500", + "72092600", + "72092700", + "72092800", + "72099000", + "72101100", + "72101200", + "72102000", + "72103010", + "72103090", + "72104110", + "72104190", + "72104910", + "72104990", + "72105000", + "72106100", + "72106911", + "72106919", + "72106990", + "72107010", + "72107020", + "72109000", + "72111300", + "72111400", + "72111900", + "72112300", + "72112910", + "72112920", + "72119010", + "72119090", + "72121000", + "72122010", + "72122090", + "72123000", + "72124010", + "72124021", + "72124029", + "72125010", + "72125090", + "72126000", + "72131000", + "72132000", + "72139110", + "72139190", + "72139910", + "72139990", + "72141010", + "72141090", + "72142000", + "72143000", + "72149100", + "72149910", + "72149990", + "72151000", + "72155000", + "72159010", + "72159090", + "72161000", + "72162100", + "72162200", + "72163100", + "72163200", + "72163300", + "72164010", + "72164090", + "72165000", + "72166110", + "72166190", + "72166910", + "72166990", + "72169100", + "72169900", + "72171011", + "72171019", + "72171090", + "72172010", + "72172090", + "72173010", + "72173090", + "72179000", + "72181000", + "72189100", + "72189900", + "72191100", + "72191200", + "72191300", + "72191400", + "72192100", + "72192200", + "72192300", + "72192400", + "72193100", + "72193200", + "72193300", + "72193400", + "72193500", + "72199010", + "72199090", + "72201100", + "72201210", + "72201220", + "72201290", + "72202010", + "72202090", + "72209000", + "72210000", + "72221100", + "72221910", + "72221990", + "72222000", + "72223000", + "72224010", + "72224090", + "72230000", + "72241000", + "72249000", + "72251100", + "72251900", + "72253000", + "72254010", + "72254020", + "72254090", + "72255010", + "72255090", + "72259100", + "72259200", + "72259910", + "72259990", + "72261100", + "72261900", + "72262010", + "72262090", + "72269100", + "72269200", + "72269900", + "72271000", + "72272000", + "72279000", + "72281010", + "72281090", + "72282000", + "72283000", + "72284000", + "72285000", + "72286000", + "72287000", + "72288000", + "72292000", + "72299000", + "73011000", + "73012000", + "73021010", + "73021090", + "73023000", + "73024000", + "73029000", + "73030000", + "73041100", + "73041900", + "73042200", + "73042310", + "73042390", + "73042400", + "73042910", + "73042931", + "73042939", + "73042990", + "73043110", + "73043190", + "73043910", + "73043920", + "73043990", + "73044110", + "73044190", + "73044900", + "73045111", + "73045119", + "73045190", + "73045910", + "73045990", + "73049011", + "73049019", + "73049090", + "73051100", + "73051200", + "73051900", + "73052000", + "73053100", + "73053900", + "73059000", + "73061100", + "73061900", + "73062100", + "73062900", + "73063010", + "73063090", + "73064000", + "73065000", + "73066100", + "73066900", + "73069010", + "73069020", + "73069090", + "73071100", + "73071910", + "73071920", + "73071990", + "73072100", + "73072200", + "73072300", + "73072900", + "73079100", + "73079200", + "73079300", + "73079900", + "73081000", + "73082000", + "73083000", + "73084000", + "73089010", + "73089090", + "73090010", + "73090020", + "73090090", + "73101010", + "73101090", + "73102110", + "73102190", + "73102910", + "73102920", + "73102990", + "73110000", + "73121010", + "73121090", + "73129000", + "73130000", + "73141200", + "73141400", + "73141900", + "73142000", + "73143100", + "73143900", + "73144100", + "73144200", + "73144900", + "73145000", + "73151110", + "73151190", + "73151210", + "73151290", + "73151900", + "73152000", + "73158100", + "73158200", + "73158900", + "73159000", + "73160000", + "73170010", + "73170020", + "73170030", + "73170090", + "73181100", + "73181200", + "73181300", + "73181400", + "73181500", + "73181600", + "73181900", + "73182100", + "73182200", + "73182300", + "73182400", + "73182900", + "73194000", + "73199000", + "73201000", + "73202010", + "73202090", + "73209000", + "73211100", + "73211200", + "73211900", + "73218100", + "73218200", + "73218900", + "73219000", + "73221100", + "73221900", + "73229010", + "73229090", + "73231000", + "73239100", + "73239200", + "73239300", + "73239400", + "73239900", + "73241000", + "73242100", + "73242900", + "73249000", + "73251000", + "73259100", + "73259910", + "73259990", + "73261100", + "73261900", + "73262000", + "73269010", + "73269020", + "73269090", + "74010000", + "74020000", + "74031100", + "74031200", + "74031300", + "74031900", + "74032100", + "74032200", + "74032900", + "74040000", + "74050000", + "74061010", + "74061020", + "74061090", + "74062000", + "74071010", + "74071021", + "74071029", + "74072110", + "74072120", + "74072910", + "74072921", + "74072929", + "74081100", + "74081900", + "74082100", + "74082200", + "74082912", + "74082913", + "74082919", + "74082990", + "74091100", + "74091900", + "74092100", + "74092900", + "74093111", + "74093119", + "74093190", + "74093900", + "74094011", + "74094019", + "74094090", + "74099000", + "74101112", + "74101113", + "74101119", + "74101190", + "74101200", + "74102110", + "74102120", + "74102130", + "74102190", + "74102200", + "74111010", + "74111090", + "74112110", + "74112190", + "74112210", + "74112290", + "74112910", + "74112990", + "74121000", + "74122000", + "74130000", + "74151000", + "74152100", + "74152900", + "74153300", + "74153900", + "74181000", + "74182000", + "74192000", + "74198010", + "74198020", + "74198030", + "74198040", + "74198090", + "75011000", + "75012000", + "75021010", + "75021090", + "75022000", + "75030000", + "75040010", + "75040090", + "75051110", + "75051121", + "75051129", + "75051210", + "75051221", + "75051229", + "75052100", + "75052210", + "75052290", + "75061000", + "75062000", + "75071100", + "75071200", + "75072000", + "75081000", + "75089010", + "75089090", + "76011000", + "76012000", + "76020000", + "76031000", + "76032000", + "76041010", + "76041021", + "76041029", + "76042100", + "76042911", + "76042919", + "76042920", + "76051110", + "76051190", + "76051910", + "76051990", + "76052110", + "76052190", + "76052910", + "76052990", + "76061110", + "76061190", + "76061210", + "76061220", + "76061230", + "76061290", + "76069100", + "76069200", + "76071110", + "76071120", + "76071190", + "76071910", + "76071990", + "76072000", + "76081000", + "76082010", + "76082090", + "76090000", + "76101000", + "76109000", + "76110000", + "76121000", + "76129011", + "76129012", + "76129019", + "76129020", + "76129090", + "76130000", + "76141010", + "76141090", + "76149010", + "76149090", + "76151000", + "76152000", + "76161000", + "76169100", + "76169900", + "78011011", + "78011019", + "78011090", + "78019100", + "78019900", + "78020000", + "78041100", + "78041900", + "78042000", + "78060010", + "78060020", + "78060090", + "79011111", + "79011119", + "79011191", + "79011199", + "79011210", + "79011290", + "79012010", + "79012090", + "79020000", + "79031000", + "79039000", + "79040000", + "79050000", + "79070010", + "79070090", + "80011000", + "80012000", + "80020000", + "80030000", + "80070010", + "80070020", + "80070030", + "80070090", + "81011000", + "81019400", + "81019600", + "81019700", + "81019910", + "81019990", + "81021000", + "81029400", + "81029500", + "81029600", + "81029700", + "81029900", + "81032000", + "81033000", + "81039100", + "81039900", + "81041100", + "81041900", + "81042000", + "81043000", + "81049000", + "81052010", + "81052021", + "81052029", + "81052090", + "81053000", + "81059010", + "81059090", + "81061000", + "81069000", + "81082000", + "81083000", + "81089000", + "81092100", + "81092900", + "81093100", + "81093900", + "81099100", + "81099900", + "81101010", + "81101020", + "81102000", + "81109000", + "81110010", + "81110020", + "81110090", + "81121200", + "81121300", + "81121900", + "81122110", + "81122120", + "81122200", + "81122900", + "81123100", + "81123900", + "81124100", + "81124900", + "81125100", + "81125200", + "81125900", + "81126100", + "81126900", + "81129200", + "81129900", + "81130010", + "81130090", + "82011000", + "82013000", + "82014000", + "82015000", + "82016000", + "82019000", + "82021000", + "82022000", + "82023100", + "82023900", + "82024000", + "82029100", + "82029910", + "82029990", + "82031010", + "82031090", + "82032010", + "82032090", + "82033000", + "82034000", + "82041100", + "82041200", + "82042000", + "82051000", + "82052000", + "82053000", + "82054000", + "82055100", + "82055900", + "82056000", + "82057000", + "82059000", + "82060000", + "82071300", + "82071910", + "82071990", + "82072000", + "82073000", + "82074010", + "82074020", + "82075011", + "82075019", + "82075090", + "82076000", + "82077010", + "82077020", + "82077090", + "82078000", + "82079000", + "82081000", + "82082000", + "82083000", + "82084000", + "82089000", + "82090011", + "82090019", + "82090090", + "82100010", + "82100090", + "82111000", + "82119100", + "82119210", + "82119220", + "82119290", + "82119310", + "82119320", + "82119390", + "82119400", + "82119500", + "82121010", + "82121020", + "82122010", + "82122020", + "82129000", + "82130000", + "82141000", + "82142000", + "82149010", + "82149090", + "82151000", + "82152000", + "82159100", + "82159910", + "82159990", + "83011000", + "83012000", + "83013000", + "83014000", + "83015000", + "83016000", + "83017000", + "83021000", + "83022000", + "83023000", + "83024100", + "83024200", + "83024900", + "83025000", + "83026000", + "83030000", + "83040000", + "83051000", + "83052000", + "83059000", + "83061000", + "83062100", + "83062900", + "83063000", + "83071010", + "83071090", + "83079000", + "83081000", + "83082000", + "83089010", + "83089020", + "83089090", + "83091000", + "83099000", + "83100000", + "83111000", + "83112000", + "83113000", + "83119000", + "84011000", + "84012000", + "84013000", + "84014000", + "84021100", + "84021200", + "84021900", + "84022000", + "84029000", + "84031010", + "84031090", + "84039000", + "84041010", + "84041020", + "84042000", + "84049010", + "84049090", + "84051000", + "84059000", + "84061000", + "84068100", + "84068200", + "84069011", + "84069019", + "84069021", + "84069029", + "84069090", + "84071000", + "84072110", + "84072190", + "84072910", + "84072990", + "84073110", + "84073190", + "84073200", + "84073310", + "84073390", + "84073410", + "84073490", + "84079000", + "84081010", + "84081090", + "84082010", + "84082020", + "84082030", + "84082090", + "84089010", + "84089090", + "84091000", + "84099111", + "84099112", + "84099113", + "84099114", + "84099115", + "84099116", + "84099117", + "84099118", + "84099120", + "84099130", + "84099140", + "84099190", + "84099912", + "84099914", + "84099915", + "84099917", + "84099921", + "84099929", + "84099930", + "84099941", + "84099949", + "84099951", + "84099959", + "84099961", + "84099969", + "84099971", + "84099979", + "84099991", + "84099999", + "84101100", + "84101200", + "84101300", + "84109000", + "84111100", + "84111200", + "84112100", + "84112200", + "84118100", + "84118200", + "84119100", + "84119900", + "84121000", + "84122110", + "84122190", + "84122900", + "84123110", + "84123190", + "84123900", + "84128000", + "84129010", + "84129080", + "84129090", + "84131100", + "84131900", + "84132000", + "84133010", + "84133020", + "84133030", + "84133090", + "84134000", + "84135010", + "84135090", + "84136011", + "84136019", + "84136090", + "84137010", + "84137080", + "84137090", + "84138100", + "84138200", + "84139110", + "84139190", + "84139200", + "84141000", + "84142000", + "84143011", + "84143019", + "84143091", + "84143099", + "84144010", + "84144020", + "84144090", + "84145110", + "84145120", + "84145190", + "84145910", + "84145990", + "84146000", + "84147000", + "84148011", + "84148012", + "84148013", + "84148019", + "84148021", + "84148022", + "84148029", + "84148031", + "84148032", + "84148033", + "84148038", + "84148039", + "84148090", + "84149010", + "84149020", + "84149031", + "84149032", + "84149033", + "84149034", + "84149039", + "84149040", + "84151011", + "84151019", + "84151090", + "84152010", + "84152090", + "84158110", + "84158190", + "84158210", + "84158290", + "84158300", + "84159010", + "84159020", + "84159090", + "84161000", + "84162010", + "84162090", + "84163000", + "84169000", + "84171010", + "84171020", + "84171090", + "84172000", + "84178010", + "84178020", + "84178090", + "84179000", + "84181000", + "84182100", + "84182900", + "84183000", + "84184000", + "84185010", + "84185090", + "84186100", + "84186910", + "84186920", + "84186931", + "84186932", + "84186940", + "84186991", + "84186999", + "84189100", + "84189900", + "84191100", + "84191200", + "84191900", + "84192000", + "84193300", + "84193400", + "84193500", + "84193900", + "84194010", + "84194020", + "84194090", + "84195010", + "84195021", + "84195022", + "84195029", + "84195090", + "84196000", + "84198110", + "84198190", + "84198911", + "84198919", + "84198920", + "84198930", + "84198940", + "84198991", + "84198999", + "84199010", + "84199020", + "84199031", + "84199039", + "84199040", + "84199090", + "84201010", + "84201090", + "84209100", + "84209900", + "84211110", + "84211190", + "84211210", + "84211290", + "84211910", + "84211990", + "84212100", + "84212200", + "84212300", + "84212911", + "84212919", + "84212920", + "84212930", + "84212990", + "84213100", + "84213200", + "84213910", + "84213930", + "84213990", + "84219110", + "84219191", + "84219199", + "84219910", + "84219920", + "84219991", + "84219999", + "84221100", + "84221900", + "84222000", + "84223010", + "84223021", + "84223022", + "84223023", + "84223029", + "84223030", + "84224010", + "84224020", + "84224030", + "84224090", + "84229010", + "84229090", + "84231000", + "84232000", + "84233011", + "84233019", + "84233090", + "84238110", + "84238190", + "84238200", + "84238900", + "84239010", + "84239021", + "84239029", + "84241000", + "84242000", + "84243010", + "84243020", + "84243030", + "84243090", + "84244100", + "84244900", + "84248221", + "84248229", + "84248290", + "84248910", + "84248920", + "84248990", + "84249010", + "84249090", + "84251100", + "84251910", + "84251990", + "84253110", + "84253190", + "84253910", + "84253990", + "84254100", + "84254200", + "84254910", + "84254990", + "84261100", + "84261200", + "84261900", + "84262000", + "84263000", + "84264110", + "84264190", + "84264910", + "84264990", + "84269100", + "84269900", + "84271011", + "84271019", + "84271090", + "84272010", + "84272090", + "84279000", + "84281000", + "84282010", + "84282090", + "84283100", + "84283200", + "84283300", + "84283910", + "84283920", + "84283930", + "84283990", + "84284000", + "84286000", + "84287000", + "84289010", + "84289020", + "84289030", + "84289090", + "84291110", + "84291190", + "84291910", + "84291990", + "84292010", + "84292090", + "84293000", + "84294000", + "84295111", + "84295119", + "84295121", + "84295129", + "84295191", + "84295192", + "84295199", + "84295211", + "84295212", + "84295219", + "84295220", + "84295290", + "84295900", + "84301000", + "84302000", + "84303110", + "84303190", + "84303910", + "84303990", + "84304110", + "84304120", + "84304130", + "84304190", + "84304910", + "84304920", + "84304990", + "84305000", + "84306100", + "84306911", + "84306919", + "84306990", + "84311010", + "84311090", + "84312011", + "84312019", + "84312090", + "84313110", + "84313190", + "84313900", + "84314100", + "84314200", + "84314310", + "84314390", + "84314910", + "84314921", + "84314922", + "84314923", + "84314929", + "84321000", + "84322100", + "84322900", + "84323110", + "84323190", + "84323910", + "84323990", + "84324100", + "84324200", + "84328000", + "84329000", + "84331100", + "84331900", + "84332010", + "84332090", + "84333000", + "84334000", + "84335100", + "84335200", + "84335300", + "84335911", + "84335919", + "84335990", + "84336010", + "84336021", + "84336029", + "84336090", + "84339010", + "84339090", + "84341000", + "84342010", + "84342090", + "84349000", + "84351000", + "84359000", + "84361000", + "84362100", + "84362900", + "84368000", + "84369100", + "84369900", + "84371000", + "84378010", + "84378090", + "84379000", + "84381000", + "84382011", + "84382019", + "84382090", + "84383000", + "84384000", + "84385000", + "84386000", + "84388010", + "84388020", + "84388090", + "84389000", + "84391010", + "84391020", + "84391030", + "84391090", + "84392000", + "84393010", + "84393020", + "84393030", + "84393090", + "84399100", + "84399910", + "84399990", + "84401011", + "84401019", + "84401020", + "84401090", + "84409000", + "84411010", + "84411090", + "84412000", + "84413010", + "84413090", + "84414000", + "84418000", + "84419000", + "84423010", + "84423020", + "84423090", + "84424010", + "84424020", + "84424090", + "84425000", + "84431110", + "84431190", + "84431200", + "84431310", + "84431321", + "84431329", + "84431390", + "84431400", + "84431500", + "84431600", + "84431710", + "84431790", + "84431910", + "84431990", + "84433111", + "84433112", + "84433113", + "84433114", + "84433115", + "84433116", + "84433119", + "84433191", + "84433199", + "84433221", + "84433222", + "84433223", + "84433229", + "84433231", + "84433232", + "84433233", + "84433234", + "84433235", + "84433236", + "84433237", + "84433238", + "84433239", + "84433240", + "84433251", + "84433252", + "84433259", + "84433291", + "84433299", + "84433910", + "84433921", + "84433928", + "84433929", + "84433930", + "84433990", + "84439110", + "84439191", + "84439192", + "84439199", + "84439911", + "84439912", + "84439919", + "84439921", + "84439922", + "84439923", + "84439929", + "84439931", + "84439932", + "84439933", + "84439939", + "84439941", + "84439942", + "84439949", + "84439950", + "84439960", + "84439970", + "84439980", + "84439990", + "84440010", + "84440020", + "84440090", + "84451110", + "84451120", + "84451190", + "84451200", + "84451300", + "84451910", + "84451921", + "84451922", + "84451923", + "84451924", + "84451925", + "84451926", + "84451927", + "84451929", + "84452000", + "84453010", + "84453090", + "84454011", + "84454012", + "84454018", + "84454019", + "84454021", + "84454029", + "84454031", + "84454039", + "84454040", + "84454090", + "84459010", + "84459020", + "84459030", + "84459040", + "84459090", + "84461010", + "84461090", + "84462100", + "84462900", + "84463010", + "84463020", + "84463030", + "84463040", + "84463090", + "84471100", + "84471200", + "84472010", + "84472021", + "84472029", + "84472030", + "84479010", + "84479020", + "84479090", + "84481110", + "84481120", + "84481190", + "84481900", + "84482010", + "84482020", + "84482030", + "84482090", + "84483100", + "84483211", + "84483219", + "84483220", + "84483230", + "84483240", + "84483250", + "84483290", + "84483310", + "84483390", + "84483911", + "84483912", + "84483917", + "84483919", + "84483921", + "84483922", + "84483923", + "84483929", + "84483991", + "84483992", + "84483999", + "84484200", + "84484910", + "84484920", + "84484990", + "84485110", + "84485190", + "84485910", + "84485921", + "84485922", + "84485929", + "84485930", + "84485940", + "84485990", + "84490010", + "84490020", + "84490080", + "84490091", + "84490099", + "84501100", + "84501200", + "84501900", + "84502010", + "84502020", + "84502090", + "84509010", + "84509090", + "84511000", + "84512100", + "84512910", + "84512990", + "84513010", + "84513091", + "84513099", + "84514010", + "84514021", + "84514029", + "84514090", + "84515010", + "84515020", + "84515090", + "84518000", + "84519010", + "84519090", + "84521000", + "84522110", + "84522120", + "84522190", + "84522910", + "84522921", + "84522922", + "84522923", + "84522924", + "84522925", + "84522929", + "84522990", + "84523000", + "84529020", + "84529081", + "84529089", + "84529091", + "84529092", + "84529093", + "84529094", + "84529099", + "84531010", + "84531090", + "84532000", + "84538000", + "84539000", + "84541000", + "84542010", + "84542090", + "84543010", + "84543020", + "84543090", + "84549010", + "84549090", + "84551000", + "84552110", + "84552190", + "84552210", + "84552290", + "84553010", + "84553020", + "84553090", + "84559000", + "84561111", + "84561119", + "84561190", + "84561211", + "84561219", + "84561290", + "84562010", + "84562090", + "84563011", + "84563019", + "84563090", + "84564000", + "84565000", + "84569000", + "84571000", + "84572010", + "84572090", + "84573010", + "84573090", + "84581110", + "84581191", + "84581199", + "84581910", + "84581990", + "84589100", + "84589900", + "84591000", + "84592110", + "84592191", + "84592199", + "84592900", + "84593100", + "84593900", + "84594100", + "84594900", + "84595100", + "84595900", + "84596100", + "84596900", + "84597000", + "84601200", + "84601900", + "84602200", + "84602300", + "84602400", + "84602900", + "84603100", + "84603900", + "84604011", + "84604019", + "84604091", + "84604099", + "84609011", + "84609012", + "84609019", + "84609090", + "84612010", + "84612090", + "84613010", + "84613090", + "84614010", + "84614091", + "84614099", + "84615010", + "84615020", + "84615090", + "84619010", + "84619090", + "84621100", + "84621900", + "84622200", + "84622300", + "84622400", + "84622500", + "84622600", + "84622900", + "84623200", + "84623300", + "84623900", + "84624200", + "84624900", + "84625100", + "84625900", + "84626100", + "84626200", + "84626300", + "84626900", + "84629000", + "84631010", + "84631090", + "84632010", + "84632091", + "84632099", + "84633000", + "84639010", + "84639090", + "84641000", + "84642010", + "84642021", + "84642029", + "84642090", + "84649011", + "84649019", + "84649090", + "84651000", + "84652000", + "84659110", + "84659120", + "84659190", + "84659211", + "84659219", + "84659290", + "84659310", + "84659390", + "84659400", + "84659511", + "84659512", + "84659591", + "84659592", + "84659600", + "84659900", + "84661000", + "84662010", + "84662090", + "84663000", + "84669100", + "84669200", + "84669311", + "84669319", + "84669320", + "84669330", + "84669340", + "84669350", + "84669360", + "84669410", + "84669420", + "84669490", + "84671110", + "84671190", + "84671900", + "84672100", + "84672200", + "84672910", + "84672991", + "84672992", + "84672993", + "84672999", + "84678100", + "84678900", + "84679100", + "84679200", + "84679900", + "84681000", + "84682000", + "84688010", + "84688090", + "84689010", + "84689020", + "84689090", + "84701000", + "84702100", + "84702900", + "84703000", + "84705010", + "84705090", + "84709010", + "84709090", + "84713011", + "84713012", + "84713019", + "84713090", + "84714100", + "84714900", + "84715010", + "84715020", + "84715030", + "84715040", + "84715090", + "84716052", + "84716053", + "84716054", + "84716059", + "84716061", + "84716062", + "84716080", + "84716090", + "84717010", + "84717020", + "84717030", + "84717040", + "84717090", + "84718000", + "84719011", + "84719012", + "84719013", + "84719014", + "84719019", + "84719090", + "84721000", + "84723010", + "84723020", + "84723030", + "84723090", + "84729010", + "84729020", + "84729030", + "84729040", + "84729051", + "84729059", + "84729091", + "84729099", + "84732100", + "84732910", + "84732920", + "84732990", + "84733011", + "84733019", + "84733031", + "84733032", + "84733033", + "84733034", + "84733039", + "84733041", + "84733042", + "84733049", + "84733090", + "84734010", + "84734070", + "84734090", + "84735010", + "84735040", + "84735050", + "84735090", + "84741000", + "84742010", + "84742090", + "84743100", + "84743200", + "84743900", + "84748010", + "84748090", + "84749000", + "84751000", + "84752100", + "84752910", + "84752990", + "84759000", + "84762100", + "84762900", + "84768100", + "84768910", + "84768990", + "84769000", + "84771011", + "84771019", + "84771021", + "84771029", + "84771091", + "84771099", + "84772010", + "84772090", + "84773010", + "84773090", + "84774010", + "84774090", + "84775100", + "84775911", + "84775919", + "84775990", + "84778010", + "84778090", + "84779000", + "84781010", + "84781090", + "84789000", + "84791010", + "84791090", + "84792000", + "84793000", + "84794000", + "84795000", + "84796000", + "84797100", + "84797900", + "84798110", + "84798190", + "84798210", + "84798290", + "84798300", + "84798911", + "84798912", + "84798921", + "84798922", + "84798931", + "84798932", + "84798940", + "84798991", + "84798992", + "84798999", + "84799010", + "84799090", + "84801000", + "84802000", + "84803000", + "84804100", + "84804910", + "84804990", + "84805000", + "84806000", + "84807100", + "84807910", + "84807990", + "84811000", + "84812011", + "84812019", + "84812090", + "84813000", + "84814000", + "84818011", + "84818019", + "84818021", + "84818029", + "84818031", + "84818039", + "84818091", + "84818092", + "84818093", + "84818094", + "84818095", + "84818096", + "84818097", + "84818099", + "84819010", + "84819090", + "84821010", + "84821090", + "84822010", + "84822090", + "84823000", + "84824000", + "84825010", + "84825090", + "84828000", + "84829111", + "84829119", + "84829120", + "84829130", + "84829190", + "84829910", + "84829990", + "84831011", + "84831019", + "84831020", + "84831030", + "84831040", + "84831050", + "84831090", + "84832000", + "84833010", + "84833021", + "84833029", + "84833090", + "84834010", + "84834090", + "84835010", + "84835090", + "84836011", + "84836019", + "84836090", + "84839000", + "84841000", + "84842000", + "84849000", + "84851000", + "84852000", + "84853000", + "84858000", + "84859000", + "84861000", + "84862000", + "84863000", + "84864000", + "84869000", + "84871000", + "84879000", + "85011011", + "85011019", + "85011021", + "85011029", + "85011030", + "85012000", + "85013110", + "85013120", + "85013210", + "85013220", + "85013310", + "85013320", + "85013411", + "85013419", + "85013420", + "85014011", + "85014019", + "85014021", + "85014029", + "85015110", + "85015120", + "85015190", + "85015210", + "85015220", + "85015290", + "85015310", + "85015320", + "85015330", + "85015390", + "85016100", + "85016200", + "85016300", + "85016400", + "85017100", + "85017210", + "85017290", + "85018000", + "85021110", + "85021190", + "85021210", + "85021290", + "85021311", + "85021319", + "85021390", + "85022011", + "85022019", + "85022090", + "85023100", + "85023900", + "85024010", + "85024090", + "85030010", + "85030090", + "85041000", + "85042100", + "85042200", + "85042300", + "85043111", + "85043119", + "85043191", + "85043192", + "85043193", + "85043199", + "85043211", + "85043219", + "85043221", + "85043229", + "85043300", + "85043400", + "85044010", + "85044021", + "85044022", + "85044029", + "85044030", + "85044040", + "85044050", + "85044060", + "85044090", + "85045010", + "85045090", + "85049010", + "85049020", + "85049030", + "85049040", + "85049090", + "85051100", + "85051910", + "85051990", + "85052010", + "85052090", + "85059011", + "85059019", + "85059080", + "85059090", + "85061011", + "85061012", + "85061019", + "85061020", + "85061031", + "85061032", + "85061039", + "85063010", + "85063090", + "85064010", + "85064090", + "85065010", + "85065090", + "85066010", + "85066090", + "85068010", + "85068090", + "85069000", + "85071010", + "85071090", + "85072010", + "85072090", + "85073011", + "85073019", + "85073090", + "85075010", + "85075020", + "85075090", + "85076000", + "85078000", + "85079010", + "85079020", + "85079090", + "85081100", + "85081900", + "85086000", + "85087000", + "85094010", + "85094020", + "85094030", + "85094040", + "85094050", + "85094090", + "85098010", + "85098090", + "85099000", + "85101000", + "85102000", + "85103000", + "85109011", + "85109019", + "85109020", + "85109090", + "85111000", + "85112010", + "85112090", + "85113010", + "85113020", + "85114000", + "85115010", + "85115090", + "85118010", + "85118020", + "85118030", + "85118090", + "85119000", + "85121000", + "85122011", + "85122019", + "85122021", + "85122022", + "85122023", + "85122029", + "85123000", + "85124010", + "85124020", + "85129000", + "85131010", + "85131090", + "85139000", + "85141100", + "85141900", + "85142011", + "85142019", + "85142020", + "85143100", + "85143200", + "85143900", + "85144000", + "85149000", + "85151100", + "85151900", + "85152100", + "85152900", + "85153110", + "85153190", + "85153900", + "85158010", + "85158090", + "85159000", + "85161000", + "85162100", + "85162900", + "85163100", + "85163200", + "85163300", + "85164000", + "85165000", + "85166000", + "85167100", + "85167200", + "85167910", + "85167920", + "85167990", + "85168010", + "85168090", + "85169000", + "85171100", + "85171300", + "85171410", + "85171431", + "85171432", + "85171439", + "85171441", + "85171449", + "85171490", + "85171830", + "85171890", + "85176130", + "85176141", + "85176142", + "85176143", + "85176149", + "85176191", + "85176192", + "85176199", + "85176214", + "85176215", + "85176221", + "85176229", + "85176234", + "85176239", + "85176241", + "85176249", + "85176251", + "85176252", + "85176253", + "85176254", + "85176255", + "85176256", + "85176259", + "85176262", + "85176264", + "85176265", + "85176272", + "85176273", + "85176277", + "85176278", + "85176279", + "85176291", + "85176294", + "85176296", + "85176299", + "85176900", + "85177110", + "85177120", + "85177190", + "85177900", + "85181010", + "85181090", + "85182100", + "85182200", + "85182910", + "85182990", + "85183000", + "85184000", + "85185000", + "85189010", + "85189090", + "85192000", + "85193000", + "85198110", + "85198120", + "85198190", + "85198900", + "85211010", + "85211081", + "85211089", + "85211090", + "85219000", + "85221000", + "85229000", + "85232110", + "85232120", + "85232911", + "85232919", + "85232990", + "85234110", + "85234190", + "85234910", + "85234920", + "85234990", + "85235110", + "85235190", + "85235210", + "85235290", + "85235900", + "85238000", + "85241100", + "85241200", + "85241900", + "85249100", + "85249200", + "85249900", + "85255011", + "85255019", + "85255021", + "85255022", + "85255023", + "85255024", + "85255029", + "85256010", + "85256020", + "85256090", + "85258100", + "85258200", + "85258300", + "85258911", + "85258912", + "85258913", + "85258914", + "85258919", + "85258921", + "85258922", + "85258929", + "85261000", + "85269100", + "85269200", + "85271200", + "85271300", + "85271900", + "85272100", + "85272900", + "85279100", + "85279200", + "85279910", + "85279990", + "85284200", + "85284930", + "85284990", + "85285200", + "85285900", + "85286200", + "85286910", + "85286990", + "85287111", + "85287119", + "85287190", + "85287200", + "85287300", + "85291020", + "85291090", + "85299011", + "85299012", + "85299019", + "85299020", + "85299030", + "85299040", + "85299050", + "85299090", + "85301010", + "85301090", + "85308010", + "85308090", + "85309000", + "85311010", + "85311090", + "85312000", + "85318000", + "85319000", + "85321000", + "85322111", + "85322119", + "85322120", + "85322190", + "85322200", + "85322310", + "85322390", + "85322410", + "85322420", + "85322490", + "85322510", + "85322590", + "85322910", + "85322990", + "85323010", + "85323090", + "85329000", + "85331000", + "85332110", + "85332120", + "85332190", + "85332900", + "85333110", + "85333190", + "85333910", + "85333990", + "85334011", + "85334012", + "85334013", + "85334019", + "85334091", + "85334092", + "85334099", + "85339000", + "85340011", + "85340012", + "85340013", + "85340019", + "85340020", + "85340031", + "85340032", + "85340033", + "85340039", + "85340040", + "85340051", + "85340059", + "85351000", + "85352100", + "85352900", + "85353013", + "85353017", + "85353018", + "85353019", + "85353023", + "85353027", + "85353028", + "85353029", + "85354010", + "85354090", + "85359010", + "85359090", + "85361000", + "85362000", + "85363010", + "85363090", + "85364100", + "85364900", + "85365010", + "85365020", + "85365030", + "85365090", + "85366100", + "85366910", + "85366990", + "85367000", + "85369010", + "85369020", + "85369030", + "85369040", + "85369050", + "85369060", + "85369090", + "85371011", + "85371019", + "85371020", + "85371030", + "85371090", + "85372010", + "85372090", + "85381000", + "85389010", + "85389020", + "85389090", + "85391010", + "85391090", + "85392110", + "85392190", + "85392200", + "85392910", + "85392990", + "85393111", + "85393119", + "85393120", + "85393131", + "85393132", + "85393139", + "85393210", + "85393220", + "85393230", + "85393911", + "85393912", + "85393913", + "85393919", + "85393990", + "85394110", + "85394190", + "85394900", + "85395100", + "85395200", + "85399010", + "85399020", + "85399090", + "85401100", + "85401200", + "85402011", + "85402019", + "85402020", + "85402090", + "85404000", + "85406010", + "85406090", + "85407100", + "85407900", + "85408100", + "85408910", + "85408990", + "85409110", + "85409120", + "85409130", + "85409140", + "85409190", + "85409900", + "85411011", + "85411012", + "85411019", + "85411021", + "85411022", + "85411029", + "85411031", + "85411032", + "85411039", + "85411091", + "85411092", + "85411099", + "85412110", + "85412120", + "85412191", + "85412199", + "85412910", + "85412920", + "85413011", + "85413019", + "85413021", + "85413029", + "85414111", + "85414112", + "85414121", + "85414122", + "85414123", + "85414124", + "85414210", + "85414220", + "85414290", + "85414300", + "85414900", + "85415100", + "85415900", + "85416010", + "85416090", + "85419010", + "85419020", + "85419090", + "85423110", + "85423120", + "85423190", + "85423210", + "85423221", + "85423229", + "85423291", + "85423299", + "85423311", + "85423319", + "85423320", + "85423390", + "85423911", + "85423919", + "85423920", + "85423931", + "85423939", + "85423991", + "85423999", + "85429000", + "85431000", + "85432000", + "85433010", + "85433090", + "85434000", + "85437011", + "85437012", + "85437013", + "85437014", + "85437015", + "85437019", + "85437020", + "85437031", + "85437032", + "85437033", + "85437034", + "85437035", + "85437036", + "85437039", + "85437040", + "85437050", + "85437091", + "85437092", + "85437099", + "85439010", + "85439090", + "85441100", + "85441911", + "85441919", + "85441990", + "85442000", + "85443000", + "85444200", + "85444900", + "85446000", + "85447010", + "85447020", + "85447030", + "85447090", + "85451100", + "85451910", + "85451920", + "85451990", + "85452000", + "85459010", + "85459020", + "85459030", + "85459090", + "85461000", + "85462000", + "85469000", + "85471000", + "85472010", + "85472090", + "85479000", + "85480010", + "85480090", + "85491100", + "85491200", + "85491300", + "85491400", + "85491900", + "85492100", + "85492900", + "85493100", + "85493900", + "85499100", + "85499900", + "86011000", + "86012000", + "86021000", + "86029000", + "86031000", + "86039000", + "86040010", + "86040090", + "86050010", + "86050090", + "86061000", + "86063000", + "86069100", + "86069200", + "86069900", + "86071110", + "86071120", + "86071200", + "86071911", + "86071919", + "86071990", + "86072100", + "86072900", + "86073000", + "86079100", + "86079900", + "86080011", + "86080012", + "86080090", + "86090000", + "87011000", + "87012100", + "87012200", + "87012300", + "87012400", + "87012900", + "87013000", + "87019100", + "87019200", + "87019300", + "87019410", + "87019490", + "87019510", + "87019590", + "87021000", + "87022000", + "87023000", + "87024010", + "87024090", + "87029000", + "87031000", + "87032100", + "87032210", + "87032290", + "87032310", + "87032390", + "87032410", + "87032490", + "87033110", + "87033190", + "87033210", + "87033290", + "87033310", + "87033390", + "87034000", + "87035000", + "87036000", + "87037000", + "87038000", + "87039000", + "87041010", + "87041090", + "87042110", + "87042120", + "87042130", + "87042190", + "87042210", + "87042220", + "87042230", + "87042290", + "87042310", + "87042320", + "87042330", + "87042340", + "87042390", + "87043110", + "87043120", + "87043130", + "87043190", + "87043210", + "87043220", + "87043230", + "87043290", + "87044100", + "87044200", + "87044300", + "87045100", + "87045200", + "87046000", + "87049000", + "87051020", + "87051030", + "87051090", + "87052000", + "87053000", + "87054000", + "87059010", + "87059090", + "87060010", + "87060020", + "87060090", + "87071000", + "87079010", + "87079090", + "87081000", + "87082100", + "87082200", + "87082911", + "87082912", + "87082913", + "87082914", + "87082919", + "87082991", + "87082992", + "87082993", + "87082994", + "87082995", + "87082999", + "87083011", + "87083019", + "87083090", + "87084011", + "87084019", + "87084080", + "87084090", + "87085011", + "87085012", + "87085019", + "87085080", + "87085091", + "87085099", + "87087010", + "87087090", + "87088000", + "87089100", + "87089200", + "87089300", + "87089411", + "87089412", + "87089413", + "87089481", + "87089482", + "87089483", + "87089490", + "87089510", + "87089521", + "87089522", + "87089529", + "87089910", + "87089990", + "87091100", + "87091900", + "87099000", + "87100000", + "87111000", + "87112010", + "87112020", + "87112090", + "87113000", + "87114000", + "87115000", + "87116000", + "87119000", + "87120010", + "87120090", + "87131000", + "87139000", + "87141000", + "87142000", + "87149100", + "87149200", + "87149311", + "87149319", + "87149320", + "87149410", + "87149490", + "87149500", + "87149611", + "87149612", + "87149619", + "87149690", + "87149910", + "87149920", + "87149990", + "87150000", + "87161000", + "87162000", + "87163100", + "87163900", + "87164000", + "87168000", + "87169010", + "87169090", + "88010000", + "88021100", + "88021210", + "88021290", + "88022010", + "88022021", + "88022022", + "88022090", + "88023010", + "88023021", + "88023029", + "88023031", + "88023039", + "88023090", + "88024010", + "88024090", + "88026000", + "88040000", + "88051000", + "88052100", + "88052900", + "88061000", + "88062100", + "88062200", + "88062300", + "88062400", + "88062900", + "88069100", + "88069200", + "88069300", + "88069400", + "88069900", + "88071000", + "88072000", + "88073000", + "88079000", + "89011000", + "89012000", + "89013000", + "89019000", + "89020010", + "89020090", + "89031100", + "89031200", + "89031900", + "89032100", + "89032200", + "89032300", + "89033100", + "89033200", + "89033300", + "89039300", + "89039900", + "89040000", + "89051000", + "89052000", + "89059000", + "89061000", + "89069000", + "89071000", + "89079000", + "89080000", + "90011011", + "90011019", + "90011020", + "90012000", + "90013000", + "90014000", + "90015000", + "90019010", + "90019090", + "90021111", + "90021119", + "90021120", + "90021190", + "90021900", + "90022010", + "90022090", + "90029010", + "90029090", + "90031100", + "90031910", + "90031990", + "90039010", + "90039090", + "90041000", + "90049010", + "90049020", + "90049090", + "90051000", + "90058000", + "90059010", + "90059090", + "90063000", + "90064000", + "90065310", + "90065320", + "90065930", + "90065940", + "90065951", + "90065959", + "90066100", + "90066900", + "90069110", + "90069190", + "90069900", + "90071000", + "90072020", + "90072090", + "90079100", + "90079200", + "90085000", + "90089000", + "90101010", + "90101020", + "90101090", + "90105010", + "90105020", + "90105090", + "90106000", + "90109010", + "90109090", + "90111000", + "90112010", + "90112020", + "90112030", + "90118010", + "90118090", + "90119010", + "90119090", + "90121010", + "90121090", + "90129010", + "90129090", + "90131010", + "90131090", + "90132000", + "90138000", + "90139000", + "90141000", + "90142010", + "90142020", + "90142030", + "90142090", + "90148010", + "90148090", + "90149000", + "90151000", + "90152010", + "90152090", + "90153000", + "90154000", + "90158010", + "90158090", + "90159010", + "90159090", + "90160010", + "90160090", + "90171010", + "90171090", + "90172000", + "90173010", + "90173020", + "90173090", + "90178010", + "90178090", + "90179010", + "90179090", + "90181100", + "90181210", + "90181290", + "90181300", + "90181410", + "90181420", + "90181490", + "90181910", + "90181920", + "90181980", + "90181990", + "90182010", + "90182020", + "90182090", + "90183111", + "90183119", + "90183190", + "90183211", + "90183212", + "90183213", + "90183219", + "90183220", + "90183910", + "90183921", + "90183922", + "90183923", + "90183925", + "90183926", + "90183929", + "90183930", + "90183991", + "90183999", + "90184100", + "90184911", + "90184912", + "90184919", + "90184920", + "90184940", + "90184991", + "90184999", + "90185010", + "90185090", + "90189010", + "90189021", + "90189029", + "90189031", + "90189039", + "90189040", + "90189050", + "90189061", + "90189069", + "90189091", + "90189093", + "90189094", + "90189095", + "90189096", + "90189097", + "90189099", + "90191000", + "90192010", + "90192020", + "90192030", + "90192040", + "90192090", + "90200010", + "90200090", + "90211010", + "90211020", + "90211091", + "90211099", + "90212110", + "90212190", + "90212900", + "90213110", + "90213120", + "90213190", + "90213911", + "90213919", + "90213920", + "90213930", + "90213940", + "90213980", + "90213991", + "90213999", + "90214000", + "90215000", + "90219011", + "90219012", + "90219013", + "90219019", + "90219080", + "90219091", + "90219092", + "90219099", + "90221200", + "90221311", + "90221319", + "90221390", + "90221411", + "90221412", + "90221413", + "90221419", + "90221490", + "90221910", + "90221991", + "90221999", + "90222110", + "90222120", + "90222190", + "90222910", + "90222990", + "90223000", + "90229010", + "90229020", + "90229080", + "90229091", + "90229099", + "90230000", + "90241010", + "90241020", + "90241090", + "90248011", + "90248019", + "90248021", + "90248029", + "90248090", + "90249000", + "90251111", + "90251119", + "90251191", + "90251199", + "90251910", + "90251990", + "90258000", + "90259010", + "90259090", + "90261011", + "90261019", + "90261021", + "90261029", + "90262010", + "90262090", + "90268000", + "90269010", + "90269020", + "90269090", + "90271000", + "90272011", + "90272012", + "90272019", + "90272021", + "90272029", + "90273011", + "90273019", + "90273020", + "90275010", + "90275020", + "90275030", + "90275040", + "90275050", + "90275090", + "90278100", + "90278911", + "90278912", + "90278913", + "90278914", + "90278920", + "90278991", + "90278999", + "90279010", + "90279091", + "90279093", + "90279099", + "90281011", + "90281019", + "90281090", + "90282010", + "90282020", + "90283011", + "90283019", + "90283021", + "90283029", + "90283031", + "90283039", + "90283090", + "90289010", + "90289090", + "90291010", + "90291090", + "90292010", + "90292020", + "90299010", + "90299090", + "90301010", + "90301090", + "90302010", + "90302021", + "90302022", + "90302029", + "90302030", + "90303100", + "90303200", + "90303311", + "90303319", + "90303321", + "90303329", + "90303390", + "90303910", + "90303990", + "90304010", + "90304020", + "90304030", + "90304090", + "90308210", + "90308290", + "90308410", + "90308420", + "90308490", + "90308910", + "90308920", + "90308930", + "90308940", + "90308990", + "90309010", + "90309090", + "90311000", + "90312010", + "90312090", + "90314100", + "90314910", + "90314920", + "90314990", + "90318011", + "90318012", + "90318020", + "90318030", + "90318040", + "90318050", + "90318060", + "90318091", + "90318099", + "90319010", + "90319090", + "90321010", + "90321090", + "90322000", + "90328100", + "90328911", + "90328919", + "90328921", + "90328922", + "90328923", + "90328924", + "90328925", + "90328929", + "90328930", + "90328981", + "90328982", + "90328983", + "90328984", + "90328989", + "90328990", + "90329010", + "90329091", + "90329099", + "90330000", + "91011100", + "91011900", + "91012100", + "91012900", + "91019100", + "91019900", + "91021110", + "91021190", + "91021210", + "91021220", + "91021290", + "91021900", + "91022100", + "91022900", + "91029100", + "91029900", + "91031000", + "91039000", + "91040000", + "91051100", + "91051900", + "91052100", + "91052900", + "91059100", + "91059900", + "91061000", + "91069000", + "91070010", + "91070090", + "91081110", + "91081190", + "91081200", + "91081900", + "91082000", + "91089000", + "91091000", + "91099000", + "91101110", + "91101190", + "91101200", + "91101900", + "91109000", + "91111000", + "91112010", + "91112090", + "91118000", + "91119010", + "91119090", + "91122000", + "91129000", + "91131000", + "91132000", + "91139000", + "91143000", + "91144000", + "91149000", + "92011000", + "92012000", + "92019000", + "92021000", + "92029000", + "92051000", + "92059000", + "92060000", + "92071010", + "92071090", + "92079010", + "92079090", + "92081000", + "92089000", + "92093000", + "92099100", + "92099200", + "92099400", + "92099900", + "93011000", + "93012000", + "93019000", + "93020000", + "93031000", + "93032000", + "93033000", + "93039010", + "93039090", + "93040010", + "93040090", + "93051000", + "93052000", + "93059100", + "93059900", + "93062110", + "93062120", + "93062130", + "93062190", + "93062900", + "93063000", + "93069010", + "93069020", + "93069090", + "93070000", + "94011010", + "94011090", + "94012000", + "94013100", + "94013900", + "94014100", + "94014900", + "94015200", + "94015300", + "94015900", + "94016100", + "94016900", + "94017100", + "94017900", + "94018000", + "94019100", + "94019900", + "94021000", + "94029010", + "94029020", + "94029090", + "94031000", + "94032010", + "94032090", + "94033000", + "94034000", + "94035000", + "94036000", + "94037000", + "94038200", + "94038300", + "94038900", + "94039100", + "94039900", + "94041000", + "94042100", + "94042900", + "94043000", + "94044000", + "94049000", + "94051110", + "94051190", + "94051910", + "94051990", + "94052100", + "94052900", + "94053100", + "94053900", + "94054100", + "94054200", + "94054900", + "94055000", + "94056100", + "94056900", + "94059100", + "94059200", + "94059900", + "94061010", + "94061090", + "94062000", + "94069010", + "94069020", + "94069090", + "95030010", + "95030021", + "95030022", + "95030029", + "95030031", + "95030039", + "95030040", + "95030050", + "95030060", + "95030070", + "95030080", + "95030091", + "95030097", + "95030098", + "95030099", + "95042000", + "95043000", + "95044000", + "95045000", + "95049010", + "95049090", + "95051000", + "95059000", + "95061100", + "95061200", + "95061900", + "95062100", + "95062900", + "95063100", + "95063200", + "95063900", + "95064000", + "95065100", + "95065900", + "95066100", + "95066200", + "95066900", + "95067000", + "95069100", + "95069900", + "95071000", + "95072000", + "95073000", + "95079000", + "95081000", + "95082110", + "95082120", + "95082190", + "95082210", + "95082290", + "95082300", + "95082400", + "95082500", + "95082600", + "95082900", + "95083000", + "95084000", + "96011000", + "96019000", + "96020010", + "96020020", + "96020090", + "96031000", + "96032100", + "96032900", + "96033000", + "96034010", + "96034090", + "96035000", + "96039000", + "96040000", + "96050000", + "96061000", + "96062100", + "96062200", + "96062900", + "96063000", + "96071100", + "96071900", + "96072000", + "96081000", + "96082000", + "96083000", + "96084000", + "96085000", + "96086000", + "96089100", + "96089981", + "96089989", + "96089990", + "96091000", + "96092000", + "96099000", + "96100000", + "96110000", + "96121000", + "96122000", + "96131000", + "96132000", + "96138000", + "96139000", + "96140000", + "96151100", + "96151900", + "96159000", + "96161000", + "96162000", + "96170010", + "96170020", + "96180000", + "96190000", + "96200000", + "97012100", + "97012200", + "97012900", + "97019100", + "97019200", + "97019900", + "97021000", + "97029000", + "97031000", + "97039000", + "97040000", + "97051000", + "97052100", + "97052200", + "97052900", + "97053100", + "97053900", + "97061000", + "97069000", +]; diff --git a/src/is-valid-ncm/is-valid-ncm.test.ts b/src/is-valid-ncm/is-valid-ncm.test.ts new file mode 100644 index 00000000..e343ae4e --- /dev/null +++ b/src/is-valid-ncm/is-valid-ncm.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidNcm } from "./is-valid-ncm"; + +describe("isValidNcm", () => { + it("should validate an NCM code without a mask (cerveja de malte)", () => { + expect(isValidNcm("22030000")).toBe(true); + }); + + it("should validate an NCM code with the dotted mask", () => { + expect(isValidNcm("2203.00.00")).toBe(true); + }); + + it("should validate an NCM code given as a number", () => { + expect(isValidNcm(22030000)).toBe(true); + }); + + it("should validate a leading zero NCM code (cavalos reprodutores de raça pura)", () => { + expect(isValidNcm("01012100")).toBe(true); + expect(isValidNcm("0101.21.00")).toBe(true); + }); + + it("should return false for a number that lost a leading zero (1012100 is not 01012100)", () => { + expect(isValidNcm(1012100)).toBe(false); + }); + + it("should validate an NCM code with surrounding whitespace", () => { + expect(isValidNcm(" 22030000 ")).toBe(true); + }); + + it("should return false for an unknown 8 digit code", () => { + expect(isValidNcm("12345678")).toBe(false); + }); + + it("should return false when the digit count is not eight", () => { + expect(isValidNcm("2203000")).toBe(false); + expect(isValidNcm("220300000")).toBe(false); + }); + + it("should return false for an empty string", () => { + expect(isValidNcm("")).toBe(false); + }); + + it("should return false for null and undefined", () => { + // @ts-expect-error not a string or number + expect(isValidNcm(null)).toBe(false); + // @ts-expect-error not a string or number + expect(isValidNcm(undefined)).toBe(false); + }); + + it("should return false for whitespace only", () => { + expect(isValidNcm(" ")).toBe(false); + }); + + it("should return false for a non numeric string", () => { + expect(isValidNcm("abcdefgh")).toBe(false); + }); +}); diff --git a/src/is-valid-ncm/is-valid-ncm.ts b/src/is-valid-ncm/is-valid-ncm.ts new file mode 100644 index 00000000..d113da47 --- /dev/null +++ b/src/is-valid-ncm/is-valid-ncm.ts @@ -0,0 +1,38 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { NCM_CODES } from "./constants"; + +let cache: Set | undefined; + +const getCache = (): Set => { + cache ??= new Set(NCM_CODES); + return cache; +}; + +/** + * Validates if a NCM (Nomenclatura Comum do Mercosul) code exists in the official table. + * + * A bare `number` input cannot represent a code that starts with `0` (the leading zero is + * lost), so a numeric NCM code starting with `0` must be passed as a string to validate + * correctly. + * + * @param {string|number} value - The NCM code to be validated, with or without the + * `NNNN.NN.NN` mask. + * @returns {boolean} True when the code is a known 8 digit NCM code, false otherwise. + * + * @example + * ```typescript + * isValidNcm("0101.21.00"); // true + * isValidNcm("01012100"); // true + * isValidNcm("00000000"); // false + * ``` + * + * @see Official: https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json + */ +export const isValidNcm = (value: string | number): boolean => { + if (isNullish(value) || value === "") return false; + + const digits = sanitizeToDigits(value); + + return digits.length === 8 && getCache().has(digits); +}; From bc03e52df603bc691c45b7aa4bd139e70dc4c85b Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:55 -0300 Subject: [PATCH 19/22] feat(cfop): add getCfop and isValidCfop CFOP (SINIEF) lookup/validation against a table with no check digit, generated by scripts/cfop.ts. --- scripts/cfop.ts | 62 ++ src/_internals/constants/cfop.ts | 763 ++++++++++++++++++++++++ src/get-cfop/get-cfop.test.ts | 53 ++ src/get-cfop/get-cfop.ts | 40 ++ src/is-valid-cfop/is-valid-cfop.test.ts | 51 ++ src/is-valid-cfop/is-valid-cfop.ts | 29 + 6 files changed, 998 insertions(+) create mode 100644 scripts/cfop.ts create mode 100644 src/_internals/constants/cfop.ts create mode 100644 src/get-cfop/get-cfop.test.ts create mode 100644 src/get-cfop/get-cfop.ts create mode 100644 src/is-valid-cfop/is-valid-cfop.test.ts create mode 100644 src/is-valid-cfop/is-valid-cfop.ts diff --git a/scripts/cfop.ts b/scripts/cfop.ts new file mode 100644 index 00000000..2e2da9c6 --- /dev/null +++ b/scripts/cfop.ts @@ -0,0 +1,62 @@ +#!/usr/bin/env node + +import { writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; + +const scriptsDir = dirname(fileURLToPath(import.meta.url)); + +const main = async () => { + const response = await fetchWithRetry( + "https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv", + ); + + if (!response.ok) { + throw new Error(`CFOP mirror request failed with status ${response.status}`); + } + + const csv = await response.text(); + + const data: Record = {}; + + for (const line of csv.split("\n")) { + const match = line.match(/^(\d{4});"(.*)"\s*$/); + + if (!match) continue; + + const [, code, description] = match; + + if (code.endsWith("00")) continue; + + data[code] = description.trim(); + } + + const sorted: Record = {}; + for (const code of Object.keys(data).sort()) { + sorted[code] = data[code]; + } + + await writeFile( + resolve(scriptsDir, "..", "./src/_internals/constants/cfop.ts"), + `/** + * CFOP (Código Fiscal de Operações e Prestações) table, indexed by the 4 digit code. + * + * Group and subgroup headers (codes ending in "00", e.g. "1000", "1100") are section + * titles from the official nomenclature rather than operable codes, so they are excluded. + * + * Generated by \`node ./scripts/cfop.ts\`. Do not edit by hand. + * + * @see https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv + * @see https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 + */ +export const CFOP_TABLE: Record = ${JSON.stringify(sorted)}; +`, + ); +}; + +await main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +}); diff --git a/src/_internals/constants/cfop.ts b/src/_internals/constants/cfop.ts new file mode 100644 index 00000000..93514477 --- /dev/null +++ b/src/_internals/constants/cfop.ts @@ -0,0 +1,763 @@ +/** + * CFOP (Código Fiscal de Operações e Prestações) table, indexed by the 4 digit code. + * + * Group and subgroup headers (codes ending in "00", e.g. "1000", "1100") are section + * titles from the official nomenclature rather than operable codes, so they are excluded. + * + * Generated by `node ./scripts/cfop.ts`. Do not edit by hand. + * + * @see https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv + * @see https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 + */ +export const CFOP_TABLE: Record = { + "1101": "Compra para industrialização ou produção rural", + "1102": "Compra para comercialização", + "1111": + "Compra para industrialização de mercadoria recebida anteriormente em consignação industrial", + "1113": + "Compra para comercialização, de mercadoria recebida anteriormente em consignação mercantil", + "1116": + "Compra para industrialização ou produção rural originada de encomenda para recebimento futuro", + "1117": "Compra para comercialização originada de encomenda para recebimento futuro", + "1118": + "Compra de mercadoria para comercialização pelo adquirente originário, entregue pelo vendedor remetente ao destinatário, em venda à ordem", + "1120": "Compra para industrialização, em venda à ordem, já recebida do vendedor remetente", + "1121": "Compra para comercialização, em venda à ordem, já recebida do vendedor remetente", + "1122": + "Compra para industrialização em que a mercadoria foi remetida pelo fornecedor ao industrializador sem transitar pelo estabelecimento adquirente", + "1124": "Industrialização efetuada por outra empresa", + "1125": + "Industrialização efetuada por outra empresa quando a mercadoria remetida para utilização no processo de industrialização não transitou pelo estabelecimento adquirente da mercadoria", + "1126": "compras para utilização na prestação de serviços sujeitas ao ICMS", + "1128": "compras para utilização na prestação de serviços sujeitas ao ISSQN", + "1150": "TRANSFERÊNCIAS PARA INDUSTRIALIZAÇÃO, COMERCIALIZAÇÃO OU PRESTAÇÃO DE SERVIÇOS", + "1151": "Transferência para industrialização ou produção rural", + "1152": "Transferência para comercialização", + "1153": "Transferência de energia elétrica para distribuição", + "1154": "Transferência para utilização na prestação de serviço", + "1201": "Devolução de venda de produção do estabelecimento", + "1202": "Devolução de venda de mercadoria adquirida ou recebida de terceiros", + "1203": + "Devolução de venda de produção do estabelecimento, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "1204": + "Devolução de venda de mercadoria adquirida ou recebida de terceiros, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "1205": "Anulação de valor relativo à prestação de serviço de comunicação", + "1206": "Anulação de valor relativo à prestação de serviço de transporte", + "1207": "Anulação de valor relativo à venda de energia elétrica", + "1208": "Devolução de produção do estabelecimento, remetida em transferência", + "1209": "Devolução de mercadoria adquirida ou recebida de terceiros, remetida em transferência", + "1250": "COMPRAS DE ENERGIA ELÉTRICA", + "1251": "Compra de energia elétrica para distribuição ou comercialização", + "1252": "Compra de energia elétrica por estabelecimento industrial", + "1253": "Compra de energia elétrica por estabelecimento comercial", + "1254": "Compra de energia elétrica por estabelecimento prestador de serviço de transporte", + "1255": "Compra de energia elétrica por estabelecimento prestador de serviço de comunicação", + "1256": "Compra de energia elétrica por estabelecimento de produtor rural", + "1257": "Compra de energia elétrica para consumo por demanda contratada", + "1301": "Aquisição de serviço de comunicação para execução de serviço da mesma natureza", + "1302": "Aquisição de serviço de comunicação por estabelecimento industrial", + "1303": "Aquisição de serviço de comunicação por estabelecimento comercial", + "1304": + "Aquisição de serviço de comunicação por estabelecimento de prestador de serviço de transporte", + "1305": + "Aquisição de serviço de comunicação por estabelecimento de geradora ou de distribuidora de energia elétrica 1.306 - Aquisição de serviço de comunicação por estabelecimento de produtor rural", + "1350": "AQUISIÇÕES DE SERVIÇOS DE TRANSPORTE", + "1351": "Aquisição de serviço de transporte para execução de serviço da mesma natureza", + "1352": "Aquisição de serviço de transporte por estabelecimento industrial", + "1353": "Aquisição de serviço de transporte por estabelecimento comercial", + "1354": + "Aquisição de serviço de transporte por estabelecimento de prestador de serviço de comunicação", + "1355": + "Aquisição de serviço de transporte por estabelecimento de geradora ou de distribuidora de energia elétrica", + "1356": "Aquisição de serviço de transporte por estabelecimento de produtor rural", + "1360": + "Aquisição de serviço de transporte por contribuinte substituto em relação ao serviço de transporte", + "1401": + "Compra para industrialização ou produção rural em operação com mercadoria sujeita ao regime de substituição tributária", + "1403": + "Compra para comercialização em operação com mercadoria sujeita ao regime de substituição tributária", + "1406": + "Compra de bem para o ativo imobilizado cuja mercadoria está sujeita ao regime de substituição tributária", + "1407": + "Compra de mercadoria para uso ou consumo cuja mercadoria está sujeita ao regime de substituição tributária", + "1408": + "Transferência para industrialização ou produção rural em operação com mercadoria sujeita ao regime de substituição tributária", + "1409": + "Transferência para comercialização em operação com mercadoria sujeita ao regime de substituição tributária", + "1410": + "Devolução de venda de produção do estabelecimento em operação com produto sujeito ao regime de substituição tributária", + "1411": + "Devolução de venda de mercadoria adquirida ou recebida de terceiros em operação com mercadoria sujeita ao regime de substituição tributária 1.414 - Retorno de produção do estabelecimento, remetida para venda fora do estabelecimento em operação com produto sujeito ao regime de substituição tributária", + "1415": + "Retorno de mercadoria adquirida ou recebida de terceiros, remetida para venda fora do estabelecimento em operação com mercadoria sujeita ao regime de substituição tributária", + "1450": "SISTEMAS DE INTEGRAÇÃO", + "1451": "Retorno de animal do estabelecimento produtor", + "1452": "Retorno de insumo não utilizado na produção", + "1501": "Entrada de mercadoria recebida com fim específico de exportação", + "1503": + "Entrada decorrente de devolução de produto remetido com fim específico de exportação, de produção do estabelecimento", + "1504": + "Entrada decorrente de devolução de mercadoria remetida com fim específico de exportação, adquirida ou recebida de terceiros", + "1505": + "Entrada decorrente de devolução simbólica de mercadorias remetidas para formação de lote de exportação, de produtos industrializados ou produzidos pelo próprio estabelecimento", + "1506": + "Entrada decorrente de devolução simbólica de mercadorias, adquiridas ou recebidas de terceiros, remetidas para formação de lote de exportação", + "1550": "OPERAÇÕES COM BENS DE ATIVO IMOBILIZADO E MATERIAIS PARA USO OU CONSUMO", + "1551": "Compra de bem para o ativo imobilizado", + "1552": "Transferência de bem do ativo imobilizado", + "1553": "Devolução de venda de bem do ativo imobilizado", + "1554": "Retorno de bem do ativo imobilizado remetido para uso fora do estabelecimento", + "1555": "Entrada de bem do ativo imobilizado de terceiro, remetido para uso no estabelecimento", + "1556": "Compra de material para uso ou consumo", + "1557": "Transferência de material para uso ou consumo", + "1601": "Recebimento, por transferência, de crédito de ICMS", + "1602": + "Recebimento, por transferência, de saldo credor de ICMS de outro estabelecimento da mesma empresa, para compensação de saldo devedor de ICMS", + "1603": + "Ressarcimento de ICMS retido por substituição tributária 1.604 - Lançamento do crédito relativo à compra de bem para o ativo imobilizado", + "1605": + "Recebimento, por transferência, de saldo devedor de ICMS de outro estabelecimento da mesma empresa", + "1652": "Compra de combustível ou lubrificante para comercialização", + "1653": "Compra de combustível ou lubrificante por consumidor ou usuário final", + "1658": "Transferência de combustível e lubrificante para industrialização", + "1659": "Transferência de combustível e lubrificante para comercialização", + "1660": + "Devolução de venda de combustível ou lubrificante destinado à industrialização subseqüente", + "1661": "Devolução de venda de combustível ou lubrificante destinado à comercialização", + "1662": + "Devolução de venda de combustível ou lubrificante destinado a consumidor ou usuário final", + "1663": "Entrada de combustível ou lubrificante para armazenagem", + "1664": "Retorno de combustível ou lubrificante remetido para armazenagem", + "1901": "Entrada para industrialização por encomenda", + "1902": "Retorno de mercadoria remetida para industrialização por encomenda", + "1903": + "Entrada de mercadoria remetida para industrialização e não aplicada no referido processo", + "1904": "Retorno de remessa para venda fora do estabelecimento", + "1905": "Entrada de mercadoria recebida para depósito em depósito fechado ou armazém geral", + "1906": "Retorno de mercadoria remetida para depósito fechado ou armazém geral", + "1907": "Retorno simbólico de mercadoria remetida para depósito fechado ou armazém geral", + "1908": "Entrada de bem por conta de contrato de comodato", + "1909": "Retorno de bem remetido por conta de contrato de comodato", + "1910": "Entrada de bonificação, doação ou brinde", + "1911": "Entrada de amostra grátis", + "1912": "Entrada de mercadoria ou bem recebido para demonstração", + "1913": "Retorno de mercadoria ou bem remetido para demonstração", + "1914": "Retorno de mercadoria ou bem remetido para exposição ou feira", + "1915": "Entrada de mercadoria ou bem recebido para conserto ou reparo", + "1916": "Retorno de mercadoria ou bem remetido para conserto ou reparo", + "1917": "Entrada de mercadoria recebida em consignação mercantil ou industrial", + "1918": "Devolução de mercadoria remetida em consignação mercantil ou industrial", + "1919": + "Devolução simbólica de mercadoria vendida ou utilizada em processo industrial, remetida anteriormente em consignação mercantil ou industrial", + "1920": "Entrada de vasilhame ou sacaria", + "1921": "Retorno de vasilhame ou sacaria", + "1922": + "Lançamento efetuado a título de simples faturamento decorrente de compra para recebimento futuro", + "1923": "Entrada de mercadoria recebida do vendedor remetente, em venda à ordem", + "1924": + "Entrada para industrialização por conta e ordem do adquirente da mercadoria, quando esta não transitar pelo estabelecimento do adquirente", + "1925": + "Retorno de mercadoria remetida para industrialização por conta e ordem do adquirente da mercadoria, quando esta não transitar pelo estabelecimento do adquirente", + "1926": + "Lançamento efetuado a título de reclassificação de mercadoria decorrente de formação de kit ou de sua desagregação", + "1931": + "Lançamento efetuado pelo tomador do serviço de transporte quando a responsabilidade de retenção do imposto for atribuída ao remetente ou alienante da mercadoria, pelo serviço de transporte realizado por transportador autônomo ou por transportador não inscrito na unidade da Federação onde iniciado o serviço", + "1932": + "Aquisição de serviço de transporte iniciado em unidade da Federação diversa daquela onde inscrito o prestador", + "1933": "Aquisição de serviço tributado pelo ISSQN", + "1949": "Outra entrada de mercadoria ou prestação de serviço não especificada", + "2101": "Compra para industrialização ou produção rural", + "2102": "Compra para comercialização", + "2111": + "Compra para industrialização de mercadoria recebida anteriormente em consignação industrial", + "2113": + "Compra para comercialização, de mercadoria recebida anteriormente em consignação mercantil", + "2116": + "Compra para industrialização ou produção rural originada de encomenda para recebimento futuro", + "2117": "Compra para comercialização originada de encomenda para recebimento futuro", + "2118": + "Compra de mercadoria para comercialização pelo adquirente originário, entregue pelo vendedor remetente ao destinatário, em venda à ordem", + "2120": + "Compra para industrialização, em venda à ordem, já recebida do vendedor remetente 2.121 - Compra para comercialização, em venda à ordem, já recebida do vendedor remetente", + "2122": + "Compra para industrialização em que a mercadoria foi remetida pelo fornecedor ao industrializador sem transitar pelo estabelecimento adquirente", + "2124": "Industrialização efetuada por outra empresa", + "2125": + "Industrialização efetuada por outra empresa quando a mercadoria remetida para utilização no processo de industrialização não transitou pelo estabelecimento adquirente da mercadoria", + "2126": "Compra para utilização na prestação de serviço", + "2150": "TRANSFERÊNCIAS PARA INDUSTRIALIZAÇÃO, COMERCIALIZAÇÃO OU PRESTAÇÃO DE SERVIÇOS", + "2151": "Transferência para industrialização ou produção rural", + "2152": "Transferência para comercialização", + "2153": "Transferência de energia elétrica para distribuição", + "2154": "Transferência para utilização na prestação de serviço", + "2201": "Devolução de venda de produção do estabelecimento", + "2202": "Devolução de venda de mercadoria adquirida ou recebida de terceiros", + "2203": + "Devolução de venda de produção do estabelecimento, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "2204": + "Devolução de venda de mercadoria adquirida ou recebida de terceiros, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "2205": "Anulação de valor relativo à prestação de serviço de comunicação", + "2206": "Anulação de valor relativo à prestação de serviço de transporte", + "2207": "Anulação de valor relativo à venda de energia elétrica", + "2208": "Devolução de produção do estabelecimento, remetida em transferência", + "2209": "Devolução de mercadoria adquirida ou recebida de terceiros, remetida em transferência", + "2250": "COMPRAS DE ENERGIA ELÉTRICA", + "2251": "Compra de energia elétrica para distribuição ou comercialização", + "2252": "Compra de energia elétrica por estabelecimento industrial", + "2253": "Compra de energia elétrica por estabelecimento comercial", + "2254": "Compra de energia elétrica por estabelecimento prestador de serviço de transporte", + "2255": "Compra de energia elétrica por estabelecimento prestador de serviço de comunicação", + "2256": "Compra de energia elétrica por estabelecimento de produtor rural", + "2257": "Compra de energia elétrica para consumo por demanda contratada", + "2301": "Aquisição de serviço de comunicação para execução de serviço da mesma natureza", + "2302": "Aquisição de serviço de comunicação por estabelecimento industrial", + "2303": "Aquisição de serviço de comunicação por estabelecimento comercial", + "2304": + "Aquisição de serviço de comunicação por estabelecimento de prestador de serviço de transporte", + "2305": + "Aquisição de serviço de comunicação por estabelecimento de geradora ou de distribuidora de energia elétrica", + "2306": "Aquisição de serviço de comunicação por estabelecimento de produtor rural", + "2351": "Aquisição de serviço de transporte para execução de serviço da mesma natureza", + "2352": "Aquisição de serviço de transporte por estabelecimento industrial", + "2353": "Aquisição de serviço de transporte por estabelecimento comercial", + "2354": + "Aquisição de serviço de transporte por estabelecimento de prestador de serviço de comunicação", + "2355": + "Aquisição de serviço de transporte por estabelecimento de geradora ou de distribuidora de energia elétrica", + "2356": "Aquisição de serviço de transporte por estabelecimento de produtor rural", + "2401": + "Compra para industrialização ou produção rural em operação com mercadoria sujeita ao regime desubstituição tributária", + "2403": + "Compra para comercialização em operação com mercadoria sujeita ao regime de substituição tributária", + "2406": + "Compra de bem para o ativo imobilizado cuja mercadoria está sujeita ao regime de substituição tributária", + "2407": + "Compra de mercadoria para uso ou consumo cuja mercadoria está sujeita ao regime de substituição tributária", + "2408": + "Transferência para industrialização ou produção rural em operação com mercadoria sujeita ao regime de substituição tributária", + "2409": + "Transferência para comercialização em operação com mercadoria sujeita ao regime de substituição tributária", + "2410": + "Devolução de venda de produção do estabelecimento em operação com produto sujeito ao regime desubstituição tributária", + "2411": + "Devolução de venda de mercadoria adquirida ou recebida de terceiros em operação com mercadoria sujeita ao regime de substituição tributária", + "2414": + "Retorno de produção do estabelecimento, remetida para venda fora do estabelecimento em operação com produto sujeito ao regime de substituição tributária", + "2415": + "Retorno de mercadoria adquirida ou recebida de terceiros, remetida para venda fora do estabelecimento em operação com mercadoria sujeita ao regime de substituição tributária", + "2501": "Entrada de mercadoria recebida com fim específico de exportação", + "2503": + "Entrada decorrente de devolução de produto remetido com fim específico de exportação, de produção do estabelecimento", + "2504": + "Entrada decorrente de devolução de mercadoria remetida com fim específico de exportação, adquirida ou recebida de terceiros", + "2505": + "Entrada decorrente de devolução simbólica de mercadorias remetidas para formação de lote de exportação, de produtos industrializados ou produzidos pelo próprio estabelecimento", + "2506": + "Entrada decorrente de devolução simbólica de mercadorias, adquiridas ou recebidas de terceiros, remetidas para formação de lote de exportação", + "2550": "OPERAÇÕES COM BENS DE ATIVO IMOBILIZADO E MATERIAIS PARA USO OU CONSUMO", + "2551": "Compra de bem para o ativo imobilizado", + "2552": "Transferência de bem do ativo imobilizado", + "2553": "Devolução de venda de bem do ativo imobilizado", + "2554": "Retorno de bem do ativo imobilizado remetido para uso fora do estabelecimento", + "2555": "Entrada de bem do ativo imobilizado de terceiro, remetido para uso no estabelecimento", + "2556": "Compra de material para uso ou consumo", + "2557": "Transferência de material para uso ou consumo", + "2603": "Ressarcimento de ICMS retido por substituição tributária", + "2651": "Compra de combustível ou lubrificante para industrialização subseqüente", + "2652": "Compra de combustível ou lubrificante para comercialização", + "2653": "Compra de combustível ou lubrificante por consumidor ou usuário final", + "2658": "Transferência de combustível e lubrificante para industrialização", + "2659": "Transferência de combustível e lubrificante para comercialização", + "2660": + "Devolução de venda de combustível ou lubrificante destinado à industrialização subseqüente", + "2661": "Devolução de venda de combustível ou lubrificante destinado à comercialização", + "2662": + "Devolução de venda de combustível ou lubrificante destinado a consumidor ou usuário final", + "2663": "Entrada de combustível ou lubrificante para armazenagem", + "2664": "Retorno de combustível ou lubrificante remetido para armazenagem", + "2901": "Entrada para industrialização por encomenda", + "2902": "Retorno de mercadoria remetida para industrialização por encomenda", + "2903": + "Entrada de mercadoria remetida para industrialização e não aplicada no referido processo", + "2904": "Retorno de remessa para venda fora do estabelecimento", + "2905": "Entrada de mercadoria recebida para depósito em depósito fechado ou armazém geral", + "2906": "Retorno de mercadoria remetida para depósito fechado ou armazém geral", + "2907": "Retorno simbólico de mercadoria remetida para depósito fechado ou armazém geral", + "2908": "Entrada de bem por conta de contrato de comodato", + "2909": "Retorno de bem remetido por conta de contrato de comodato", + "2910": "Entrada de bonificação, doação ou brinde", + "2911": "Entrada de amostra grátis", + "2912": "Entrada de mercadoria ou bem recebido para demonstração", + "2913": "Retorno de mercadoria ou bem remetido para demonstração", + "2914": "Retorno de mercadoria ou bem remetido para exposição ou feira", + "2915": "Entrada de mercadoria ou bem recebido para conserto ou reparo", + "2916": "Retorno de mercadoria ou bem remetido para conserto ou reparo", + "2917": "Entrada de mercadoria recebida em consignação mercantil ou industrial", + "2918": "Devolução de mercadoria remetida em consignação mercantil ou industrial", + "2919": + "Devolução simbólica de mercadoria vendida ou utilizada em processo industrial, remetida anteriormente em consignação mercantil ou industrial", + "2920": "Entrada de vasilhame ou sacaria", + "2921": "Retorno de vasilhame ou sacaria", + "2922": + "Lançamento efetuado a título de simples faturamento decorrente de compra para recebimento futuro", + "2923": "Entrada de mercadoria recebida do vendedor remetente, em venda à ordem", + "2924": + "Entrada para industrialização por conta e ordem do adquirente da mercadoria, quando esta não transitar pelo estabelecimento do adquirente", + "2925": + "Retorno de mercadoria remetida para industrialização por conta e ordem do adquirente da mercadoria, quando esta não transitar pelo estabelecimento do adquirente", + "2931": + "Lançamento efetuado pelo tomador do serviço de transporte quando a responsabilidade de retenção do imposto for atribuída ao remetente ou alienante da mercadoria, pelo serviço de transporte realizado por transportador autônomo ou por transportador não inscrito na unidade da Federação onde iniciado o serviço", + "2932": + "Aquisição de serviço de transporte iniciado em unidade da Federação diversa daquela onde inscrito o prestador", + "2933": "Aquisição de serviço tributado pelo ISSQN", + "2949": "Outra entrada de mercadoria ou prestação de serviço não especificado", + "3101": "Compra para industrialização ou produção rural", + "3102": "Compra para comercialização", + "3126": "Compra para utilização na prestação de serviço", + "3127": 'Compra para industrialização sob o regime de "drawback"', + "3201": "Devolução de venda de produção do estabelecimento", + "3202": "Devolução de venda de mercadoria adquirida ou recebida de terceiros", + "3205": "Anulação de valor relativo à prestação de serviço de comunicação", + "3206": "Anulação de valor relativo à prestação de serviço de transporte", + "3207": "Anulação de valor relativo à venda de energia elétrica", + "3211": 'Devolução de venda de produção do estabelecimento sob o regime de "drawback"', + "3250": "COMPRAS DE ENERGIA ELÉTRICA", + "3251": "Compra de energia elétrica para distribuição ou comercialização", + "3301": "Aquisição de serviço de comunicação para execução de serviço da mesma natureza", + "3350": "AQUISIÇÕES DE SERVIÇOS DE TRANSPORTE", + "3351": "Aquisição de serviço de transporte para execução de serviço da mesma natureza", + "3352": "Aquisição de serviço de transporte por estabelecimento industrial", + "3353": "Aquisição de serviço de transporte por estabelecimento comercial", + "3354": + "Aquisição de serviço de transporte por estabelecimento de prestador de serviço de comunicação", + "3355": + "Aquisição de serviço de transporte por estabelecimento de geradora ou de distribuidora de energia elétrica", + "3356": "Aquisição de serviço de transporte por estabelecimento de produtor rural", + "3503": + "Devolução de mercadoria exportada que tenha sido recebida com fim específico de exportação", + "3550": "OPERAÇÕES COM BENS DE ATIVO IMOBILIZADO E MATERIAIS PARA USO OU CONSUMO", + "3551": "Compra de bem para o ativo imobilizado", + "3553": "Devolução de venda de bem do ativo imobilizado", + "3556": "Compra de material para uso ou consumo", + "3651": "Compra de combustível ou lubrificante para industrialização subseqüente", + "3652": "Compra de combustível ou lubrificante para comercialização", + "3653": "Compra de combustível ou lubrificante por consumidor ou usuário final", + "3930": + "Lançamento efetuado a título de entrada de bem sob amparo de regime especial aduaneiro de admissão temporária", + "3949": "Outra entrada de mercadoria ou prestação de serviço não especificado", + "5101": "Venda de produção do estabelecimento", + "5102": "Venda de mercadoria adquirida ou recebida de terceiros", + "5103": "Venda de produção do estabelecimento, efetuada fora do estabelecimento", + "5104": + "Venda de mercadoria adquirida ou recebida de terceiros, efetuada fora do estabelecimento", + "5105": "Venda de produção do estabelecimento que não deva por ele transitar", + "5106": "Venda de mercadoria adquirida ou recebida de terceiros, que não deva por ele transitar", + "5109": + "Venda de produção do estabelecimento, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "5110": + "Venda de mercadoria adquirida ou recebida de terceiros, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "5111": "Venda de produção do estabelecimento remetida anteriormente em consignação industrial", + "5112": + "Venda de mercadoria adquirida ou recebida de terceiros remetida anteriormente em consignação industrial", + "5113": "Venda de produção do estabelecimento remetida anteriormente em consignação mercantil", + "5114": + "Venda de mercadoria adquirida ou recebida de terceiros remetida anteriormente em consignação mercantil", + "5115": + "Venda de mercadoria adquirida ou recebida de terceiros, recebida anteriormente em consignação mercantil", + "5116": "Venda de produção do estabelecimento originada de encomenda para entrega futura", + "5117": + "Venda de mercadoria adquirida ou recebida de terceiros, originada de encomenda para entrega futura", + "5118": + "Venda de produção do estabelecimento entregue ao destinatário por conta e ordem do adquirente originário, em venda à ordem", + "5119": + "Venda de mercadoria adquirida ou recebida de terceiros entregue ao destinatário por conta e ordem do adquirente originário, em venda à ordem", + "5120": + "Venda de mercadoria adquirida ou recebida de terceiros entregue ao destinatário pelo vendedor remetente, em venda à ordem", + "5122": + "Venda de produção do estabelecimento remetida para industrialização, por conta e ordem do adquirente, sem transitar pelo estabelecimento do adquirente", + "5123": + "Venda de mercadoria adquirida ou recebida de terceiros remetida para industrialização, por conta e ordem do adquirente, sem transitar pelo estabelecimento do adquirente", + "5124": "Industrialização efetuada para outra empresa", + "5125": + "Industrialização efetuada para outra empresa quando a mercadoria recebida para utilização no processo de industrialização não transitar pelo estabelecimento adquirente da mercadoria", + "5150": "TRANSFERÊNCIAS DE PRODUÇÃO PRÓPRIA OU DE TERCEIROS", + "5151": "Transferência de produção do estabelecimento", + "5152": "Transferência de mercadoria adquirida ou recebida de terceiros", + "5153": "Transferência de energia elétrica", + "5155": "Transferência de produção do estabelecimento, que não deva por ele transitar", + "5156": + "Transferência de mercadoria adquirida ou recebida de terceiros, que não deva por ele transitar", + "5201": "Devolução de compra para industrialização ou produção rural", + "5202": "Devolução de compra para comercialização", + "5205": "Anulação de valor relativo a aquisição de serviço de comunicação", + "5206": "Anulação de valor relativo a aquisição de serviço de transporte", + "5207": "Anulação de valor relativo à compra de energia elétrica", + "5208": + "Devolução de mercadoria recebida em transferência para industrialização ou produção rural", + "5209": "Devolução de mercadoria recebida em transferência para comercialização", + "5210": "Devolução de compra para utilização na prestação de serviço sujeitas ao ICMS ou ISSQN", + "5250": "VENDAS DE ENERGIA ELÉTRICA", + "5251": "Venda de energia elétrica para distribuição ou comercialização", + "5252": "Venda de energia elétrica para estabelecimento industrial", + "5253": "Venda de energia elétrica para estabelecimento comercial", + "5254": "Venda de energia elétrica para estabelecimento prestador de serviço de transporte", + "5255": "Venda de energia elétrica para estabelecimento prestador de serviço de comunicação", + "5256": "Venda de energia elétrica para estabelecimento de produtor rural", + "5257": "Venda de energia elétrica para consumo por demanda contratada", + "5258": "Venda de energia elétrica a não contribuinte", + "5301": "Prestação de serviço de comunicação para execução de serviço da mesma natureza", + "5302": "Prestação de serviço de comunicação a estabelecimento industrial", + "5303": "Prestação de serviço de comunicação a estabelecimento comercial", + "5304": + "Prestação de serviço de comunicação a estabelecimento de prestador de serviço de transporte", + "5305": + "Prestação de serviço de comunicação a estabelecimento de geradora ou de distribuidora de energia elétrica", + "5306": "Prestação de serviço de comunicação a estabelecimento de produtor rural", + "5307": "Prestação de serviço de comunicação a não contribuinte", + "5350": "PRESTAÇÕES DE SERVIÇOS DE TRANSPORTE", + "5351": "Prestação de serviço de transporte para execução de serviço da mesma natureza", + "5352": "Prestação de serviço de transporte a estabelecimento industrial", + "5353": "Prestação de serviço de transporte a estabelecimento comercial", + "5354": + "Prestação de serviço de transporte a estabelecimento de prestador de serviço de comunicação", + "5355": + "Prestação de serviço de transporte a estabelecimento de geradora ou de distribuidora de energia elétrica", + "5356": "Prestação de serviço de transporte a estabelecimento de produtor rural", + "5357": "Prestação de serviço de transporte a não contribuinte", + "5359": + "Prestação de serviço de transporte a contribuinte ou a não contribuinte quando a mercadoria transportada está dispensada de emissão de nota fiscal", + "5360": + "Prestação de serviço de transporte a contribuinte substituto em relação ao serviço de transporte", + "5401": + "Venda de produção do estabelecimento em operação com produto sujeito ao regime de substituição tributária, na condição de contribuinte substituto", + "5402": + "Venda de produção do estabelecimento de produto sujeito ao regime de substituição tributária, em operação entre contribuintes substitutos do mesmo produto", + "5403": + "Venda de mercadoria adquirida ou recebida de terceiros em operação com mercadoria sujeita ao regime de substituição tributária, na condição de contribuinte substituto", + "5405": + "Venda de mercadoria adquirida ou recebida de terceiros em operação com mercadoria sujeita ao regime de substituição tributária, na condição de contribuinte substituído", + "5408": + "Transferência de produção do estabelecimento em operação com produto sujeito ao regime de substituição tributária", + "5409": + "Transferência de mercadoria adquirida ou recebida de terceiros em operação com mercadoria sujeita ao regime de substituição tributária", + "5410": + "Devolução de compra para industrialização ou produção rural em operação com mercadoria sujeita ao regime de substituição tributária", + "5411": + "Devolução de compra para comercialização em operação com mercadoria sujeita ao regime de substituição tributária", + "5412": + "Devolução de bem do ativo imobilizado, em operação com mercadoria sujeita ao regime de substituição tributária", + "5413": + "Devolução de mercadoria destinada ao uso ou consumo, em operação com mercadoria sujeita ao regime de substituição tributária", + "5414": + "Remessa de produção do estabelecimento para venda fora do estabelecimento em operação com produto sujeito ao regime de substituição tributária", + "5415": + "Remessa de mercadoria adquirida ou recebida de terceiros para venda fora do estabelecimento, em operação com mercadoria sujeita ao regime de substituição tributária", + "5450": "SISTEMAS DE INTEGRAÇÃO", + "5451": "Remessa de animal e de insumo para estabelecimento produtor", + "5501": + "Remessa de produção do estabelecimento, com fim específico de exportação 5.502 - Remessa de mercadoria adquirida ou recebida de terceiros, com fim específico de exportação", + "5503": "Devolução de mercadoria recebida com fim específico de exportação", + "5504": + "Remessa de mercadorias para formação de lote de exportação, de produtos industrializados ou produzidos pelo próprio estabelecimento", + "5505": + "Remessa de mercadorias, adquiridas ou recebidas de terceiros, para formação de lote de exportação", + "5550": "OPERAÇÕES COM BENS DE ATIVO IMOBILIZADO E MATERIAIS PARA USO OU CONSUMO", + "5551": "Venda de bem do ativo imobilizado", + "5552": "Transferência de bem do ativo imobilizado", + "5553": "Devolução de compra de bem para o ativo imobilizado", + "5554": "Remessa de bem do ativo imobilizado para uso fora do estabelecimento", + "5555": "Devolução de bem do ativo imobilizado de terceiro, recebido para uso no estabelecimento", + "5556": "Devolução de compra de material de uso ou consumo", + "5557": "Transferência de material de uso ou consumo", + "5601": "Transferência de crédito de ICMS acumulado", + "5602": + "Transferência de saldo credor de ICMS para outro estabelecimento da mesma empresa, destinado à compensação de saldo devedor de ICMS", + "5603": "Ressarcimento de ICMS retido por substituição tributária", + "5605": "Transferência de saldo devedor de ICMS de outro estabelecimento da mesma empresa", + "5606": "Utilização de saldo credor de ICMS para extinção por compensação de débitos fiscais", + "5651": + "Venda de combustível ou lubrificante de produção do estabelecimento destinado à industrialização subseqüente", + "5652": + "Venda de combustível ou lubrificante de produção do estabelecimento destinado à comercialização", + "5653": + "Venda de combustível ou lubrificante de produção do estabelecimento destinado a consumidor ou usuário final", + "5654": + "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado à industrialização subseqüente", + "5655": + "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado à comercialização", + "5656": + "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado a consumidor ou usuário final", + "5657": + "Remessa de combustível ou lubrificante adquirido ou recebido de terceiros para venda fora do estabelecimento", + "5658": "Transferência de combustível ou lubrificante de produção do estabelecimento", + "5659": "Transferência de combustível ou lubrificante adquirido ou recebido de terceiro", + "5660": + "Devolução de compra de combustível ou lubrificante adquirido para industrialização subseqüente", + "5661": "Devolução de compra de combustível ou lubrificante adquirido para comercialização", + "5662": + "Devolução de compra de combustível ou lubrificante adquirido por consumidor ou usuário final", + "5663": "Remessa para armazenagem de combustível ou lubrificante", + "5664": "Retorno de combustível ou lubrificante recebido para armazenagem", + "5665": "Retorno simbólico de combustível ou lubrificante recebido para armazenagem", + "5666": + "Remessa por conta e ordem de terceiros de combustível ou lubrificante recebido para armazenagem", + "5901": "Remessa para industrialização por encomenda", + "5902": "Retorno de mercadoria utilizada na industrialização por encomenda", + "5903": + "Retorno de mercadoria recebida para industrialização e não aplicada no referido processo", + "5904": "Remessa para venda fora do estabelecimento", + "5905": "Remessa para depósito fechado ou armazém geral", + "5906": "Retorno de mercadoria depositada em depósito fechado ou armazém geral", + "5907": "Retorno simbólico de mercadoria depositada em depósito fechado ou armazém geral", + "5908": "Remessa de bem por conta de contrato de comodato", + "5909": "Retorno de bem recebido por conta de contrato de comodato", + "5910": "Remessa em bonificação, doação ou brinde", + "5911": "Remessa de amostra grátis", + "5912": "Remessa de mercadoria ou bem para demonstração", + "5913": "Retorno de mercadoria ou bem recebido para demonstração", + "5914": "Remessa de mercadoria ou bem para exposição ou feira", + "5915": "Remessa de mercadoria ou bem para conserto ou reparo", + "5916": "Retorno de mercadoria ou bem recebido para conserto ou reparo", + "5917": "Remessa de mercadoria em consignação mercantil ou industrial", + "5918": "Devolução de mercadoria recebida em consignação mercantil ou industrial", + "5919": + "Devolução simbólica de mercadoria vendida ou utilizada em processo industrial, recebida anteriormente em consignação mercantil ou industrial", + "5920": "Remessa de vasilhame ou sacaria", + "5921": "Devolução de vasilhame ou sacaria", + "5922": + "Lançamento efetuado a título de simples faturamento decorrente de venda para entrega futura", + "5923": "Remessa de mercadoria por conta e ordem de terceiros, em venda à ordem", + "5924": + "Remessa para industrialização por conta e ordem do adquirente da mercadoria, quando esta não transitar pelo estabelecimento do adquirente", + "5925": + "Retorno de mercadoria recebida para industrialização por conta e ordem do adquirente da mercadoria, quando aquela não transitar pelo estabelecimento do adquirente", + "5926": + "Lançamento efetuado a título de reclassificação de mercadoria decorrente de formação de kit ou de sua desagregação", + "5927": + "Lançamento efetuado a título de baixa de estoque decorrente de perda, roubo ou deterioração", + "5928": + "Lançamento efetuado a título de baixa de estoque decorrente do encerramento da atividade da empresa", + "5929": + "Lançamento efetuado em decorrência de emissão de documento fiscal relativo a operação ou prestação também registrada em equipamento Emissor de Cupom Fiscal - ECF", + "5931": + "Lançamento efetuado em decorrência da responsabilidade de retenção do imposto por substituição tributária, atribuída ao remetente ou alienante da mercadoria, pelo serviço de transporte realizado por transportador autônomo ou por transportador não inscrito na unidade da Federação onde iniciado o serviço", + "5932": + "Prestação de serviço de transporte iniciada em unidade da Federação diversa daquela onde inscrito o prestador", + "5933": "Prestação de serviço tributado pelo ISSQN", + "5949": "Outra saída de mercadoria ou prestação de serviço não especificado", + "6101": "Venda de produção do estabelecimento", + "6102": "Venda de mercadoria adquirida ou recebida de terceiros", + "6103": "Venda de produção do estabelecimento, efetuada fora do estabelecimento", + "6104": + "Venda de mercadoria adquirida ou recebida de terceiros, efetuada fora do estabelecimento", + "6105": "Venda de produção do estabelecimento que não deva por ele transitar", + "6106": "Venda de mercadoria adquirida ou recebida de terceiros, que não deva por ele transitar", + "6107": "Venda de produção do estabelecimento, destinada a não contribuinte", + "6108": "Venda de mercadoria adquirida ou recebida de terceiros, destinada a não contribuinte", + "6109": + "Venda de produção do estabelecimento, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "6110": + "Venda de mercadoria adquirida ou recebida de terceiros, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", + "6111": "Venda de produção do estabelecimento remetida anteriormente em consignação industrial", + "6112": + "Venda de mercadoria adquirida ou recebida de Terceiros remetida anteriormente em consignação industrial", + "6113": "Venda de produção do estabelecimento remetida anteriormente em consignação mercantil", + "6114": + "Venda de mercadoria adquirida ou recebida de terceiros remetida anteriormente em consignação mercantil", + "6115": + "Venda de mercadoria adquirida ou recebida de terceiros, recebida anteriormente em consignação mercantil", + "6116": "Venda de produção do estabelecimento originada de encomenda para entrega futura", + "6117": + "Venda de mercadoria adquirida ou recebida de terceiros, originada de encomenda para entrega futura", + "6118": + "Venda de produção do estabelecimento entregue ao destinatário por conta e ordem do adquirente originário, em venda à ordem", + "6119": + "Venda de mercadoria adquirida ou recebida de terceiros entregue ao destinatário por conta e ordem do adquirente originário, em venda à ordem", + "6120": + "Venda de mercadoria adquirida ou recebida de terceiros entregue ao destinatário pelo vendedor remetente, em venda à ordem", + "6122": + "Venda de produção do estabelecimento remetida para industrialização, por conta e ordem do adquirente, sem transitar pelo estabelecimento do adquirente", + "6123": + "Venda de mercadoria adquirida ou recebida de terceiros remetida para industrialização, por conta e ordem do adquirente, sem transitar pelo estabelecimento do adquirente", + "6124": "Industrialização efetuada para outra empresa", + "6125": + "Industrialização efetuada para outra empresa quando a mercadoria recebida para utilização no processo de industrialização não transitar pelo estabelecimento adquirente da mercadoria", + "6150": "TRANSFERÊNCIAS DE PRODUÇÃO PRÓPRIA OU DE TERCEIROS", + "6151": "Transferência de produção do estabelecimento", + "6152": "Transferência de mercadoria adquirida ou recebida de terceiros", + "6153": + "Transferência de energia elétrica 6.155 - Transferência de produção do estabelecimento, que não deva por ele transitar", + "6156": + "Transferência de mercadoria adquirida ou recebida de terceiros, que não deva por ele transitar", + "6201": "Devolução de compra para industrialização ou produção rural", + "6202": "Devolução de compra para comercialização", + "6205": "Anulação de valor relativo a aquisição de serviço de comunicação", + "6206": "Anulação de valor relativo a aquisição de serviço de transporte", + "6207": "Anulação de valor relativo à compra de energia elétrica", + "6208": + "Devolução de mercadoria recebida em transferência para industrialização ou produção rural", + "6209": "Devolução de mercadoria recebida em transferência para comercialização", + "6210": "Devolução de compra para utilização na prestação de serviço", + "6250": "VENDAS DE ENERGIA ELÉTRICA", + "6251": "Venda de energia elétrica para distribuição ou comercialização", + "6252": "Venda de energia elétrica para estabelecimento industrial", + "6253": "Venda de energia elétrica para estabelecimento comercial", + "6254": "Venda de energia elétrica para estabelecimento prestador de serviço de transporte", + "6255": "Venda de energia elétrica para estabelecimento prestador de serviço de comunicação", + "6256": "Venda de energia elétrica para estabelecimento de produtor rural", + "6257": "Venda de energia elétrica para consumo por demanda contratada", + "6258": "Venda de energia elétrica a não contribuinte", + "6301": "Prestação de serviço de comunicação para execução de serviço da mesma natureza", + "6302": "Prestação de serviço de comunicação a estabelecimento industrial", + "6303": "Prestação de serviço de comunicação a estabelecimento comercial", + "6304": + "Prestação de serviço de comunicação a estabelecimento de prestador de serviço de transporte", + "6305": + "Prestação de serviço de comunicação a estabelecimento de geradora ou de distribuidora de energia elétrica", + "6306": "Prestação de serviço de comunicação a estabelecimento de produtor rural", + "6307": "Prestação de serviço de comunicação a não contribuinte", + "6350": "PRESTAÇÕES DE SERVIÇOS DE TRANSPORTE", + "6351": "Prestação de serviço de transporte para execução de serviço da mesma natureza", + "6352": "Prestação de serviço de transporte a estabelecimento industrial", + "6353": "Prestação de serviço de transporte a estabelecimento comercial", + "6354": + "Prestação de serviço de transporte a estabelecimento de prestador de serviço de comunicação", + "6355": + "Prestação de serviço de transporte a estabelecimento de geradora ou de distribuidora de energia elétrica", + "6356": "Prestação de serviço de transporte a estabelecimento de produtor rural", + "6357": "Prestação de serviço de transporte a não contribuinte", + "6359": + "Prestação de serviço de transporte a contribuinte ou a não contribuinte quando a mercadoria transportada está dispensada de emissão de nota fiscal", + "6401": + "Venda de produção do estabelecimento em operação com produto sujeito ao regime de substituição tributária, na condição de contribuinte substituto", + "6402": + "Venda de produção do estabelecimento de produto sujeito ao regime de substituição tributária, em operação entre contribuintes substitutos do mesmo produto", + "6403": + "Venda de mercadoria adquirida ou recebida de terceiros em operação com mercadoria sujeita ao regime de substituição tributária, na condição de contribuinte substituto", + "6404": + "Venda de mercadoria sujeita ao regime de substituição tributária, cujo imposto já tenha sido retido anteriormente", + "6408": + "Transferência de produção do estabelecimento em operação com produto sujeito ao regime de substituição tributária", + "6409": + "Transferência de mercadoria adquirida ou recebida de terceiros em operação com mercadoria sujeita ao regime de substituição tributária", + "6410": + "Devolução de compra para industrialização ou produção rural em operação com mercadoria sujeita ao regime de substituição tributária", + "6411": + "Devolução de compra para comercialização em operação com mercadoria sujeita ao regime de substituição tributária", + "6412": + "Devolução de bem do ativo imobilizado, em operação com mercadoria sujeita ao regime de substituição tributária", + "6413": + "Devolução de mercadoria destinada ao uso ou consumo, em operação com mercadoria sujeita ao regime de substituição tributária", + "6414": + "Remessa de produção do estabelecimento para venda fora do estabelecimento em operação com produto sujeito ao regime de substituição tributária", + "6415": + "Remessa de mercadoria adquirida ou recebida de terceiros para venda fora do estabelecimento, em operação com mercadoria sujeita ao regime de substituição tributária", + "6501": "Remessa de produção do estabelecimento, com fim específico de exportação", + "6502": + "Remessa de mercadoria adquirida ou recebida de terceiros, com fim específico de exportação", + "6503": "Devolução de mercadoria recebida com fim específico de exportação", + "6504": + "Remessa de mercadorias para formação de lote de exportação, de produtos industrializados ou produzidos pelo próprio estabelecimento", + "6505": + "Remessa de mercadorias, adquiridas ou recebidas de terceiros, para formação de lote de exportação", + "6550": "OPERAÇÕES COM BENS DE ATIVO IMOBILIZADO E MATERIAIS PARA USO OU CONSUMO", + "6551": "Venda de bem do ativo imobilizado", + "6552": "Transferência de bem do ativo imobilizado", + "6553": "Devolução de compra de bem para o ativo imobilizado", + "6554": "Remessa de bem do ativo imobilizado para uso fora do estabelecimento", + "6555": "Devolução de bem do ativo imobilizado de terceiro, recebido para uso no estabelecimento", + "6556": "Devolução de compra de material de uso ou consumo", + "6557": "Transferência de material de uso ou consumo", + "6603": "Ressarcimento de ICMS retido por substituição tributária", + "6651": + "Venda de combustível ou lubrificante de produção do estabelecimento destinado à industrialização subseqüente", + "6652": + "Venda de combustível ou lubrificante de produção do estabelecimento destinado à comercialização", + "6653": + "Venda de combustível ou lubrificante de produção do estabelecimento destinado a consumidor ou usuário final", + "6654": + "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado à industrialização subseqüente", + "6655": + "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado à comercialização", + "6656": + "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado a consumidor ou usuário final", + "6657": + "Remessa de combustível ou lubrificante adquirido ou recebido de terceiros para venda fora do estabelecimento", + "6658": "Transferência de combustível ou lubrificante de produção do estabelecimento", + "6659": "Transferência de combustível ou lubrificante adquirido ou recebido de terceiro", + "6660": + "Devolução de compra de combustível ou lubrificante adquirido para industrialização subseqüente", + "6661": "Devolução de compra de combustível ou lubrificante adquirido para comercialização", + "6662": + "Devolução de compra de combustível ou lubrificante adquirido por consumidor ou usuário final", + "6663": "Remessa para armazenagem de combustível ou lubrificante", + "6664": "Retorno de combustível ou lubrificante recebido para armazenagem", + "6665": "Retorno simbólico de combustível ou lubrificante recebido para armazenagem", + "6666": + "Remessa por conta e ordem de terceiros de combustível ou lubrificante recebido para armazenagem", + "6901": "Remessa para industrialização por encomenda", + "6902": "Retorno de mercadoria utilizada na industrialização por encomenda", + "6903": + "Retorno de mercadoria recebida para industrialização e não aplicada no referido processo", + "6904": "Remessa para venda fora do estabelecimento", + "6905": "Remessa para depósito fechado ou armazém geral", + "6906": "Retorno de mercadoria depositada em depósito fechado ou armazém geral", + "6907": "Retorno simbólico de mercadoria depositada em depósito fechado ou armazém geral", + "6908": "Remessa de bem por conta de contrato de comodato", + "6909": "Retorno de bem recebido por conta de contrato de comodato", + "6910": "Remessa em bonificação, doação ou brinde", + "6911": "Remessa de amostra grátis", + "6912": + "Remessa de mercadoria ou bem para demonstração 6.913 - Retorno de mercadoria ou bem recebido para demonstração", + "6914": "Remessa de mercadoria ou bem para exposição ou feira", + "6915": "Remessa de mercadoria ou bem para conserto ou reparo", + "6916": "Retorno de mercadoria ou bem recebido para conserto ou reparo", + "6917": "Remessa de mercadoria em consignação mercantil ou industrial", + "6918": "Devolução de mercadoria recebida em consignação mercantil ou industrial", + "6919": + "Devolução simbólica de mercadoria vendida ou utilizada em processo industrial, recebida anteriormente em consignação mercantil ou industrial", + "6920": "Remessa de vasilhame ou sacaria", + "6921": "Devolução de vasilhame ou sacaria", + "6922": + "Lançamento efetuado a título de simples faturamento decorrente de venda para entrega futura", + "6923": "Remessa de mercadoria por conta e ordem de terceiros, em venda à ordem", + "6924": + "Remessa para industrialização por conta e ordem do adquirente da mercadoria, quando esta não transitar pelo estabelecimento do adquirente", + "6925": + "Retorno de mercadoria recebida para industrialização por conta e ordem do adquirente da mercadoria, quando aquela não transitar pelo estabelecimento do adquirente", + "6929": + "Lançamento efetuado em decorrência de emissão de documento fiscal relativo a operação ou prestação também registrada em equipamento Emissor de Cupom Fiscal - ECF", + "6931": + "Lançamento efetuado em decorrência da responsabilidade de retenção do imposto por substituição tributária, atribuída ao remetente ou alienante da mercadoria, pelo serviço de transporte realizado por transportador autônomo ou por transportador não inscrito na unidade da Federação onde iniciado o serviço", + "6932": + "Prestação de serviço de transporte iniciada em unidade da Federação diversa daquela onde inscrito o prestador", + "6933": "Prestação de serviço tributado pelo ISSQN", + "6949": "Outra saída de mercadoria ou prestação de serviço não especificado", + "7101": "Venda de produção do estabelecimento", + "7102": "Venda de mercadoria adquirida ou recebida de terceiros", + "7105": "Venda de produção do estabelecimento, que não deva por ele transitar", + "7106": "Venda de mercadoria adquirida ou recebida de terceiros, que não deva por ele transitar", + "7127": 'Venda de produção do estabelecimento sob o regime de "drawback"', + "7201": "Devolução de compra para industrialização ou produção rural", + "7202": "Devolução de compra para comercialização", + "7205": "Anulação de valor relativo à aquisição de serviço de comunicação", + "7206": "Anulação de valor relativo a aquisição de serviço de transporte", + "7207": "Anulação de valor relativo à compra de energia elétrica", + "7210": "Devolução de compra para utilização na prestação de serviço", + "7211": 'Devolução de compras para industrialização sob o regime de drawback"', + "7250": "VENDAS DE ENERGIA ELÉTRICA", + "7251": "Venda de energia elétrica para o exterior", + "7301": "Prestação de serviço de comunicação para execução de serviço da mesma natureza", + "7350": "PRESTAÇÕES DE SERVIÇO DE TRANSPORTE", + "7358": "Prestação de serviço de transporte", + "7501": "Exportação de mercadorias recebidas com fim específico de exportação", + "7550": "OPERAÇÕES COM BENS DE ATIVO IMOBILIZADO E MATERIAIS PARA USO OU CONSUMO", + "7551": "Venda de bem do ativo imobilizado", + "7553": "Devolução de compra de bem para o ativo imobilizado", + "7556": "Devolução de compra de material de uso ou consumo", + "7651": "Venda de combustível ou lubrificante de produção do estabelecimento", + "7654": "Venda de combustível ou lubrificante adquirido ou recebido de terceiros", + "7930": + "Lançamento efetuado atítulo de devolução de bem cuja entrada tenha ocorrido sob amparo de regime especial aduaneiro de admissão temporária", + "7949": "Outra saída de mercadoria ou prestação de serviço não especificado", +}; diff --git a/src/get-cfop/get-cfop.test.ts b/src/get-cfop/get-cfop.test.ts new file mode 100644 index 00000000..6a14869a --- /dev/null +++ b/src/get-cfop/get-cfop.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getCfop } from "./get-cfop"; + +describe("getCfop", () => { + it("should return the CFOP entry for a known code as a string", () => { + expect(getCfop("5102")).toEqual({ + code: "5102", + description: "Venda de mercadoria adquirida ou recebida de terceiros", + }); + }); + + it("should return the CFOP entry for a known code as a number", () => { + expect(getCfop(5102)).toEqual({ + code: "5102", + description: "Venda de mercadoria adquirida ou recebida de terceiros", + }); + }); + + it("should return the CFOP entry for a masked code (5.102)", () => { + expect(getCfop("5.102")).toEqual({ + code: "5102", + description: "Venda de mercadoria adquirida ou recebida de terceiros", + }); + }); + + it("should return a fresh object on every call", () => { + const first = getCfop("5102"); + const second = getCfop("5102"); + expect(first).not.toBe(second); + }); + + it("should return null for an unknown 4 digit code", () => { + expect(getCfop("0000")).toBeNull(); + }); + + it("should return null for a code with a length different from 4", () => { + expect(getCfop("510")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getCfop("")).toBeNull(); + }); + + it("should return null for null", () => { + // @ts-expect-error not a string or number + expect(getCfop(null)).toBeNull(); + }); + + it("should return null for undefined", () => { + // @ts-expect-error not a string or number + expect(getCfop(undefined)).toBeNull(); + }); +}); diff --git a/src/get-cfop/get-cfop.ts b/src/get-cfop/get-cfop.ts new file mode 100644 index 00000000..042c8771 --- /dev/null +++ b/src/get-cfop/get-cfop.ts @@ -0,0 +1,40 @@ +import { CFOP_TABLE } from "../_internals/constants/cfop"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * A CFOP (Código Fiscal de Operações e Prestações) code. + */ +export type Cfop = { + /** The 4 digit CFOP code. */ + code: string; + /** The official operation description. */ + description: string; +}; + +/** + * Looks a CFOP (Código Fiscal de Operações e Prestações) code up in the official table. + * + * @param {string|number} value - The CFOP code to look up. + * @returns {Cfop|null} The matching CFOP entry, or null when the code is unknown or + * invalid. + * + * @example + * ```typescript + * getCfop("5102"); // { code: "5102", description: "Venda de mercadoria adquirida ou recebida de terceiros" } + * getCfop("0000"); // null + * ``` + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 + * @see Based on: https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv + * Community-maintained CSV mirror of the official CFOP table used to build `CFOP_TABLE`. + */ +export const getCfop = (value: string | number): Cfop | null => { + if (isNullish(value) || value === "") return null; + + const digits = sanitizeToDigits(value); + + if (digits.length !== 4 || !(digits in CFOP_TABLE)) return null; + + return { code: digits, description: CFOP_TABLE[digits] }; +}; diff --git a/src/is-valid-cfop/is-valid-cfop.test.ts b/src/is-valid-cfop/is-valid-cfop.test.ts new file mode 100644 index 00000000..3407be51 --- /dev/null +++ b/src/is-valid-cfop/is-valid-cfop.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidCfop } from "./is-valid-cfop"; + +describe("isValidCfop", () => { + it("should return true for a known CFOP code as a string", () => { + expect(isValidCfop("5102")).toBe(true); + }); + + it("should return true for a known CFOP code as a number", () => { + expect(isValidCfop(5102)).toBe(true); + }); + + it("should return true for a masked CFOP code (5.102)", () => { + expect(isValidCfop("5.102")).toBe(true); + }); + + it("should return true for a code with surrounding whitespace", () => { + expect(isValidCfop(" 5102 ")).toBe(true); + }); + + it("should validate the sale of goods acquired from third parties (CFOP 5102, Ajuste SINIEF 07/2001)", () => { + expect(isValidCfop("5102")).toBe(true); + }); + + it("should return false for an unknown 4 digit code", () => { + expect(isValidCfop("0000")).toBe(false); + }); + + it("should return false for a code with a length different from 4", () => { + expect(isValidCfop("510")).toBe(false); + expect(isValidCfop("51020")).toBe(false); + }); + + it("should return false for an empty string", () => { + expect(isValidCfop("")).toBe(false); + }); + + it("should return false for null", () => { + // @ts-expect-error not a string or number + expect(isValidCfop(null)).toBe(false); + }); + + it("should return false for undefined", () => { + // @ts-expect-error not a string or number + expect(isValidCfop(undefined)).toBe(false); + }); + + it("should return false for a non numeric string", () => { + expect(isValidCfop("abcd")).toBe(false); + }); +}); diff --git a/src/is-valid-cfop/is-valid-cfop.ts b/src/is-valid-cfop/is-valid-cfop.ts new file mode 100644 index 00000000..b657879b --- /dev/null +++ b/src/is-valid-cfop/is-valid-cfop.ts @@ -0,0 +1,29 @@ +import { CFOP_TABLE } from "../_internals/constants/cfop"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Validates if a CFOP (Código Fiscal de Operações e Prestações) code exists in the + * official table. + * + * @param {string|number} value - The CFOP code to be validated. + * @returns {boolean} True when the code is a known 4 digit CFOP code, false otherwise. + * + * @example + * ```typescript + * isValidCfop("5102"); // true + * isValidCfop(5102); // true + * isValidCfop("0000"); // false + * ``` + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 + * @see Based on: https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv + * Community-maintained CSV mirror of the official CFOP table used to build `CFOP_TABLE`. + */ +export const isValidCfop = (value: string | number): boolean => { + if (isNullish(value) || value === "") return false; + + const digits = sanitizeToDigits(value); + + return digits.length === 4 && digits in CFOP_TABLE; +}; From 45ede38c5a184c3ec4719f14937e35ccc0628ceb Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:55 -0300 Subject: [PATCH 20/22] feat(cst): add isValidCst and isValidCsosn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validates CST (Código de Situação Tributária, per tax: icms/ipi/pis/cofins) and CSOSN (the Simples Nacional variant of the ICMS table) against their fixed code lists — neither carries a check digit. --- src/is-valid-csosn/constants.ts | 18 ++++ src/is-valid-csosn/is-valid-csosn.test.ts | 46 +++++++++ src/is-valid-csosn/is-valid-csosn.ts | 28 +++++ src/is-valid-cst/constants.ts | 74 +++++++++++++ src/is-valid-cst/is-valid-cst.test.ts | 120 ++++++++++++++++++++++ src/is-valid-cst/is-valid-cst.ts | 87 ++++++++++++++++ 6 files changed, 373 insertions(+) create mode 100644 src/is-valid-csosn/constants.ts create mode 100644 src/is-valid-csosn/is-valid-csosn.test.ts create mode 100644 src/is-valid-csosn/is-valid-csosn.ts create mode 100644 src/is-valid-cst/constants.ts create mode 100644 src/is-valid-cst/is-valid-cst.test.ts create mode 100644 src/is-valid-cst/is-valid-cst.ts diff --git a/src/is-valid-csosn/constants.ts b/src/is-valid-csosn/constants.ts new file mode 100644 index 00000000..84c88ed7 --- /dev/null +++ b/src/is-valid-csosn/constants.ts @@ -0,0 +1,18 @@ +/** + * CSOSN (Código de Situação da Operação no Simples Nacional) codes, per Convênio ICMS + * 92/2015 (Anexo, Código de Situação da Operação no Simples Nacional). + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/convenios/2015/CV092_15 + */ +export const CSOSN_CODES = [ + "101", + "102", + "103", + "201", + "202", + "203", + "300", + "400", + "500", + "900", +] as const; diff --git a/src/is-valid-csosn/is-valid-csosn.test.ts b/src/is-valid-csosn/is-valid-csosn.test.ts new file mode 100644 index 00000000..3325ac58 --- /dev/null +++ b/src/is-valid-csosn/is-valid-csosn.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { CSOSN_CODES } from "./constants"; +import { isValidCsosn } from "./is-valid-csosn"; + +describe("isValidCsosn", () => { + it("should return true for every known CSOSN code", () => { + for (const code of CSOSN_CODES) { + expect(isValidCsosn(code)).toBe(true); + } + }); + + it("should return true for a number input", () => { + expect(isValidCsosn(101)).toBe(true); + }); + + it("should return true with surrounding whitespace", () => { + expect(isValidCsosn(" 101 ")).toBe(true); + }); + + it("should return false for an unknown 3 digit code", () => { + expect(isValidCsosn("999")).toBe(false); + }); + + it("should return false for a length different from 3", () => { + expect(isValidCsosn("10")).toBe(false); + expect(isValidCsosn("1010")).toBe(false); + }); + + it("should return false for an empty string", () => { + expect(isValidCsosn("")).toBe(false); + }); + + it("should return false for null", () => { + // @ts-expect-error not a string or number + expect(isValidCsosn(null)).toBe(false); + }); + + it("should return false for undefined", () => { + // @ts-expect-error not a string or number + expect(isValidCsosn(undefined)).toBe(false); + }); + + it("should return false for a non numeric string", () => { + expect(isValidCsosn("abc")).toBe(false); + }); +}); diff --git a/src/is-valid-csosn/is-valid-csosn.ts b/src/is-valid-csosn/is-valid-csosn.ts new file mode 100644 index 00000000..c87cb1f0 --- /dev/null +++ b/src/is-valid-csosn/is-valid-csosn.ts @@ -0,0 +1,28 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { CSOSN_CODES } from "./constants"; + +/** + * Validates if a CSOSN (Código de Situação da Operação no Simples Nacional) code is valid. + * + * Accepted codes are `101, 102, 103, 201, 202, 203, 300, 400, 500, 900`. + * + * @param {string|number} value - The CSOSN code to be validated. + * @returns {boolean} True when the code is a known CSOSN code, false otherwise. + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/convenios/2015/CV092_15 + * + * @example + * ```typescript + * isValidCsosn("101"); // true + * isValidCsosn(900); // true + * isValidCsosn("999"); // false + * ``` + */ +export const isValidCsosn = (value: string | number): boolean => { + if (isNullish(value) || value === "") return false; + + const digits = sanitizeToDigits(value); + + return (CSOSN_CODES as readonly string[]).includes(digits); +}; diff --git a/src/is-valid-cst/constants.ts b/src/is-valid-cst/constants.ts new file mode 100644 index 00000000..65169cfb --- /dev/null +++ b/src/is-valid-cst/constants.ts @@ -0,0 +1,74 @@ +/** + * CST (Código de Situação Tributária) code tables per tax, per Ajuste SINIEF 07/2001 (Anexo, + * Tabela B) for ICMS and IPI, and Instrução Normativa RFB n. 594/2005 (Tabelas 4.3.3 and 4.3.4) + * for PIS and COFINS. + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/aj007_01 + * @see Official: https://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=15304 + */ +export const ICMS_CST_CODES = [ + "00", + "10", + "20", + "30", + "40", + "41", + "50", + "51", + "60", + "70", + "90", +] as const; + +export const IPI_CST_CODES = [ + "00", + "01", + "02", + "03", + "04", + "05", + "49", + "50", + "51", + "52", + "53", + "54", + "55", + "99", +] as const; + +export const PIS_COFINS_CST_CODES = [ + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "49", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "70", + "71", + "72", + "73", + "74", + "75", + "98", + "99", +] as const; diff --git a/src/is-valid-cst/is-valid-cst.test.ts b/src/is-valid-cst/is-valid-cst.test.ts new file mode 100644 index 00000000..b4d0f074 --- /dev/null +++ b/src/is-valid-cst/is-valid-cst.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidCst } from "./is-valid-cst"; + +describe("isValidCst", () => { + describe("icms", () => { + it("should return true for a valid origin + CST combination", () => { + expect(isValidCst("110", { tax: "icms" })).toBe(true); + expect(isValidCst("000", { tax: "icms" })).toBe(true); + expect(isValidCst("890", { tax: "icms" })).toBe(true); + }); + + it("should return true for a number input", () => { + expect(isValidCst(110, { tax: "icms" })).toBe(true); + }); + + it("should return false when the origin digit is greater than 8", () => { + expect(isValidCst("910", { tax: "icms" })).toBe(false); + }); + + it("should return false when the CST part is not a known code", () => { + expect(isValidCst("199", { tax: "icms" })).toBe(false); + }); + + it("should return false for a length different from 3", () => { + expect(isValidCst("10", { tax: "icms" })).toBe(false); + expect(isValidCst("1020", { tax: "icms" })).toBe(false); + }); + }); + + describe("ipi", () => { + it("should return true for known codes", () => { + expect(isValidCst("00", { tax: "ipi" })).toBe(true); + expect(isValidCst("49", { tax: "ipi" })).toBe(true); + expect(isValidCst("99", { tax: "ipi" })).toBe(true); + }); + + it("should return false for an unknown code", () => { + expect(isValidCst("06", { tax: "ipi" })).toBe(false); + }); + }); + + describe("pis", () => { + it("should return true for known codes", () => { + expect(isValidCst("07", { tax: "pis" })).toBe(true); + expect(isValidCst("98", { tax: "pis" })).toBe(true); + }); + + it("should return false for an unknown code", () => { + expect(isValidCst("11", { tax: "pis" })).toBe(false); + }); + }); + + describe("cofins", () => { + it("should return true for known codes (same table as pis)", () => { + expect(isValidCst("07", { tax: "cofins" })).toBe(true); + }); + + it("should return false for an unknown code", () => { + expect(isValidCst("11", { tax: "cofins" })).toBe(false); + }); + }); + + it("should return false for an unknown tax", () => { + // @ts-expect-error not a valid tax + expect(isValidCst("00", { tax: "iss" })).toBe(false); + }); + + describe("without options (tax omitted)", () => { + it("should return true when the code is a valid icms combination", () => { + expect(isValidCst("110")).toBe(true); + }); + + it("should return true when the code exists only in the ipi table", () => { + expect(isValidCst("00")).toBe(true); + }); + + it("should return true when the code exists only in the pis/cofins table", () => { + expect(isValidCst("07")).toBe(true); + }); + + it("should return true when the code exists in both the ipi and pis/cofins tables", () => { + expect(isValidCst("49")).toBe(true); + }); + + it("should return true when options is undefined", () => { + expect(isValidCst("110", undefined)).toBe(true); + }); + + it("should return true when options.tax is undefined", () => { + expect(isValidCst("110", {})).toBe(true); + }); + + it("should return false when the code exists in no table", () => { + expect(isValidCst("999")).toBe(false); + }); + }); + + it("should return false when options is null", () => { + // @ts-expect-error not an options object + expect(isValidCst("00", null)).toBe(false); + }); + + it("should return false for an empty string", () => { + expect(isValidCst("", { tax: "icms" })).toBe(false); + }); + + it("should return false for null", () => { + // @ts-expect-error not a string or number + expect(isValidCst(null, { tax: "icms" })).toBe(false); + }); + + it("should return false for undefined", () => { + // @ts-expect-error not a string or number + expect(isValidCst(undefined, { tax: "icms" })).toBe(false); + }); + + it("should sanitize whitespace and mask characters", () => { + expect(isValidCst(" 1-10 ", { tax: "icms" })).toBe(true); + }); +}); diff --git a/src/is-valid-cst/is-valid-cst.ts b/src/is-valid-cst/is-valid-cst.ts new file mode 100644 index 00000000..bb128190 --- /dev/null +++ b/src/is-valid-cst/is-valid-cst.ts @@ -0,0 +1,87 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { ICMS_CST_CODES, IPI_CST_CODES, PIS_COFINS_CST_CODES } from "./constants"; + +/** + * Options for `isValidCst`. + */ +export type IsValidCstOptions = { + /** + * The tax whose CST (Código de Situação Tributária) table the value is checked against. + * Omit it to accept a code that exists in any of the four tables (`icms`, `ipi`, `pis`, + * `cofins`). + */ + tax?: "icms" | "ipi" | "pis" | "cofins"; +}; + +const isValidIcmsCst = (digits: string): boolean => + digits.length === 3 && + digits.charAt(0) >= "0" && + digits.charAt(0) <= "8" && + (ICMS_CST_CODES as readonly string[]).includes(digits.slice(1)); + +const isValidForTax = ( + digits: string, + tax: "icms" | "ipi" | "pis" | "cofins" | undefined, +): boolean => { + switch (tax) { + case "icms": + return isValidIcmsCst(digits); + case "ipi": + return (IPI_CST_CODES as readonly string[]).includes(digits); + case "pis": + case "cofins": + return (PIS_COFINS_CST_CODES as readonly string[]).includes(digits); + default: + return false; + } +}; + +/** + * Validates if a CST (Código de Situação Tributária) code is valid for a given tax. + * + * `icms` accepts the 3 digit form used on tax documents (1 origin digit from `0` to `8` + * followed by 1 of the 11 codes `00, 10, 20, 30, 40, 41, 50, 51, 60, 70, 90`). + * + * `ipi` accepts 1 of the 14 codes `00, 01, 02, 03, 04, 05, 49, 50, 51, 52, 53, 54, 55, 99`. + * + * `pis` and `cofins` accept 1 of the 33 codes `01, 02, 03, 04, 05, 06, 07, 08, 09, 49, 50, 51, + * 52, 53, 54, 55, 56, 60, 61, 62, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 98, 99`. + * + * `options.tax` is optional. When it is omitted, the code is valid as long as it exists in any + * one of the four tables above; when it is given, only that table is consulted. + * + * @param {string|number} value - The CST code to be validated. + * @param {IsValidCstOptions} [options] - The tax whose table the value is checked against. + * Checks every table when omitted. + * @returns {boolean} True when the code is valid for the given tax (or for any tax, when + * `options.tax` is omitted), false otherwise. + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/aj007_01 + * @see Official: https://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=15304 + * + * @example + * ```typescript + * isValidCst("110", { tax: "icms" }); // true + * isValidCst("00", { tax: "ipi" }); // true + * isValidCst("49", { tax: "pis" }); // true + * isValidCst("07", { tax: "cofins" }); // true + * isValidCst("99", { tax: "icms" }); // false + * isValidCst("110"); // true (found in the icms table) + * isValidCst("49"); // true (found in the ipi table) + * isValidCst("999"); // false (not in any table) + * ``` + */ +export const isValidCst = (value: string | number, options?: IsValidCstOptions): boolean => { + if (isNullish(value) || value === "") return false; + if (options !== undefined && (options === null || typeof options !== "object")) return false; + + const digits = sanitizeToDigits(value); + const tax = options?.tax; + + if (tax !== undefined) return isValidForTax(digits, tax); + + return ( + isValidForTax(digits, "icms") || isValidForTax(digits, "ipi") || isValidForTax(digits, "pis") + ); +}; From cc9b6e44d59247c2b75a00f9d56be32662ba7605 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:55 -0300 Subject: [PATCH 21/22] feat(business-days): add isBusinessDay, addBusinessDays and differenceInBusinessDays All 3 build on getHolidays and the weekend to skip non-working days (options: { stateCode }). --- .../add-business-days.test.ts | 174 ++++++++++++++++ src/add-business-days/add-business-days.ts | 93 +++++++++ .../difference-in-business-days.test.ts | 188 ++++++++++++++++++ .../difference-in-business-days.ts | 90 +++++++++ src/is-business-day/is-business-day.test.ts | 93 +++++++++ src/is-business-day/is-business-day.ts | 74 +++++++ 6 files changed, 712 insertions(+) create mode 100644 src/add-business-days/add-business-days.test.ts create mode 100644 src/add-business-days/add-business-days.ts create mode 100644 src/difference-in-business-days/difference-in-business-days.test.ts create mode 100644 src/difference-in-business-days/difference-in-business-days.ts create mode 100644 src/is-business-day/is-business-day.test.ts create mode 100644 src/is-business-day/is-business-day.ts diff --git a/src/add-business-days/add-business-days.test.ts b/src/add-business-days/add-business-days.test.ts new file mode 100644 index 00000000..36499010 --- /dev/null +++ b/src/add-business-days/add-business-days.test.ts @@ -0,0 +1,174 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { addBusinessDays } from "./add-business-days"; + +describe("addBusinessDays", () => { + it("should match the date-fns addBusinessDays example (10 business days from 2014-09-01 lands on 2014-09-15, https://date-fns.org/docs/addBusinessDays)", () => { + const result = addBusinessDays({ date: new Date(2014, 8, 1), days: 10 }); + + expect(result).toEqual(new Date(2014, 8, 15)); + }); + + it("should skip a weekend when the very next day is a business day (Tue 2024-01-02 + 1 -> Wed 2024-01-03, noon)", () => { + const result = addBusinessDays({ date: new Date(2024, 0, 2, 12), days: 1 }); + + expect(result).toEqual(new Date(2024, 0, 3, 12)); + }); + + it("should skip Saturday and Sunday to land on the next Monday (Fri 2024-01-05 + 1)", () => { + const result = addBusinessDays({ date: new Date(2024, 0, 5, 12), days: 1 }); + + expect(result).toEqual(new Date(2024, 0, 8, 12)); + }); + + describe("national holidays and year boundaries", () => { + it("should skip Ano novo across a year boundary (2024-12-31 + 1 -> 2025-01-02)", () => { + const result = addBusinessDays({ date: new Date(2024, 11, 31, 12), days: 1 }); + + expect(result).toEqual(new Date(2025, 0, 2, 12)); + }); + + it("should treat 2025-01-01 (Ano novo) as a holiday, not counted towards the business days", () => { + const result = addBusinessDays({ date: new Date(2024, 11, 30, 12), days: 2 }); + + expect(result).toEqual(new Date(2025, 0, 2, 12)); + }); + }); + + describe("state holidays", () => { + it("should skip a state holiday when stateCode is provided (SP, Revolução Constitucionalista 2024-07-09)", () => { + const result = addBusinessDays({ date: new Date(2024, 6, 8, 12), days: 1, stateCode: "SP" }); + + expect(result).toEqual(new Date(2024, 6, 10, 12)); + }); + + it("should not skip the same date when stateCode is not provided", () => { + const result = addBusinessDays({ date: new Date(2024, 6, 8, 12), days: 1 }); + + expect(result).toEqual(new Date(2024, 6, 9, 12)); + }); + }); + + describe("includeOptional", () => { + it("should skip Carnaval 2024-02-13 by default (includeOptional defaults to true)", () => { + const result = addBusinessDays({ date: new Date(2024, 1, 12, 12), days: 1 }); + + expect(result).toEqual(new Date(2024, 1, 14, 12)); + }); + + it("should count Carnaval 2024-02-13 as a business day when includeOptional is false", () => { + const result = addBusinessDays({ + date: new Date(2024, 1, 12, 12), + days: 1, + includeOptional: false, + }); + + expect(result).toEqual(new Date(2024, 1, 13, 12)); + }); + }); + + describe("negative days", () => { + it("should walk backwards, skipping weekends (Fri 2024-01-05 - 1 -> Thu 2024-01-04)", () => { + const result = addBusinessDays({ date: new Date(2024, 0, 5, 12), days: -1 }); + + expect(result).toEqual(new Date(2024, 0, 4, 12)); + }); + + it("should walk backwards across a weekend (Mon 2024-01-08 - 1 -> Fri 2024-01-05)", () => { + const result = addBusinessDays({ date: new Date(2024, 0, 8, 12), days: -1 }); + + expect(result).toEqual(new Date(2024, 0, 5, 12)); + }); + }); + + describe("days: 0", () => { + it("should return a new Date equal to a business day input, unchanged", () => { + const input = new Date(2024, 0, 2, 12); + const result = addBusinessDays({ date: input, days: 0 }); + + expect(result).toEqual(new Date(2024, 0, 2, 12)); + expect(result).not.toBe(input); + }); + + it("should return the same calendar day even when it is a Saturday, mirroring date-fns' addBusinessDays(date, 0) behavior of not rolling to the next business day", () => { + const result = addBusinessDays({ date: new Date(2024, 0, 6, 12), days: 0 }); + + expect(result).toEqual(new Date(2024, 0, 6, 12)); + }); + + it("should return the same calendar day even when it is a holiday", () => { + const result = addBusinessDays({ date: new Date(2024, 0, 1, 12), days: 0 }); + + expect(result).toEqual(new Date(2024, 0, 1, 12)); + }); + }); + + describe("invalid input", () => { + it("should return null when params is null", () => { + // @ts-expect-error + expect(addBusinessDays(null)).toBeNull(); + }); + + it("should return null when params is undefined", () => { + // @ts-expect-error + expect(addBusinessDays(undefined)).toBeNull(); + }); + + it("should return null when params is not an object", () => { + // @ts-expect-error + expect(addBusinessDays("2024-01-02")).toBeNull(); + }); + + it("should return null when date is an invalid Date", () => { + expect(addBusinessDays({ date: new Date("not a date"), days: 1 })).toBeNull(); + }); + + it("should return null when date is not a Date", () => { + // @ts-expect-error + expect(addBusinessDays({ date: "2024-01-02", days: 1 })).toBeNull(); + }); + + it("should return null when days is not an integer", () => { + expect(addBusinessDays({ date: new Date(2024, 0, 2), days: 1.5 })).toBeNull(); + }); + + it("should return null when days is NaN", () => { + expect(addBusinessDays({ date: new Date(2024, 0, 2), days: Number.NaN })).toBeNull(); + }); + + it("should return null when days is Infinity", () => { + expect( + addBusinessDays({ date: new Date(2024, 0, 2), days: Number.POSITIVE_INFINITY }), + ).toBeNull(); + }); + + it("should return null when days is not a number", () => { + // @ts-expect-error + expect(addBusinessDays({ date: new Date(2024, 0, 2), days: "1" })).toBeNull(); + }); + + it("should return null when stateCode is not a string", () => { + expect( + // @ts-expect-error + addBusinessDays({ date: new Date(2024, 0, 2), days: 1, stateCode: 123 }), + ).toBeNull(); + }); + }); + + it("should not mutate the input Date", () => { + const input = new Date(2024, 0, 2, 12); + const before = input.getTime(); + + addBusinessDays({ date: input, days: 5 }); + + expect(input.getTime()).toBe(before); + }); + + it("should preserve the time-of-day of the input", () => { + const result = addBusinessDays({ date: new Date(2024, 0, 2, 9, 30, 15, 500), days: 1 }); + + expect(result?.getHours()).toBe(9); + expect(result?.getMinutes()).toBe(30); + expect(result?.getSeconds()).toBe(15); + expect(result?.getMilliseconds()).toBe(500); + }); +}); diff --git a/src/add-business-days/add-business-days.ts b/src/add-business-days/add-business-days.ts new file mode 100644 index 00000000..79771cbf --- /dev/null +++ b/src/add-business-days/add-business-days.ts @@ -0,0 +1,93 @@ +import type { StateCode } from "../_internals/constants/states"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { isBusinessDay } from "../is-business-day/is-business-day"; + +export type AddBusinessDaysParams = { + /** The date to count from. Never mutated: a new `Date` is returned. */ + date: Date; + /** Number of business days to add; a negative value walks backwards. Must be a finite integer. */ + days: number; + /** Two letter state code whose state holidays are also treated as non-business days (default: national holidays only). */ + stateCode?: StateCode; + /** Whether optional-type holidays (e.g. Carnaval, Corpus Christi) count as non-business days (default: `true`, matching Brazilian banking practice). */ + includeOptional?: boolean; +}; + +/** + * Adds a number of Brazilian business days (dias úteis) to a date. + * + * A business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a + * Sunday, or a Brazilian holiday), evaluated with the same `stateCode`/`includeOptional` + * options. The function walks one calendar day at a time, in the direction of `days`, + * counting only business days, so it is exact regardless of the arrangement of holidays + * around `date` (cheap in practice: `getHolidays` is memoized per year). + * + * `days: 0` returns a **new `Date` equal to `date`, unchanged**, even when `date` itself + * falls on a weekend or holiday. This mirrors the verified behavior of date-fns' + * `addBusinessDays(date, 0)`, which also returns the input date as-is rather than rolling + * it to the next business day; see `@see` below. A negative `days` walks backwards, one + * business day at a time, exactly like date-fns. + * + * The time-of-day (hours, minutes, seconds, milliseconds) of `date` is preserved in the + * result, and `date` itself is never mutated. + * + * If `stateCode` is provided but is not a valid/known state code, it is ignored and only + * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`). + * + * @param {AddBusinessDaysParams} params - The parameters for the calculation. + * @param {Date} params.date - The date to count from. + * @param {number} params.days - The number of business days to add (negative to subtract). + * @param {StateCode} [params.stateCode] - Brazilian state code whose state holidays are also considered. + * @param {boolean} [params.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @returns {Date | null} A new `Date`, `days` business days after `date`. `null` on bad + * input: a `params` that is not an object, a `date` that is not a valid `Date`, a `days` + * that is not a finite integer, or a `stateCode` that is not a string. + * + * @example + * ```typescript + * addBusinessDays({ date: new Date(2024, 0, 2, 12), days: 1 }); // Wed 2024-01-03, 12:00 (the next day is already a business day) + * addBusinessDays({ date: new Date(2024, 11, 31, 12), days: 1 }); // Thu 2025-01-02, 12:00 (Jan 1 is Ano novo, skipped) + * addBusinessDays({ date: new Date(2024, 0, 5, 12), days: -1 }); // Thu 2024-01-04, 12:00 (walks backwards) + * addBusinessDays({ date: new Date(2024, 0, 6, 12), days: 0 }); // Sat 2024-01-06, 12:00 (unchanged, even though Saturday is not a business day) + * addBusinessDays({ date: new Date("not a date"), days: 1 }); // null + * addBusinessDays({ date: new Date(2024, 0, 2), days: 1.5 }); // null (not an integer) + * addBusinessDays(null); // null + * ``` + * + * @see Based on: https://date-fns.org/docs/addBusinessDays Reference behavior for `days: 0` and + * for walking backwards on a negative `days`. The underlying holiday determination's official + * sources are cited in `isBusinessDay`/`getHolidays`. + */ +export const addBusinessDays = (params: AddBusinessDaysParams): Date | null => { + if (isNullish(params) || typeof params !== "object") return null; + + const { date, days, stateCode, includeOptional } = params; + + if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null; + + if (typeof days !== "number" || !Number.isFinite(days) || !Number.isInteger(days)) { + return null; + } + + if (stateCode !== undefined && typeof stateCode !== "string") return null; + + const result = new Date(date.getTime()); + + if (days === 0) return result; + + const hours = result.getHours(); + const step = days > 0 ? 1 : -1; + let remaining = Math.abs(days); + + while (remaining > 0) { + result.setDate(result.getDate() + step); + + if (isBusinessDay(result, { stateCode, includeOptional })) { + remaining -= 1; + } + } + + result.setHours(hours); + + return result; +}; diff --git a/src/difference-in-business-days/difference-in-business-days.test.ts b/src/difference-in-business-days/difference-in-business-days.test.ts new file mode 100644 index 00000000..e83665f5 --- /dev/null +++ b/src/difference-in-business-days/difference-in-business-days.test.ts @@ -0,0 +1,188 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { differenceInBusinessDays } from "./difference-in-business-days"; + +describe("differenceInBusinessDays", () => { + it("should return 0 for the same calendar day", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 0, 2, 9), + to: new Date(2024, 0, 2, 18), + }); + + expect(result).toBe(0); + }); + + it("should count the from day when it is a business day and exclude the to day (Tue 2024-01-02 to Wed 2024-01-03)", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 0, 2), + to: new Date(2024, 0, 3), + }); + + expect(result).toBe(1); + }); + + it("should not count the from day when it is a holiday (2024-01-01 Ano novo to 2024-01-02)", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 0, 1), + to: new Date(2024, 0, 2), + }); + + expect(result).toBe(0); + }); + + it("should skip weekends between from and to (Fri 2024-01-05 to Mon 2024-01-08)", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 0, 5), + to: new Date(2024, 0, 8), + }); + + expect(result).toBe(1); + }); + + it("should ignore the time of day of both from and to", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 0, 2, 23, 59), + to: new Date(2024, 0, 3, 0, 1), + }); + + expect(result).toBe(1); + }); + + describe("negative results", () => { + it("should return a negative number when to is before from (Wed 2024-01-03 to Tue 2024-01-02)", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 0, 3), + to: new Date(2024, 0, 2), + }); + + expect(result).toBe(-1); + }); + + it("should return positive zero, not negative zero, when there are no business days walking backwards (Sun 2024-01-07 to Sat 2024-01-06)", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 0, 7), + to: new Date(2024, 0, 6), + }); + + expect(result).toBe(0); + expect(Object.is(result, -0)).toBe(false); + }); + }); + + describe("national holidays and year boundaries", () => { + it("should count business days across a year boundary, skipping Ano novo (2024-12-30 Mon to 2025-01-03 Fri)", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 11, 30), + to: new Date(2025, 0, 3), + }); + + expect(result).toBe(3); + }); + }); + + describe("state holidays", () => { + it("should skip a state holiday when stateCode is provided (SP, Revolução Constitucionalista 2024-07-09)", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 6, 8), + to: new Date(2024, 6, 10), + stateCode: "SP", + }); + + expect(result).toBe(1); + }); + + it("should not skip that date when stateCode is not provided", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 6, 8), + to: new Date(2024, 6, 10), + }); + + expect(result).toBe(2); + }); + }); + + describe("includeOptional", () => { + it("should skip Carnaval 2024-02-13 by default (includeOptional defaults to true)", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 1, 12), + to: new Date(2024, 1, 14), + }); + + expect(result).toBe(1); + }); + + it("should count Carnaval 2024-02-13 as a business day when includeOptional is false", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 1, 12), + to: new Date(2024, 1, 14), + includeOptional: false, + }); + + expect(result).toBe(2); + }); + }); + + describe("invalid input", () => { + it("should return null when params is null", () => { + // @ts-expect-error + expect(differenceInBusinessDays(null)).toBeNull(); + }); + + it("should return null when params is undefined", () => { + // @ts-expect-error + expect(differenceInBusinessDays(undefined)).toBeNull(); + }); + + it("should return null when params is not an object", () => { + // @ts-expect-error + expect(differenceInBusinessDays("2024-01-02")).toBeNull(); + }); + + it("should return null when from is an invalid Date", () => { + expect( + differenceInBusinessDays({ from: new Date("not a date"), to: new Date(2024, 0, 2) }), + ).toBeNull(); + }); + + it("should return null when to is an invalid Date", () => { + expect( + differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date("not a date") }), + ).toBeNull(); + }); + + it("should return null when from is not a Date", () => { + expect( + // @ts-expect-error + differenceInBusinessDays({ from: "2024-01-02", to: new Date(2024, 0, 3) }), + ).toBeNull(); + }); + + it("should return null when to is not a Date", () => { + expect( + // @ts-expect-error + differenceInBusinessDays({ from: new Date(2024, 0, 2), to: "2024-01-03" }), + ).toBeNull(); + }); + + it("should return null when stateCode is not a string", () => { + expect( + differenceInBusinessDays({ + from: new Date(2024, 0, 2), + to: new Date(2024, 0, 3), + // @ts-expect-error + stateCode: 11, + }), + ).toBeNull(); + }); + + it("should ignore a stateCode that is not a known state", () => { + const result = differenceInBusinessDays({ + from: new Date(2024, 0, 2), + to: new Date(2024, 0, 3), + // @ts-expect-error + stateCode: "XX", + }); + + expect(result).toBe(1); + }); + }); +}); diff --git a/src/difference-in-business-days/difference-in-business-days.ts b/src/difference-in-business-days/difference-in-business-days.ts new file mode 100644 index 00000000..ab596a91 --- /dev/null +++ b/src/difference-in-business-days/difference-in-business-days.ts @@ -0,0 +1,90 @@ +import type { StateCode } from "../_internals/constants/states"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { isBusinessDay } from "../is-business-day/is-business-day"; + +export type DifferenceInBusinessDaysParams = { + /** The date to count from. Counted as a business day when it is one; never mutated. */ + from: Date; + /** The date to count to. Never counted itself, regardless of whether it is a business day. */ + to: Date; + /** Two letter state code whose state holidays are also treated as non-business days (default: national holidays only). */ + stateCode?: StateCode; + /** Whether optional-type holidays (e.g. Carnaval, Corpus Christi) count as non-business days (default: `true`, matching Brazilian banking practice). */ + includeOptional?: boolean; +}; + +const toLocalDayTimestamp = (date: Date): number => + Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()); + +/** + * Counts the number of Brazilian business days (dias úteis) between two dates. + * + * Mirrors the semantics of date-fns' `differenceInBusinessDays`, verified against its source + * (`differenceInBusinessDays.js` in the `date-fns` package): the day at `from` is counted when + * it is itself a business day, the day at `to` is never counted, and every business day + * strictly in between is counted once. Concretely, the function walks one calendar day at a + * time from `from` towards `to` (or the other way around, when `to` is before `from`), adding + * one for every day that `isBusinessDay` accepts, stopping just before reaching `to`. Only the + * calendar day of each `Date` matters, exactly like `differenceInCalendarDays`: the time of day + * is ignored. + * + * A business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a Sunday, + * or a Brazilian holiday), evaluated with the same `stateCode`/`includeOptional` options. + * + * `from` and `to` on the same calendar day return `0`. A `to` before `from` returns a negative + * number, mirroring date-fns. + * + * If `stateCode` is provided but is not a valid/known state code, it is ignored and only + * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`). + * + * @param {DifferenceInBusinessDaysParams} params - The parameters of the calculation. + * @param {Date} params.from - The date to count from. + * @param {Date} params.to - The date to count to. + * @param {StateCode} [params.stateCode] - Brazilian state code whose state holidays are also considered. + * @param {boolean} [params.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @returns {number|null} The number of business days between `from` and `to`, or `null` on bad + * input: a `params` that is not an object, a `from`/`to` that is not a valid `Date`, or a + * `stateCode` that is not a string. + * + * @example + * ```typescript + * differenceInBusinessDays({ from: new Date(2024, 0, 1), to: new Date(2024, 0, 2) }); // 0 (Jan 1 is Ano novo) + * differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 3) }); // 1 (Jan 2 counted, a Tuesday) + * differenceInBusinessDays({ from: new Date(2024, 0, 3), to: new Date(2024, 0, 2) }); // -1 (to before from) + * differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 2) }); // 0 (same day) + * differenceInBusinessDays({ from: new Date(2024, 6, 8), to: new Date(2024, 6, 10), stateCode: "SP" }); // 1 (Jul 9 is a state holiday in SP) + * differenceInBusinessDays({ from: new Date("not a date"), to: new Date() }); // null + * ``` + * + * @see Based on: https://date-fns.org/docs/differenceInBusinessDays Documented behavior. + * @see Based on: https://unpkg.com/date-fns@4.1.0/differenceInBusinessDays.js Source used to + * verify the exact boundary treatment (`from` counted, `to` excluded) and the sign convention. + * The underlying holiday determination's official sources are cited in + * `isBusinessDay`/`getHolidays`. + */ +export const differenceInBusinessDays = (params: DifferenceInBusinessDaysParams): number | null => { + if (isNullish(params) || typeof params !== "object") return null; + + const { from, to, stateCode, includeOptional } = params; + + if (!(from instanceof Date) || Number.isNaN(from.getTime())) return null; + if (!(to instanceof Date) || Number.isNaN(to.getTime())) return null; + if (stateCode !== undefined && typeof stateCode !== "string") return null; + + const fromDay = toLocalDayTimestamp(from); + const toDay = toLocalDayTimestamp(to); + + if (fromDay === toDay) return 0; + + const step = fromDay < toDay ? 1 : -1; + const movingDate = new Date(from.getFullYear(), from.getMonth(), from.getDate()); + + let result = 0; + + while (toLocalDayTimestamp(movingDate) !== toDay) { + if (isBusinessDay(movingDate, { stateCode, includeOptional })) result += step; + movingDate.setDate(movingDate.getDate() + step); + } + + return result === 0 ? 0 : result; +}; diff --git a/src/is-business-day/is-business-day.test.ts b/src/is-business-day/is-business-day.test.ts new file mode 100644 index 00000000..6de54712 --- /dev/null +++ b/src/is-business-day/is-business-day.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isBusinessDay } from "./is-business-day"; + +describe("isBusinessDay", () => { + it("should return true for a plain weekday that is not a holiday (noon, DST-safe)", () => { + expect(isBusinessDay(new Date(2024, 0, 2, 12))).toBe(true); + }); + + it("should return false for a Saturday (noon, DST-safe)", () => { + expect(isBusinessDay(new Date(2024, 0, 6, 12))).toBe(false); + }); + + it("should return false for a Sunday (noon, DST-safe)", () => { + expect(isBusinessDay(new Date(2024, 0, 7, 12))).toBe(false); + }); + + it("should return false for a national holiday (Ano novo, noon, DST-safe)", () => { + expect(isBusinessDay(new Date(2024, 0, 1, 12))).toBe(false); + }); + + it("should return false for Corpus Christi 2024 by default (optional holiday counts, banking practice)", () => { + expect(isBusinessDay(new Date(2024, 4, 30, 12))).toBe(false); + }); + + describe("state holidays", () => { + it("should return false for a state holiday when stateCode is provided (SP, Revolução Constitucionalista 2024-07-09)", () => { + expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: "SP" })).toBe(false); + }); + + it("should return true for the same date when stateCode is not provided", () => { + expect(isBusinessDay(new Date(2024, 6, 9, 12))).toBe(true); + }); + + it("should ignore an unknown stateCode and fall back to national holidays", () => { + // @ts-expect-error + expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: "XX" })).toBe(true); + }); + }); + + describe("includeOptional", () => { + it("should return false for Carnaval 2024-02-13 by default (includeOptional defaults to true)", () => { + expect(isBusinessDay(new Date(2024, 1, 13, 12))).toBe(false); + }); + + it("should return true for Carnaval 2024-02-13 when includeOptional is false", () => { + expect(isBusinessDay(new Date(2024, 1, 13, 12), { includeOptional: false })).toBe(true); + }); + + it("should still return false for a national (non-optional) holiday when includeOptional is false", () => { + expect(isBusinessDay(new Date(2024, 0, 1, 12), { includeOptional: false })).toBe(false); + }); + }); + + describe("year boundaries", () => { + it("should return false for 2024-12-31 only if it were a holiday, but treat it as a business day (Tuesday, no holiday)", () => { + expect(isBusinessDay(new Date(2024, 11, 31, 12))).toBe(true); + }); + + it("should return false for 2025-01-01 (Ano novo, next year)", () => { + expect(isBusinessDay(new Date(2025, 0, 1, 12))).toBe(false); + }); + }); + + describe("invalid input", () => { + it("should return false for an invalid Date", () => { + expect(isBusinessDay(new Date("not a date"))).toBe(false); + }); + + it("should return false for a non-Date value", () => { + // @ts-expect-error + expect(isBusinessDay("2024-01-02")).toBe(false); + }); + + it("should return false for null", () => { + // @ts-expect-error + expect(isBusinessDay(null)).toBe(false); + }); + + it("should return false for undefined", () => { + // @ts-expect-error + expect(isBusinessDay(undefined)).toBe(false); + }); + }); + + it("should not mutate the input Date", () => { + const value = new Date(2024, 0, 6, 12); + const original = new Date(value.getTime()); + + isBusinessDay(value, { stateCode: "SP" }); + + expect(value.getTime()).toBe(original.getTime()); + }); +}); diff --git a/src/is-business-day/is-business-day.ts b/src/is-business-day/is-business-day.ts new file mode 100644 index 00000000..fcf47381 --- /dev/null +++ b/src/is-business-day/is-business-day.ts @@ -0,0 +1,74 @@ +import type { StateCode } from "../_internals/constants/states"; +import { getHolidays } from "../get-holidays/get-holidays"; + +export type IsBusinessDayOptions = { + /** Two letter state code whose state holidays are also treated as non-business days (default: national holidays only). */ + stateCode?: StateCode; + /** Whether optional-type holidays (`Holiday.type === "optional"`, e.g. Carnaval, Corpus Christi) count as non-business days (default: `true`, matching Brazilian banking practice, where these days are not settlement days). */ + includeOptional?: boolean; +}; + +const WEEKEND_DAYS = [0, 6]; + +/** + * Checks whether a given date is a Brazilian business day (dia útil). + * + * A day is not a business day when it falls on Saturday or Sunday, or when it is a + * Brazilian holiday returned by `getHolidays({ year, stateCode })` for `value`'s **local + * calendar day** (its local year/month/day, as read by `Date#getFullYear`/`getMonth`/`getDate`), + * the same convention used by `isHoliday`. Build `value` from local components + * (`new Date(2024, 11, 25)`) rather than from a date-only ISO string when you mean a + * specific local day, for the same reason documented in `isHoliday`. + * + * `options.includeOptional` defaults to `true`: holidays whose `Holiday.type` is + * `"optional"` (Carnaval and Corpus Christi) are treated as non-business days, matching + * the Brazilian banking calendar (FEBRABAN/CMN), where these days are not settlement days + * even though they are not statutory holidays. Pass `false` to only treat statutory + * (`"national"` and `"state"`) holidays as non-business days. + * + * If `options.stateCode` is provided but is not a valid/known state code, it is ignored + * and only national holidays are considered (same behavior as `getHolidays`/`isHoliday`). + * + * @param {Date} value - The date to check. + * @param {IsBusinessDayOptions} [options] - Options for the check. + * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. + * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @returns {boolean} True when `value` is a business day, false otherwise. Bad input also + * returns false: a `value` that is not a valid `Date` (including non-`Date` values). + * + * @example + * ```typescript + * isBusinessDay(new Date(2024, 0, 2)); // true (Tuesday, not a holiday) + * isBusinessDay(new Date(2024, 0, 1)); // false (Ano novo) + * isBusinessDay(new Date(2024, 0, 6)); // false (Saturday) + * isBusinessDay(new Date(2024, 1, 13)); // false (Carnaval, optional holiday, banking practice) + * isBusinessDay(new Date(2024, 1, 13), { includeOptional: false }); // true + * isBusinessDay(new Date(2024, 6, 9), { stateCode: "SP" }); // false (Revolução Constitucionalista) + * isBusinessDay(new Date(2024, 6, 9)); // true (state holiday ignored without stateCode) + * isBusinessDay(new Date("not a date")); // false + * ``` + * + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm + */ +export const isBusinessDay = (value: Date, options?: IsBusinessDayOptions): boolean => { + if (!(value instanceof Date) || Number.isNaN(value.getTime())) return false; + + if (WEEKEND_DAYS.includes(value.getDay())) return false; + + const stateCode = options?.stateCode; + const includeOptional = options?.includeOptional ?? true; + + const year = value.getFullYear(); + const month = value.getMonth(); + const date = value.getDate(); + + return !getHolidays({ year, stateCode }).some((holiday) => { + if (!includeOptional && holiday.type === "optional") return false; + + return ( + holiday.date.getFullYear() === year && + holiday.date.getMonth() === month && + holiday.date.getDate() === date + ); + }); +}; From 3db81d25f61511696624755ec262f4129a8b5a73 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:05:55 -0300 Subject: [PATCH 22/22] feat(legal-nature): add getLegalNature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Looks up a single Natureza Jurídica entry, instead of requiring callers to filter the full getLegalNatures() list themselves. --- src/get-legal-nature/get-legal-nature.test.ts | 53 +++++++++++++++++++ src/get-legal-nature/get-legal-nature.ts | 40 ++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/get-legal-nature/get-legal-nature.test.ts create mode 100644 src/get-legal-nature/get-legal-nature.ts diff --git a/src/get-legal-nature/get-legal-nature.test.ts b/src/get-legal-nature/get-legal-nature.test.ts new file mode 100644 index 00000000..03dc1438 --- /dev/null +++ b/src/get-legal-nature/get-legal-nature.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getLegalNature } from "./get-legal-nature"; + +describe("getLegalNature", () => { + it("should return the legal nature entry for a known code as a string", () => { + expect(getLegalNature("2062")).toEqual({ + code: "2062", + description: "Sociedade Empresária Limitada", + }); + }); + + it("should return the legal nature entry for a known code as a number", () => { + expect(getLegalNature(2062)).toEqual({ + code: "2062", + description: "Sociedade Empresária Limitada", + }); + }); + + it("should return the legal nature entry for a masked code (206-2)", () => { + expect(getLegalNature("206-2")).toEqual({ + code: "2062", + description: "Sociedade Empresária Limitada", + }); + }); + + it("should return a fresh object on every call", () => { + const first = getLegalNature("2062"); + const second = getLegalNature("2062"); + expect(first).not.toBe(second); + }); + + it("should return null for an unknown 4 digit code", () => { + expect(getLegalNature("0000")).toBeNull(); + }); + + it("should return null for a code with a length different from 4", () => { + expect(getLegalNature("206")).toBeNull(); + }); + + it("should return null for an empty string", () => { + expect(getLegalNature("")).toBeNull(); + }); + + it("should return null for null", () => { + // @ts-expect-error not a string or number + expect(getLegalNature(null)).toBeNull(); + }); + + it("should return null for undefined", () => { + // @ts-expect-error not a string or number + expect(getLegalNature(undefined)).toBeNull(); + }); +}); diff --git a/src/get-legal-nature/get-legal-nature.ts b/src/get-legal-nature/get-legal-nature.ts new file mode 100644 index 00000000..f1ea21bf --- /dev/null +++ b/src/get-legal-nature/get-legal-nature.ts @@ -0,0 +1,40 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; + +/** + * A Brazilian legal nature (natureza jurídica) entry. + */ +export type LegalNature = { + /** The 4 digit legal nature code, without formatting. */ + code: string; + /** The official description in Portuguese, per IBGE/CONCLA. */ + description: string; +}; + +/** + * Looks a Brazilian legal nature (natureza jurídica) code up. + * + * @param {string|number} value - The legal nature code to look up, with or without formatting. + * @returns {LegalNature|null} The matching legal nature entry, or null when the code is unknown + * or invalid. + * + * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 + * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf + * + * @example + * ```typescript + * getLegalNature("2062"); // { code: "2062", description: "Sociedade Empresária Limitada" } + * getLegalNature("206-2"); // { code: "2062", description: "Sociedade Empresária Limitada" } + * getLegalNature("0000"); // null + * ``` + */ +export const getLegalNature = (value: string | number): LegalNature | null => { + if (isNullish(value) || value === "") return null; + + const digits = sanitizeToDigits(value); + + if (digits.length !== 4 || !Object.hasOwn(LEGAL_NATURE, digits)) return null; + + return { code: digits, description: LEGAL_NATURE[digits] }; +};