From 6fe61d07bb06ead05b359228af2394428bc22031 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:10:19 -0300 Subject: [PATCH 01/14] ci: drop the lychee link check, the citations are verified by hand --- .github/workflows/links.yml | 29 ----------------------------- .lycheeignore | 14 -------------- CONTRIBUTING.md | 5 +++-- 3 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 .github/workflows/links.yml delete mode 100644 .lycheeignore diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml deleted file mode 100644 index 6804c23e..00000000 --- a/.github/workflows/links.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Links - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths: - - "**/*.md" - - "src/**/*.ts" - -permissions: - contents: read - -jobs: - links: - name: Check links - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Run lychee - uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 - with: - args: --no-progress --accept 200..299,403,429 --timeout 45 --retry-wait-time 5 --max-retries 2 --root-dir docs --exclude-path node_modules --exclude-path dist --exclude-path CHANGELOG.md --exclude-path docs/llms.txt --exclude-path docs/llms-full.txt --exclude-path docs/_navbar.md --exclude-path docs/_sidebar.md --exclude-path docs/_coverpage.md --exclude-path docs/pt-br/_navbar.md --exclude-path docs/pt-br/_sidebar.md --exclude-path docs/pt-br/_coverpage.md '**/*.md' 'src/**/*.ts' - fail: true diff --git a/.lycheeignore b/.lycheeignore deleted file mode 100644 index 61344308..00000000 --- a/.lycheeignore +++ /dev/null @@ -1,14 +0,0 @@ -%7B -linkedin\.com -github\.com/saculbr -matalmeida\.me -engenhandosoftware\.com\.br -rfoel\.com -planalto\.gov\.br -confaz\.fazenda\.gov\.br -bcb\.gov\.br -alerj\.rj\.gov\.br -sintegra\.gov\.br -sirc\.gov\.br -legis\.alepe\.pe\.gov\.br -sped\.rfb\.gov\.br diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d45f1763..f200213e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -235,8 +235,9 @@ signatures are pinned by the `describe(" types")` blocks in the tests, and requires one. - Commit messages are checked with commitlint on every pull request, since release-please derives the version bump and the changelog from them. -- The `Links` workflow checks every URL in the Markdown files and in the `@see` tags of the source - with [lychee](https://lychee.cli.rs) when a pull request touches them. +- The URLs cited in the Markdown files and in the `@see` tags of the source are checked by hand + when a citation is added or changed: an automated link check was tried and dropped, since the + government hosts the library cites time out or answer 403 to anything that is not a browser. ## Zero runtime dependencies From c4156399982383d3e95153915fd3f130f041d81f Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:43 -0300 Subject: [PATCH 02/14] fix(email): follow the WHATWG domain label grammar --- src/is-valid-email/is-valid-email.test.ts | 22 +++++++++++++++++++++- src/is-valid-email/is-valid-email.ts | 8 +++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/is-valid-email/is-valid-email.test.ts b/src/is-valid-email/is-valid-email.test.ts index 6b8af69e..fdff24e0 100644 --- a/src/is-valid-email/is-valid-email.test.ts +++ b/src/is-valid-email/is-valid-email.test.ts @@ -47,6 +47,18 @@ describe("isValidEmail", () => { test("when the local part has consecutive dots that are not at the very start", () => { expect(isValidEmail("ab..c@example.com")).toBe(false); }); + + test("when a domain label ends with a hyphen", () => { + expect(isValidEmail("user@example-.com")).toBe(false); + }); + + test("when a domain label starts with a hyphen", () => { + expect(isValidEmail("user@-example.com")).toBe(false); + }); + + test("when a domain label is longer than the 63 characters WHATWG allows", () => { + expect(isValidEmail(`user@${"a".repeat(64)}.com`)).toBe(false); + }); }); describe("should return true", () => { @@ -60,6 +72,14 @@ describe("isValidEmail", () => { expect(isValidEmail("test@subdomain.example.com")).toBe(true); }); + test("when a domain label carries an inner hyphen", () => { + expect(isValidEmail("user@ex-ample.com")).toBe(true); + }); + + test("when a domain label is exactly 63 characters long", () => { + expect(isValidEmail(`user@${"a".repeat(63)}.com`)).toBe(true); + }); + test("when is a valid email with special characters", () => { expect(isValidEmail("user+tag@example.co.uk")).toBe(true); }); @@ -67,7 +87,7 @@ describe("isValidEmail", () => { describe("properties", () => { const addresses = fc.stringMatching( - /^[a-z0-9][a-z0-9_+-]{0,15}@[a-z0-9][a-z0-9-]{0,10}\.[a-z]{2,6}$/, + /^[a-z0-9][a-z0-9_+-]{0,15}@[a-z0-9](?:[a-z0-9-]{0,9}[a-z0-9])?\.[a-z]{2,6}$/, ); test("should accept a well-formed address", () => { diff --git a/src/is-valid-email/is-valid-email.ts b/src/is-valid-email/is-valid-email.ts index c3a7b1ea..adbfe275 100644 --- a/src/is-valid-email/is-valid-email.ts +++ b/src/is-valid-email/is-valid-email.ts @@ -1,5 +1,5 @@ const EMAIL_REGEX = - /^(?!\.)(?!.*\.\.)([a-z0-9_'+\-.]*)[a-z0-9_+-]@([a-z0-9][a-z0-9-]*\.)+[a-z]{2,}$/i; + /^(?!\.)(?!.*\.\.)([a-z0-9_'+\-.]*)[a-z0-9_+-]@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i; /** * Validates if an email address is valid. @@ -17,8 +17,10 @@ const EMAIL_REGEX = * The WHATWG HTML "valid e-mail address" definition is narrowed further: the local part is * limited to letters, digits and `_'+-.`, it may not start with a dot or contain two dots in a * row, and the domain must carry at least one dot and end in an alphabetic label of two or more - * letters. It is a practical subset of that WHATWG definition, not of IETF RFC 5322: quoted - * local parts and address literals are rejected. + * letters. Each dotted label follows the WHATWG production `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?`, + * so a label may neither start nor end with a hyphen nor exceed 63 characters. It is a practical + * subset of that WHATWG definition, not of IETF RFC 5322: quoted local parts and address + * literals are rejected. * * @see Official: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address * @see Official: https://www.rfc-editor.org/rfc/rfc5322 From 203ea5725e2ce28f3768ab4473418e0c9a95af73 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:43 -0300 Subject: [PATCH 03/14] fix(credit-card): accept only digits, spaces and hyphens and reject unsafe numbers --- .../is-valid-credit-card.test.ts | 21 +++++++++++++++++++ .../is-valid-credit-card.ts | 17 +++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) 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 index ef4faca4..121d1664 100644 --- a/src/is-valid-credit-card/is-valid-credit-card.test.ts +++ b/src/is-valid-credit-card/is-valid-credit-card.test.ts @@ -35,6 +35,15 @@ describe("isValidCreditCard", () => { expect(isValidCreditCard("4111-1111-1111-1111")).toBe(true); }); + test("for a value whose digit groups are separated by a run of mask characters", () => { + expect(isValidCreditCard("4111 - 1111 - 1111 - 1111")).toBe(true); + expect(isValidCreditCard("4111 1111 1111 1111")).toBe(true); + }); + + test("for a value surrounded by whitespace", () => { + expect(isValidCreditCard(" 4111 1111 1111 1111 ")).toBe(true); + }); + test("for the shortest accepted length (12 digits)", () => { expect(isValidCreditCard("601100000004")).toBe(true); }); @@ -86,6 +95,18 @@ describe("isValidCreditCard", () => { expect(isValidCreditCard("abcdabcdabcd")).toBe(false); }); + test("when letters are attached before the digits", () => { + expect(isValidCreditCard("abc4111111111111111")).toBe(false); + }); + + test("when letters are embedded between the digit groups", () => { + expect(isValidCreditCard("4111a1111b1111c1111")).toBe(false); + }); + + test("when the mask uses characters other than spaces and hyphens", () => { + expect(isValidCreditCard("(41)11-1111 1111 1111")).toBe(false); + }); + test("when it is null", () => { // @ts-expect-error: intentionally invalid input expect(isValidCreditCard(null)).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 index 7909fb63..02c974d6 100644 --- a/src/is-valid-credit-card/is-valid-credit-card.ts +++ b/src/is-valid-credit-card/is-valid-credit-card.ts @@ -3,13 +3,18 @@ import { mod10 } from "../_internals/mod10/mod10"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { MAX_LENGTH, MIN_LENGTH } from "./constants"; +const FORMAT_REGEX = /^\d+(?:[ -]+\d+)*$/; + /** * 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: 12 is the de-facto industry minimum PAN length, e.g. Maestro, and - * ISO/IEC 7812-1 caps the PAN at 19) and the Luhn check digit; it performs no brand detection - * (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks. + * Accepts the usual mask characters (spaces and hyphens) between digits, a run of them included, + * so `"4111 - 1111 - 1111 - 1111"` reads as the same PAN, and whitespace around the value; any + * other character makes the value invalid, so `"4111a1111b1111c1111"` is rejected instead of + * being read as `"4111111111111111"`. Only checks the digit count (12 to 19: 12 is + * the de-facto industry minimum PAN length, e.g. Maestro, and ISO/IEC 7812-1 caps the PAN at 19) + * and the Luhn check digit; it performs no brand detection (Visa, Mastercard, Amex...), issuer + * range lookup or expiration/CVV checks. * * A number is only accepted when it is a non-negative safe integer: a card number above * `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different @@ -25,7 +30,9 @@ import { MAX_LENGTH, MIN_LENGTH } from "./constants"; * isValidCreditCard("5555555555554444"); // true (Mastercard test number) * isValidCreditCard("378282246310005"); // true (American Express test number) * isValidCreditCard("4111 1111 1111 1111"); // true (spaced mask) + * isValidCreditCard("4111 - 1111 - 1111 - 1111"); // true (a run of separators between the digits) * isValidCreditCard("4111111111111112"); // false (bad check digit) + * isValidCreditCard("4111a1111b1111c1111"); // false (letters between the digits) * isValidCreditCard("123456789"); // false (too short) * isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a string) * ``` @@ -38,6 +45,8 @@ import { MAX_LENGTH, MIN_LENGTH } from "./constants"; export const isValidCreditCard = (value: string | number): boolean => { if (!isLookupCode(value)) return false; + if (!FORMAT_REGEX.test(value.toString().trim())) return false; + const digits = sanitizeToDigits(value); if (digits.length < MIN_LENGTH || digits.length > MAX_LENGTH) return false; From 873065e8187f4d235f6362842414ac7b8512ffd9 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:43 -0300 Subject: [PATCH 04/14] fix(renavam): reject repeated digits and stray characters --- src/is-valid-renavam/is-valid-renavam.test.ts | 22 +++++++++++++-- src/is-valid-renavam/is-valid-renavam.ts | 27 ++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/is-valid-renavam/is-valid-renavam.test.ts b/src/is-valid-renavam/is-valid-renavam.test.ts index 89bf6471..3f0945fc 100644 --- a/src/is-valid-renavam/is-valid-renavam.test.ts +++ b/src/is-valid-renavam/is-valid-renavam.test.ts @@ -60,6 +60,20 @@ describe("isValidRenavam", () => { expect(isValidRenavam("639884963xyz")).toBe(false); }); + test("when letters are attached to an otherwise valid RENAVAM", () => { + expect(isValidRenavam("639884962abc")).toBe(false); + expect(isValidRenavam("ab00639884962")).toBe(false); + }); + + test("when the mask uses a character other than whitespace, a dot or a hyphen", () => { + expect(isValidRenavam("0063988/4962")).toBe(false); + }); + + test("when every digit is the same", () => { + expect(isValidRenavam("00000000000")).toBe(false); + expect(isValidRenavam("000000000")).toBe(false); + }); + test("when is a RENAVAM with invalid length: 8 digits (too short), 10 digits (invalid), or 12 digits (too long)", () => { expect(isValidRenavam("12345678")).toBe(false); expect(isValidRenavam("1234567890")).toBe(false); @@ -80,8 +94,10 @@ describe("isValidRenavam", () => { expect(isValidRenavam(639_884_962)).toBe(true); }); - test("when is a RENAVAM valid with mixed characters that sanitize to a valid RENAVAM", () => { - expect(isValidRenavam("639884962abc")).toBe(true); + test("when is a RENAVAM valid with the usual mask characters", () => { + expect(isValidRenavam("0063988.4962")).toBe(true); + expect(isValidRenavam("00639884-962")).toBe(true); + expect(isValidRenavam(" 00639884962 ")).toBe(true); }); test("when the multiplier cycle wraps from 9 back to 2 on non-zero digits", () => { @@ -97,6 +113,8 @@ describe("isValidRenavam", () => { test("should accept exactly one check digit for any base", () => { fc.assert( fc.property(fc.stringMatching(/^[0-9]{10}$/), (base) => { + fc.pre(!/^(\d)\1{9}$/.test(base)); + const accepted = RENAVAM_DIGITS.filter((digit) => isValidRenavam(`${base}${digit}`)); expect(accepted.length).toBe(1); diff --git a/src/is-valid-renavam/is-valid-renavam.ts b/src/is-valid-renavam/is-valid-renavam.ts index 7331df7a..277b55ba 100644 --- a/src/is-valid-renavam/is-valid-renavam.ts +++ b/src/is-valid-renavam/is-valid-renavam.ts @@ -1,7 +1,11 @@ -import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; const RENAVAM_LENGTH = 11; +const SEPARATORS_REGEX = /[\s.-]/g; + +const FORMAT_REGEX = /^\d{9}$|^\d{11}$/; + const padLeft = (input: string, padLength: number): string => "0".repeat(padLength - input.length) + input; @@ -14,6 +18,10 @@ const padLeft = (input: string, padLength: number): string => * * The validation uses a checksum algorithm based on modulo 11. * + * Spaces, dots and hyphens are ignored, so every punctuated form of a RENAVAM is accepted, but + * any other character, a letter in particular, makes the value invalid. A registration whose + * digits are all the same (`"00000000000"`) is rejected as well, matching both references below. + * * @param {string} renavam - The RENAVAM value to be validated. * @returns {boolean} True if the RENAVAM is valid, false otherwise. * @@ -21,23 +29,30 @@ const padLeft = (input: string, padLength: number): string => * ```typescript * isValidRenavam("639884962"); // true (9 digits, old format) * isValidRenavam("00639884962"); // true (11 digits, new format) + * isValidRenavam("0063988.4962"); // true (dots and hyphens are ignored) * isValidRenavam("12345678901"); // false (invalid checksum) + * isValidRenavam("00000000000"); // false (repeated digits) + * isValidRenavam("ab00639884962"); // false (invalid format) * ``` * - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9503compilado.htm The Código de - * Trânsito Brasileiro, which creates the RENAVAM registry but does not define its check digit. + * The Código de Trânsito Brasileiro creates the RENAVAM registry but does not define its check + * digit, so the algorithm below follows the two community references cited as `Based on:`. + * + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9503compilado.htm * @see Based on: https://github.com/klawdyo/validation-br/blob/main/src/renavam.ts - * @see Based on: https://github.com/brazilian-utils/brutils-python/blob/main/brutils/renavam.py + * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/renavam.py */ export const isValidRenavam = (renavam: string | number): boolean => { if (typeof renavam !== "string" && typeof renavam !== "number") return false; - const digits = sanitizeToDigits(renavam); + const digits = renavam.toString().replace(SEPARATORS_REGEX, ""); - if (digits.length !== 9 && digits.length !== 11) return false; + if (!FORMAT_REGEX.test(digits)) return false; const paddedDigits = padLeft(digits, RENAVAM_LENGTH); + if (isRepeatedDigits(paddedDigits)) return false; + const renavamWithoutDigit = paddedDigits.slice(0, 10); let reversedRenavam = ""; From 47a9db2a2317e2bec7eba3ed9647f4d7eba45adb Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:43 -0300 Subject: [PATCH 05/14] fix: reject stray characters in isValidCnh, isValidVoterId and isValidProcessoJuridico --- src/generate-cnh/generate-cnh.ts | 6 ++++++ src/is-valid-cnh/is-valid-cnh.test.ts | 11 +++++++++- src/is-valid-cnh/is-valid-cnh.ts | 19 ++++++++++++++--- .../is-valid-processo-juridico.test.ts | 20 +++++++++++++++++- .../is-valid-processo-juridico.ts | 18 ++++++++++------ .../is-valid-voter-id.test.ts | 17 ++++++++++++++- src/is-valid-voter-id/is-valid-voter-id.ts | 21 ++++++++++++------- 7 files changed, 92 insertions(+), 20 deletions(-) diff --git a/src/generate-cnh/generate-cnh.ts b/src/generate-cnh/generate-cnh.ts index f92266cd..5e36f200 100644 --- a/src/generate-cnh/generate-cnh.ts +++ b/src/generate-cnh/generate-cnh.ts @@ -19,6 +19,12 @@ import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-d * 2 security check digits, but no official text publishes the check-digit weights; the algorithm * below follows the community reference cited as `Based on:`. * + * Art. 4º § 1º of the same resolution states that the check digit is computed by the DSR system + * with a "módulo 11" routine in which a remainder of 0 or 1 yields the digit 0. That rounding is + * not the rule the registry numbers use in practice: the first verifier keeps the remainder + * itself, so a remainder of 1 yields the digit 1 (which is why `"00000000119"` is a valid CNH). + * The implementation follows the cited `Based on:` reference, not § 1º. + * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/Resolucao8862021F.pdf * @see Based on: https://siga0984.wordpress.com/2019/05/01/algoritmos-validacao-de-cnh/ */ diff --git a/src/is-valid-cnh/is-valid-cnh.test.ts b/src/is-valid-cnh/is-valid-cnh.test.ts index d14e302b..0e62eba9 100644 --- a/src/is-valid-cnh/is-valid-cnh.test.ts +++ b/src/is-valid-cnh/is-valid-cnh.test.ts @@ -29,6 +29,15 @@ describe("isValidCnh", () => { expect(isValidCnh("0000000011900")).toBe(false); }); + it("should return false when a letter is attached to the digits", () => { + expect(isValidCnh("ab00000000119")).toBe(false); + expect(isValidCnh("00000000119ab")).toBe(false); + }); + + it("should return false when the mask uses a character other than whitespace, a dot or a hyphen", () => { + expect(isValidCnh("000000001/19")).toBe(false); + }); + it("should return false for falsy or non-string values", () => { expect(isValidCnh("")).toBe(false); // @ts-expect-error: intentionally invalid input @@ -40,7 +49,7 @@ describe("isValidCnh", () => { describe("properties", () => { test("should accept a generated CNH whatever mask characters surround its digits", () => { fc.assert( - fc.property(maskSeparators([".", "-", "/", " "], 3, 3), (separators) => { + fc.property(maskSeparators([".", "-", " "], 3, 3), (separators) => { const cnh = generateCnh(); const base = `${separators[0]}${cnh.slice(0, 9)}${separators[1]}`; diff --git a/src/is-valid-cnh/is-valid-cnh.ts b/src/is-valid-cnh/is-valid-cnh.ts index 61bbfa60..25d91723 100644 --- a/src/is-valid-cnh/is-valid-cnh.ts +++ b/src/is-valid-cnh/is-valid-cnh.ts @@ -1,11 +1,17 @@ import { calculateCnhFirstVerifier } from "../_internals/calculate-cnh-first-verifier/calculate-cnh-first-verifier"; import { calculateCnhSecondVerifier } from "../_internals/calculate-cnh-second-verifier/calculate-cnh-second-verifier"; import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; -import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +const SEPARATORS_REGEX = /[\s.-]/g; + +const FORMAT_REGEX = /^\d{11}$/; /** * Validates if a CNH (Carteira Nacional de Habilitação, the Brazilian driver's license number) is valid. * + * Spaces, dots and hyphens are ignored, so every punctuated form of a CNH is accepted, but any + * other character, a letter in particular, makes the value invalid. + * * @param {string} value - The CNH value to be validated. * @returns {boolean} True if the CNH is valid, false otherwise. * @@ -15,21 +21,28 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * isValidCnh("000000001-19"); // true * isValidCnh("11111111111"); // false (repeated digits) * isValidCnh("12345678901"); // false (invalid checksum) + * isValidCnh("ab00000000119"); // false (invalid format) * ``` * * Resolução CONTRAN nº 886/2021, art. 4º I, defines the CNH registry number as 9 characters plus * 2 security check digits, but no official text publishes the check-digit weights; the algorithm * below follows the community reference cited as `Based on:`. * + * Art. 4º § 1º of the same resolution states that the check digit is computed by the DSR system + * with a "módulo 11" routine in which a remainder of 0 or 1 yields the digit 0. That rounding is + * not the rule the registry numbers use in practice: the first verifier keeps the remainder + * itself, so a remainder of 1 yields the digit 1 (which is why `"00000000119"` is accepted). The + * implementation follows the cited `Based on:` reference, not § 1º. + * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/Resolucao8862021F.pdf * @see Based on: https://siga0984.wordpress.com/2019/05/01/algoritmos-validacao-de-cnh/ */ export const isValidCnh = (value: string): boolean => { if (typeof value !== "string") return false; - const digits = sanitizeToDigits(value); + const digits = value.replace(SEPARATORS_REGEX, ""); - if (digits.length !== 11 || isRepeatedDigits(digits)) return false; + if (!FORMAT_REGEX.test(digits) || isRepeatedDigits(digits)) return false; // Stryker disable next-line MethodExpression: the verifier helpers only read indices 0-8. const base = digits.slice(0, 9); diff --git a/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts b/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts index 8252fbc9..2b5a4941 100644 --- a/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts +++ b/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts @@ -34,6 +34,19 @@ describe("isValidProcessoJuridico", () => { test("when it is a 20 digit value with a mismatched check digit", () => { expect(isValidProcessoJuridico("00020802520125150050")).toBe(false); }); + + test("when a letter is attached to the digits", () => { + expect(isValidProcessoJuridico("ab00020802520125150049")).toBe(false); + expect(isValidProcessoJuridico("00020802520125150049ab")).toBe(false); + }); + + test("when the mask uses a character the CNJ layout does not carry", () => { + expect(isValidProcessoJuridico("0002080/25.2012.5.15.0049")).toBe(false); + }); + + test("when a mask separator falls outside the CNJ field boundaries", () => { + expect(isValidProcessoJuridico("000208-0252012.5.15.0049")).toBe(false); + }); }); describe("should return true", () => { @@ -45,6 +58,11 @@ describe("isValidProcessoJuridico", () => { expect(isValidProcessoJuridico("0002080-25.2012.5.15.0049")).toBe(true); }); + test("when a masked processo juridico is surrounded by whitespace", () => { + expect(isValidProcessoJuridico(" 0002080-25.2012.5.15.0049 ")).toBe(true); + expect(isValidProcessoJuridico("\n00020802520125150049\t")).toBe(true); + }); + test("when is a processo juridico valid with the legacy fused mask", () => { expect(isValidProcessoJuridico("0002080-25.2012.515.0049")).toBe(true); }); @@ -53,7 +71,7 @@ describe("isValidProcessoJuridico", () => { describe("properties", () => { test("should accept a generated number whatever mask separates its fields", () => { fc.assert( - fc.property(maskSeparators([".", "-", "/", " "], 5, 3), (separators) => { + fc.property(maskSeparators([".", "-", " "], 5, 3), (separators) => { const value = generateProcessoJuridico() as string; const head = `${value.slice(0, 7)}${separators[0]}${value.slice(7, 9)}`; const body = `${separators[1]}${value.slice(9, 13)}${separators[2]}`; diff --git a/src/is-valid-processo-juridico/is-valid-processo-juridico.ts b/src/is-valid-processo-juridico/is-valid-processo-juridico.ts index f04c984d..c4d87a98 100644 --- a/src/is-valid-processo-juridico/is-valid-processo-juridico.ts +++ b/src/is-valid-processo-juridico/is-valid-processo-juridico.ts @@ -1,5 +1,3 @@ -import { PROCESSO_JURIDICO_LENGTH } from "../_internals/constants/processo-juridico"; -import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { CHECK_DIGIT_LENGTH, CHECK_DIGIT_START_POSITION, @@ -7,6 +5,10 @@ import { MOD_97_10_SUM, } from "./constants"; +const SEPARATORS_REGEX = /[\s.-]/g; + +const FORMAT_REGEX = /^\d{7}[\s.-]*\d{2}[\s.-]*\d{4}[\s.-]*\d[\s.-]*\d{2}[\s.-]*\d{4}$/; + const verifyCheckDigit = (value: string): boolean => { const verificationDigits = Number.parseInt( value.slice(CHECK_DIGIT_START_POSITION, CHECK_DIGIT_START_POSITION + CHECK_DIGIT_LENGTH), @@ -39,6 +41,10 @@ const verifyCheckDigit = (value: string): boolean => { /** * Validates a Brazilian Processo Jurídico (court case) number. * + * The CNJ mask separators (whitespace, `.` and `-`) are accepted between the + * `NNNNNNN-DD.AAAA.J.TR.OOOO` fields, and whitespace around the value is ignored, but any other + * character, a letter in particular, makes the value invalid. + * * @param {string} value - The Processo Jurídico number to validate. * @returns {boolean} True if the Processo Jurídico number is valid, false otherwise. * @@ -46,6 +52,8 @@ const verifyCheckDigit = (value: string): boolean => { * ```typescript * isValidProcessoJuridico("00020802520125150049"); // true * isValidProcessoJuridico("0002080-25.2012.5.15.0049"); // true + * isValidProcessoJuridico(" 0002080-25.2012.5.15.0049 "); // true (surrounding whitespace) + * isValidProcessoJuridico("ab00020802520125150049"); // false (invalid format) * ``` * * Resolução CNJ nº 65/2008 defines this Número Único de Processo layout and its check digits. @@ -55,9 +63,7 @@ const verifyCheckDigit = (value: string): boolean => { export const isValidProcessoJuridico = (value: string): boolean => { if (typeof value !== "string") return false; - const digits = sanitizeToDigits(value); - - if (digits.length !== PROCESSO_JURIDICO_LENGTH) return false; + if (!FORMAT_REGEX.test(value.trim())) return false; - return verifyCheckDigit(digits); + return verifyCheckDigit(value.replace(SEPARATORS_REGEX, "")); }; diff --git a/src/is-valid-voter-id/is-valid-voter-id.test.ts b/src/is-valid-voter-id/is-valid-voter-id.test.ts index 6e71778d..e4586e49 100644 --- a/src/is-valid-voter-id/is-valid-voter-id.test.ts +++ b/src/is-valid-voter-id/is-valid-voter-id.test.ts @@ -62,6 +62,21 @@ describe("isValidVoterId", () => { expect(isValidVoterId("1234567890345")).toBe(false); }); + it("should reject a value with a letter attached to the digits", () => { + expect(isValidVoterId("ab102385010671")).toBe(false); + expect(isValidVoterId("102385010671ab")).toBe(false); + }); + + it("should reject a mask that is not whitespace or a dot", () => { + expect(isValidVoterId("1023-8501-06-71")).toBe(false); + }); + + it("should accept the documented whitespace and dot masks", () => { + expect(isValidVoterId("1023 8501 06 71")).toBe(true); + expect(isValidVoterId("1023.8501.06.71")).toBe(true); + expect(isValidVoterId("1234 5678 8 01 91")).toBe(true); + }); + it("should return false for null, undefined, a number or an empty string", () => { // @ts-expect-error: intentionally invalid input expect(isValidVoterId(null)).toBe(false); @@ -95,7 +110,7 @@ describe("isValidVoterId", () => { describe("properties", () => { test("should accept a generated voter id whatever mask surrounds its digits", () => { fc.assert( - fc.property(maskSeparators([".", "-", "/", " "], 3, 3), (separators) => { + fc.property(maskSeparators([".", " "], 3, 3), (separators) => { const voterId = generateVoterId(); const head = `${separators[0]}${voterId.slice(0, 8)}${separators[1]}`; diff --git a/src/is-valid-voter-id/is-valid-voter-id.ts b/src/is-valid-voter-id/is-valid-voter-id.ts index a81b4236..6ffba7d4 100644 --- a/src/is-valid-voter-id/is-valid-voter-id.ts +++ b/src/is-valid-voter-id/is-valid-voter-id.ts @@ -1,14 +1,10 @@ import { calculateVoterIdFirstDigit } from "../_internals/calculate-voter-id-first-digit/calculate-voter-id-first-digit"; import { calculateVoterIdSecondDigit } from "../_internals/calculate-voter-id-second-digit/calculate-voter-id-second-digit"; import { NINE_DIGIT_FEDERATIVE_UNION_CODES } from "../_internals/constants/voter-id"; -import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -const isValidLength = (value: string): boolean => { - if (value.length === 12) return true; +const SEPARATORS_REGEX = /[\s.]/g; - const federativeUnion = value.slice(-4, -2); - return value.length === 13 && NINE_DIGIT_FEDERATIVE_UNION_CODES.includes(federativeUnion); -}; +const FORMAT_REGEX = /^[\s.]*\d{4}[\s.]*\d{4}[\s.]*(?:\d[\s.]*)?\d{2}[\s.]*\d{2}[\s.]*$/; /** * Validates if a Brazilian voter id (título de eleitor) is valid. @@ -17,6 +13,9 @@ const isValidLength = (value: string): boolean => { * union code (01-28) and a 2-digit verification code. São Paulo (01) and Minas Gerais (02) * may instead issue voter ids with a 9-digit sequential number, totalling 13 digits. * + * Whitespace and dots are accepted around and between the "0000 0000 00 00" groups, but any + * other character, a letter in particular, makes the value invalid. + * * @param {string} value - The voter id value to be validated. * @returns {boolean} True if the voter id is valid, false otherwise. * @@ -24,7 +23,9 @@ const isValidLength = (value: string): boolean => { * ```typescript * isValidVoterId("102385010671"); // true (12 digits) * isValidVoterId("1234567880191"); // true (13 digits, São Paulo) + * isValidVoterId("1023 8501 06 71"); // true (whitespace mask) * isValidVoterId("123456780124"); // false (invalid checksum) + * isValidVoterId("ab102385010671"); // false (invalid format) * ``` * * Resolução TSE nº 23.659/2021, art. 36, parágrafo único, confirms the federative union table and @@ -39,15 +40,19 @@ const isValidLength = (value: string): boolean => { export const isValidVoterId = (value: string): boolean => { if (typeof value !== "string") return false; - const digits = sanitizeToDigits(value); + if (!FORMAT_REGEX.test(value)) return false; - if (!isValidLength(digits)) return false; + const digits = value.replace(SEPARATORS_REGEX, ""); // Stryker disable next-line MethodExpression: the check digits are computed from the first eight digits only, so passing the whole value instead of the sequential part yields the same result. const sequentialNumber = digits.slice(0, -4); const federativeUnion = digits.slice(-4, -2); const verifier = digits.slice(-2); + if (digits.length === 13 && !NINE_DIGIT_FEDERATIVE_UNION_CODES.includes(federativeUnion)) { + return false; + } + const ufCode = Number(federativeUnion); if (ufCode < 1 || ufCode > 28) return false; From 3712fcdfe571df714dd7203e49c5f74fb2bbcb74 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:43 -0300 Subject: [PATCH 06/14] fix(cnpj): select the alphanumeric rule only for version 2 --- src/is-valid-cnpj/is-valid-cnpj.test.ts | 11 +++++++++++ src/is-valid-cnpj/is-valid-cnpj.ts | 5 +++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/is-valid-cnpj/is-valid-cnpj.test.ts b/src/is-valid-cnpj/is-valid-cnpj.test.ts index ec4809a2..6d476b12 100644 --- a/src/is-valid-cnpj/is-valid-cnpj.test.ts +++ b/src/is-valid-cnpj/is-valid-cnpj.test.ts @@ -71,6 +71,17 @@ describe("isValidCnpj", () => { expect(isValidCnpj("Q0SLFMBD7VX439")).toBe(false); }); + test("when it has letters and the version option is not 2, as formatCnpj and parseCnpj read it", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidCnpj("Q0SLFMBD7VX439", { version: 3 })).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCnpj("Q0SLFMBD7VX439", { version: 0 })).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCnpj("Q0SLFMBD7VX439", { version: "2" })).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCnpj("Q0SLFMBD7VX439", { version: true })).toBe(false); + }); + test("when it is a reserved number under version 2, even though its raw checksum happens to be valid", () => { expect(isValidCnpj("00000000000000", { version: 2 })).toBe(false); }); diff --git a/src/is-valid-cnpj/is-valid-cnpj.ts b/src/is-valid-cnpj/is-valid-cnpj.ts index 50bf6049..701f3f55 100644 --- a/src/is-valid-cnpj/is-valid-cnpj.ts +++ b/src/is-valid-cnpj/is-valid-cnpj.ts @@ -67,7 +67,8 @@ const isValidChecksum = (cnpj: string): boolean => { * @param {string} cnpj - The CNPJ value to be validated. * @param {IsValidCnpjOptions} [options] - Optional options. * @param {1|2} [options.version] - `1` validates the numeric-only format (the default), - * `2` validates both the numeric and the alphanumeric formats. + * `2` validates both the numeric and the alphanumeric formats. Any other value is read as `1`, + * as `formatCnpj` and `parseCnpj` do. * @returns {boolean} True if the CNPJ is valid, false otherwise. * * @example @@ -104,7 +105,7 @@ export const isValidCnpj = (cnpj: string, options?: IsValidCnpjOptions): boolean let isNumeric = true; - if (version !== 1) { + if (version === 2) { // Stryker disable next-line EqualityOperator: cleaned.length is always exactly CNPJ_LENGTH here (checked above), so the extra i===CNPJ_LENGTH iteration reads charCodeAt(CNPJ_LENGTH), which is NaN and fails both boundary comparisons either way. for (let i = 0; i < CNPJ_LENGTH; i++) { const code = cleaned.charCodeAt(i); From ff7eda6024b57f15cc12986406573332cf38e0d2 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:44 -0300 Subject: [PATCH 07/14] fix(legal-nature): strip the mask from a numeric code in getLegalNature --- src/get-legal-nature/get-legal-nature.test.ts | 12 ++++++++++++ src/get-legal-nature/get-legal-nature.ts | 8 ++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/get-legal-nature/get-legal-nature.test.ts b/src/get-legal-nature/get-legal-nature.test.ts index ddfa6d6b..d68d0213 100644 --- a/src/get-legal-nature/get-legal-nature.test.ts +++ b/src/get-legal-nature/get-legal-nature.test.ts @@ -30,6 +30,14 @@ describe("getLegalNature", () => { }); }); + it("should strip the mask of a number just like the mask of a string", () => { + expect(getLegalNature(206.2)).toEqual({ + code: "2062", + description: "Sociedade Empresária Limitada", + }); + expect(getLegalNature(206.2)).toEqual(getLegalNature("206.2")); + }); + it("should return the legal nature entry for a masked code (206-2)", () => { expect(getLegalNature("206-2")).toEqual({ code: "2062", @@ -55,6 +63,10 @@ describe("getLegalNature", () => { expect(getLegalNature("")).toBeNull(); }); + it("should return null for an object with no string form, instead of throwing", () => { + expect(getLegalNature(Object.create(null))).toBeNull(); + }); + it("should return null for null", () => { // @ts-expect-error not a string or number expect(getLegalNature(null)).toBeNull(); diff --git a/src/get-legal-nature/get-legal-nature.ts b/src/get-legal-nature/get-legal-nature.ts index e7eaf3de..311cc297 100644 --- a/src/get-legal-nature/get-legal-nature.ts +++ b/src/get-legal-nature/get-legal-nature.ts @@ -19,6 +19,9 @@ const lookUp = (code: string): LegalNature | null => { /** * Looks a Brazilian legal nature (natureza jurídica) code up. * + * The usual mask characters (hyphens, dots, whitespace) are stripped before the lookup, from a + * number as well as from a string, so `getLegalNature(206.2)` resolves like `getLegalNature("206.2")`. + * * @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. @@ -30,11 +33,12 @@ const lookUp = (code: string): LegalNature | null => { * ```typescript * getLegalNature("2062"); // { code: "2062", description: "Sociedade Empresária Limitada" } * getLegalNature("206-2"); // { 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 (typeof value === "number") return lookUp(String(value)); + if (typeof value !== "string" && typeof value !== "number") return null; - return typeof value === "string" ? lookUp(value.replace(MASK_REGEX, "")) : null; + return lookUp(String(value).replace(MASK_REGEX, "")); }; From e476a92d22d5624081acb652267095762f1c96b3 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:44 -0300 Subject: [PATCH 08/14] fix(bank-account): accept only P for a Bradesco remainder of 1 and drop the extinct bank 746 --- src/is-valid-bank-account/constants.ts | 1 - .../is-valid-bank-account.test.ts | 69 +++++++++++++++++-- .../is-valid-bank-account.ts | 7 +- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/is-valid-bank-account/constants.ts b/src/is-valid-bank-account/constants.ts index 00a566a3..73a961a0 100644 --- a/src/is-valid-bank-account/constants.ts +++ b/src/is-valid-bank-account/constants.ts @@ -79,7 +79,6 @@ export const STRUCTURE_ONLY_BANK_CODES = [ "623", "655", "707", - "746", "748", "756", ]; diff --git a/src/is-valid-bank-account/is-valid-bank-account.test.ts b/src/is-valid-bank-account/is-valid-bank-account.test.ts index 0a99d50e..c801ad9a 100644 --- a/src/is-valid-bank-account/is-valid-bank-account.test.ts +++ b/src/is-valid-bank-account/is-valid-bank-account.test.ts @@ -2,8 +2,12 @@ import * as fc from "fast-check"; import { BANKS } from "../_internals/constants/banks"; import { bench, describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; -import { COMPE_CODES } from "./constants"; -import { isValidBankAccount, type IsValidBankAccountOptions } from "./is-valid-bank-account"; +import { COMPE_CODES, STRUCTURE_ONLY_BANK_CODES } from "./constants"; +import { + ALGORITHM_BANK_CODES, + isValidBankAccount, + type IsValidBankAccountOptions, +} from "./is-valid-bank-account"; const BANCO_DO_BRASIL_AGENCY_TOO_LONG_PARAMS = { bankCode: "001", @@ -595,7 +599,7 @@ describe("isValidBankAccount", () => { ).toBe(true); }); - test("when the check digit is P for account 0301357 (sum 67, remainder 1), also accepted rendered as 0, and false when the digit is 1", () => { + test("when the check digit is P for account 0301357 (sum 67, remainder 1), which is the only digit accepted for that remainder", () => { expect( isValidBankAccount({ bankCode: "237", @@ -612,7 +616,7 @@ describe("isValidBankAccount", () => { account: "0301357", digit: "0", }), - ).toBe(true); + ).toBe(false); expect( isValidBankAccount({ @@ -1166,6 +1170,63 @@ describe("isValidBankAccount", () => { }), ).toBe(false); }); + + test("should accept one account of every structure only bank code", () => { + const accepted = STRUCTURE_ONLY_BANK_CODES.filter((bankCode) => + isValidBankAccount({ bankCode, agency: "0001", account: "1234567", digit: "0" }), + ); + + expect(accepted).toStrictEqual([ + "077", + "085", + "102", + "136", + "197", + "208", + "212", + "290", + "318", + "323", + "336", + "380", + "403", + "623", + "655", + "707", + "748", + "756", + ]); + }); + }); + + describe("bank table consistency", () => { + test("should list every structure only bank code in COMPE_CODES and in BANKS", () => { + const missing = STRUCTURE_ONLY_BANK_CODES.filter( + (bankCode) => !LISTED_CODES.has(bankCode) || !BANKS.some((bank) => bank.code === bankCode), + ); + + expect(missing).toStrictEqual([]); + }); + + test("should list every bank code with a published algorithm in COMPE_CODES and in BANKS", () => { + expect([...ALGORITHM_BANK_CODES].sort()).toStrictEqual([ + "001", + "033", + "041", + "104", + "237", + "260", + "341", + "399", + "745", + ]); + + const missing = ALGORITHM_BANK_CODES.filter( + (bankCode) => !LISTED_CODES.has(bankCode) || !BANKS.some((bank) => bank.code === bankCode), + ); + + expect(missing).toStrictEqual([]); + }); }); describe("reference vectors of the already supported banks", () => { diff --git a/src/is-valid-bank-account/is-valid-bank-account.ts b/src/is-valid-bank-account/is-valid-bank-account.ts index ffc99227..bbfd2c8b 100644 --- a/src/is-valid-bank-account/is-valid-bank-account.ts +++ b/src/is-valid-bank-account/is-valid-bank-account.ts @@ -84,7 +84,7 @@ const caixaDigits: BankAccountDigits = (agency, account) => { const bradescoDigits: BankAccountDigits = (_agency, account) => { const digit = mod11(account, { variant: "bank", maxWeight: 7 }); - return digit === 10 ? ["P", "0"] : [String(digit)]; + return digit === 10 ? ["P"] : [String(digit)]; }; const nubankDigits: BankAccountDigits = (_agency, account) => { @@ -181,6 +181,9 @@ const BANK_RULES: Record = { }, }; +/** The bank codes validated by a published check digit algorithm. */ +export const ALGORITHM_BANK_CODES = Object.keys(BANK_RULES); + const STRUCTURE_ONLY_RULE: BankAccountRule = { minAgencyLength: 1, maxAgencyLength: 5, @@ -252,7 +255,7 @@ const sanitizeCheckDigit = (value: string): string => * Banks validated by structure only, because they publish no check digit rule: * Inter (077), Ailos (085), XP (102), Unicred (136), Stone (197), BTG Pactual (208), * Original (212), PagBank (290), BMG (318), Mercado Pago (323), C6 (336), PicPay (380), - * Cora (403), Pan (623), BV (655), Daycoval (707), Modal (746), Sicredi (748) and Sicoob (756). + * Cora (403), Pan (623), BV (655), Daycoval (707), Sicredi (748) and Sicoob (756). * For those the agency and account only need to match the documented digit lengths. * * Every other bank of the list falls back to a generic modulus 10 and modulus 11 check. From 33ebc6eecf96dd0da1a804f03348f6fc3aa29935 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:44 -0300 Subject: [PATCH 09/14] fix(phone): group a 10-digit landline under nanp and auto, never throw on an unknown mask --- src/_internals/constants/area-codes.ts | 14 +++++--- src/format-phone/constants.ts | 19 +++++++++++ src/format-phone/format-phone.test.ts | 22 +++++++++++-- src/format-phone/format-phone.ts | 33 ++++++++++++++++--- .../is-valid-mobile-phone.ts | 5 +++ .../is-valid-service-phone.ts | 4 ++- src/parse-phone/parse-phone.ts | 1 + 7 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/_internals/constants/area-codes.ts b/src/_internals/constants/area-codes.ts index 530d85d2..e2e4b34b 100644 --- a/src/_internals/constants/area-codes.ts +++ b/src/_internals/constants/area-codes.ts @@ -7,14 +7,18 @@ import { type StateCode } from "./states"; * all but a handful of its municipalities, and `AREA_CODE_SECONDARY_STATES` carries the other * states the four cross-border codes also serve. * - * Resolução Anatel nº 749/2022, art. 15, defines the Código Nacional (area code); the gov.br - * page below lists the codes actually allocated and links, under "POR MUNICÍPIO", to the Anexo - * of Resolução Anatel nº 263/2001, which gives the Código Nacional of every municipality. That - * Anexo was parsed to derive both tables. + * Resolução Anatel nº 749/2022, art. 15, defines the Código Nacional (area code). The Plano + * Geral de Códigos Nacionais that art. 15 referred to was revoked by Resolução Anatel nº + * 755/2022, and the allocation in force is the one Despacho Decisório nº 17/2025/PRRE/SPR + * approved; Anatel publishes it on the gov.br page below, which lists the codes actually + * allocated and links, under "POR MUNICÍPIO", to the Anexo of Resolução Anatel nº 263/2001, + * giving the Código Nacional of every municipality. That Anexo was parsed to derive both + * tables. * * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 * @see Official: https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais - * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2001/383-resolucao-263 + * @see Based on: https://informacoes.anatel.gov.br/legislacao/resolucoes/2001/383-resolucao-263 + * Anexo of Resolução nº 263/2001 (revoked; still the table Anatel's Códigos Nacionais page links to). * @see Based on: https://brasilapi.com.br/docs#tag/DDD */ export const VALID_AREA_CODES: readonly number[] = [ diff --git a/src/format-phone/constants.ts b/src/format-phone/constants.ts index e24aafb8..8d3b49e8 100644 --- a/src/format-phone/constants.ts +++ b/src/format-phone/constants.ts @@ -1,5 +1,22 @@ export type NationalMask = "sn" | "nanp"; +/** + * Every value the `PhoneMask` union of `format-phone.ts` allows, as the set `formatPhone` checks + * `options.mask` against before it uses it; anything else is not a mask and resolves to + * `DEFAULT_MASK`. Kept here, out of the mutated sources, next to the masks themselves. + */ +export const PHONE_MASKS: ReadonlySet = new Set([ + "auto", + "e164", + "international", + "nanp", + "service", + "sn", +]); + +/** The mask `formatPhone` applies when `options.mask` is missing or is not a `PhoneMask`. */ +export const DEFAULT_MASK = "sn"; + export const LENGTH: Record = { sn: 9, nanp: 11, @@ -10,6 +27,8 @@ export const MASK: Record = { nanp: "(00) 00000-0000", }; +export const NANP_LANDLINE_MASK = "(00) 0000-0000"; + export const INTERNATIONAL_PREFIX = "+55"; export const INTERNATIONAL_MASK = { diff --git a/src/format-phone/format-phone.test.ts b/src/format-phone/format-phone.test.ts index 807d6cf1..082c7f91 100644 --- a/src/format-phone/format-phone.test.ts +++ b/src/format-phone/format-phone.test.ts @@ -45,10 +45,21 @@ describe("formatPhone", () => { expect(formatPhone("1198888", { mask: "nanp" })).toBe("(11) 98888"); expect(formatPhone("11988887", { mask: "nanp" })).toBe("(11) 98888-7"); expect(formatPhone("119888877", { mask: "nanp" })).toBe("(11) 98888-77"); - expect(formatPhone("1198888777", { mask: "nanp" })).toBe("(11) 98888-777"); + expect(formatPhone("1198888777", { mask: "nanp" })).toBe("(11) 9888-8777"); expect(formatPhone("11988887777", { mask: "nanp" })).toBe("(11) 98888-7777"); }); + it("should group a complete 10 digit landline as (00) 0000-0000", () => { + expect(formatPhone("1130000000", { mask: "nanp" })).toBe("(11) 3000-0000"); + expect(formatPhone("1130000000", { mask: "auto" })).toBe("(11) 3000-0000"); + expect(formatPhone("(11) 3000-0000", { mask: "nanp" })).toBe("(11) 3000-0000"); + }); + + it("should keep the 9 digit sn grouping for a 10 digit value, which only nanp reads as a landline", () => { + expect(formatPhone("1130000000")).toBe("11300-0000"); + expect(formatPhone("1130000000", { mask: "sn" })).toBe("11300-0000"); + }); + it("should auto format phone", () => { expect(formatPhone("", { mask: "auto" })).toBe(""); expect(formatPhone("1", { mask: "auto" })).toBe("1"); @@ -60,7 +71,7 @@ describe("formatPhone", () => { expect(formatPhone("1198888", { mask: "auto" })).toBe("11988-88"); expect(formatPhone("11988887", { mask: "auto" })).toBe("11988-887"); expect(formatPhone("119888877", { mask: "auto" })).toBe("11988-8877"); - expect(formatPhone("1198888777", { mask: "auto" })).toBe("(11) 98888-777"); + expect(formatPhone("1198888777", { mask: "auto" })).toBe("(11) 9888-8777"); expect(formatPhone("11988887777", { mask: "auto" })).toBe("(11) 98888-7777"); }); @@ -149,6 +160,13 @@ describe("formatPhone", () => { expect(formatPhone("40041234", { mask: "international" })).toBe("4004-1234"); }); + it("should fall back to the default mask when mask is outside the union", () => { + // @ts-expect-error: intentionally invalid mask + expect(formatPhone("988887777", { mask: "bogus" })).toBe("98888-7777"); + // @ts-expect-error: intentionally invalid mask + expect(formatPhone("11988887777", { mask: "bogus" })).toBe("11988-8877"); + }); + it("should return an empty string for nullish values", () => { // @ts-expect-error: intentionally invalid input expect(formatPhone(null)).toBe(""); diff --git a/src/format-phone/format-phone.ts b/src/format-phone/format-phone.ts index 1490b7fb..d30e6e1d 100644 --- a/src/format-phone/format-phone.ts +++ b/src/format-phone/format-phone.ts @@ -10,7 +10,17 @@ import { normalizePhone } from "../_internals/normalize-phone/normalize-phone"; import { resolveServicePhoneDigits } from "../_internals/resolve-service-phone-digits/resolve-service-phone-digits"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { isValidServicePhone } from "../is-valid-service-phone/is-valid-service-phone"; -import { INTERNATIONAL_MASK, INTERNATIONAL_PREFIX, LENGTH, MASK, SERVICE_MASK } from "./constants"; +import { + DEFAULT_MASK, + INTERNATIONAL_MASK, + INTERNATIONAL_PREFIX, + LENGTH, + MASK, + NANP_LANDLINE_MASK, + type NationalMask, + PHONE_MASKS, + SERVICE_MASK, +} from "./constants"; /** The masks `formatPhone` can apply. */ export type PhoneMask = "auto" | "e164" | "international" | "service" | "sn" | "nanp"; @@ -59,6 +69,9 @@ const formatInternational = (national: string): string => { const formatE164 = (national: string): string => national ? `${INTERNATIONAL_PREFIX}${national}` : ""; +const resolveNationalPattern = (digits: string, mask: NationalMask): string => + mask === "nanp" && digits.length === PHONE_NATIONAL_MIN_LENGTH ? NANP_LANDLINE_MASK : MASK[mask]; + const resolveAutoMask = (digits: string, serviceDigits: string): Exclude => { if (isValidServicePhone(serviceDigits)) return "service"; @@ -67,6 +80,8 @@ const resolveAutoMask = (digits: string, serviceDigits: string): Exclude LENGTH.sn ? "nanp" : "sn"; }; +const isPhoneMask = (value: unknown): value is PhoneMask => PHONE_MASKS.has(value); + /** * Formats a phone number according to Brazilian phone number patterns. * @@ -76,7 +91,9 @@ const resolveAutoMask = (digits: string, serviceDigits: string): Exclude { diff --git a/src/is-valid-service-phone/is-valid-service-phone.ts b/src/is-valid-service-phone/is-valid-service-phone.ts index e8cd9ba3..24bf5b4f 100644 --- a/src/is-valid-service-phone/is-valid-service-phone.ts +++ b/src/is-valid-service-phone/is-valid-service-phone.ts @@ -23,7 +23,9 @@ const UTILITY_CODES: readonly string[] = SERVICE_PHONE_UTILITY_CODES; * - the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900`, each followed by * 7 digits (11 in total, the shorter, extinct `0800` + 6 form is rejected); * - the abbreviated `300X` and `400X` numbers, followed by 4 digits, e.g. `3003-1234`. Anatel - * publishes no allocation for these, so the accepted roots are the conventional ones; + * publishes no allocation for these, so the accepted roots are the conventional ones. Only + * `300X` and `400X` are recognised: other "Número Único" carrier prefixes in market use, such + * as `4020` and `4062`, are out of scope and are rejected; * - the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated, * e.g. `190` and `192`. Undesignated codes in the `1XX` range are rejected. `112` and `911` * are accepted too: Anatel lists them alongside the `1XX` codes as mobile-only aliases of `190`. diff --git a/src/parse-phone/parse-phone.ts b/src/parse-phone/parse-phone.ts index c5743a22..123d4ffa 100644 --- a/src/parse-phone/parse-phone.ts +++ b/src/parse-phone/parse-phone.ts @@ -24,6 +24,7 @@ import { normalizePhone } from "../_internals/normalize-phone/normalize-phone"; * ``` * * @see Official: https://www.itu.int/rec/T-REC-E.164 + * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 */ export const parsePhone = (value: string | number): string => isNullish(value) ? "" : normalizePhone(value).slice(0, PHONE_NATIONAL_MAX_LENGTH); From 6334f2f227d53cb03449b857210467bec6cc4102 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:44 -0300 Subject: [PATCH 10/14] fix(pix): optional point of initiation, Pix Saque payloads and amounts that round-trip --- src/_internals/constants/pix.ts | 2 + src/generate-pix-payload/constants.ts | 10 ++ .../generate-pix-payload.test.ts | 83 +++++++----- .../generate-pix-payload.ts | 28 +++- .../is-valid-pix-payload.test.ts | 48 +++++-- .../is-valid-pix-payload.ts | 13 +- .../parse-pix-payload.test.ts | 125 +++++++++++++++--- src/parse-pix-payload/parse-pix-payload.ts | 101 ++++++++------ 8 files changed, 293 insertions(+), 117 deletions(-) diff --git a/src/_internals/constants/pix.ts b/src/_internals/constants/pix.ts index 866fb44e..f6e204e8 100644 --- a/src/_internals/constants/pix.ts +++ b/src/_internals/constants/pix.ts @@ -35,6 +35,8 @@ export const PIX_KEY_ID = "01"; export const PIX_DESCRIPTION_ID = "02"; +export const PIX_WITHDRAWAL_FACILITATOR_ID = "03"; + export const PIX_URL_ID = "25"; export const PIX_MERCHANT_CATEGORY_CODE_ID = "52"; diff --git a/src/generate-pix-payload/constants.ts b/src/generate-pix-payload/constants.ts index b839c411..20c7caab 100644 --- a/src/generate-pix-payload/constants.ts +++ b/src/generate-pix-payload/constants.ts @@ -8,6 +8,16 @@ export const AMOUNT_DECIMAL_PLACES = 2; */ export const AMOUNT_REGEX = /^\d+\.\d{2}$/; +/** + * How many decimal places the amount is read at when it is checked against the two decimal + * places actually written. Writing two decimal places rounds anything finer away, so an amount + * that does not survive the round trip is refused instead of being silently changed; reading it + * at ten places instead of comparing the two doubles exactly is what lets the representation + * noise of binary floating point through (`0.1 + 0.2` is `0.30000000000000004`, whose first ten + * decimal places are still `0.3000000000`), while `0.005` is refused. + */ +export const AMOUNT_COMPARISON_DECIMAL_PLACES = 10; + /** * How many characters one TLV object spends besides its value: the 2 digit ID plus the 2 digit * length. diff --git a/src/generate-pix-payload/generate-pix-payload.test.ts b/src/generate-pix-payload/generate-pix-payload.test.ts index 80ec48b2..da8b1f81 100644 --- a/src/generate-pix-payload/generate-pix-payload.test.ts +++ b/src/generate-pix-payload/generate-pix-payload.test.ts @@ -5,7 +5,7 @@ import { describe, expect, expectTypeOf, 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 { type PixPointOfInitiation, parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; import { type GeneratePixPayloadParams, generatePixPayload } from "./generate-pix-payload"; const BASE = { @@ -113,6 +113,12 @@ describe("generatePixPayload", () => { ).toBeNull(); }); + test("when the amount has more than two decimal places, since writing it would change the sum asked for", () => { + expect(generatePixPayload({ ...BASE, amount: 0.005 })).toBeNull(); + expect(generatePixPayload({ ...BASE, amount: 1.005 })).toBeNull(); + expect(generatePixPayload({ ...BASE, amount: 123.456 })).toBeNull(); + }); + test("when a dynamic payload (url) also carries an amount or a txid", () => { expect( generatePixPayload({ @@ -222,10 +228,15 @@ describe("generatePixPayload", () => { 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: 10.1 })).toContain("540510.10"); + expect(generatePixPayload({ ...BASE, amount: 123.45 })).toContain("5406123.45"); expect(generatePixPayload({ ...BASE, amount: 0.01 })).toContain("54040.01"); }); + test("with an amount whose binary representation is not exact, as 0.1 + 0.2 is", () => { + expect(generatePixPayload({ ...BASE, amount: 0.1 + 0.2 })).toContain("54040.30"); + }); + test("with *** as the txid when it is omitted", () => { expect(generatePixPayload(BASE)).toContain("62070503***"); }); @@ -368,49 +379,53 @@ describe("generatePixPayload", () => { }); describe("should round-trip", () => { - test("through isValidPixPayload and parsePixPayload for randomized CPF keys", () => { - for (let index = 0; index < 200; index++) { - const params = { + const ROUND_TRIPS: { + name: string; + build: (index: number) => GeneratePixPayloadParams; + pointOfInitiation: PixPointOfInitiation; + }[] = [ + { + name: "randomized CPF keys", + build: (index) => ({ 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 = { + }), + pointOfInitiation: "static", + }, + { + name: "randomized CNPJ keys", + build: () => ({ 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 = { + }), + pointOfInitiation: "static", + }, + { + name: "randomized dynamic urls", + build: (index) => ({ 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" }); - } - }); + }), + pointOfInitiation: "dynamic", + }, + ]; + + for (const { name, build, pointOfInitiation } of ROUND_TRIPS) { + test(`through isValidPixPayload and parsePixPayload for ${name}`, () => { + for (let index = 0; index < 200; index++) { + const params = build(index); + const payload = generatePixPayload(params) ?? ""; + + expect(isValidPixPayload(payload)).toBe(true); + expect(parsePixPayload(payload)).toEqual({ ...params, pointOfInitiation }); + } + }); + } }); describe("properties", () => { diff --git a/src/generate-pix-payload/generate-pix-payload.ts b/src/generate-pix-payload/generate-pix-payload.ts index 05432ac3..a6640ab4 100644 --- a/src/generate-pix-payload/generate-pix-payload.ts +++ b/src/generate-pix-payload/generate-pix-payload.ts @@ -35,7 +35,13 @@ import { isNullish } from "../_internals/is-nullish/is-nullish"; import { isValidPixUrl } from "../_internals/is-valid-pix-url/is-valid-pix-url"; import { sanitizeToAscii } from "../_internals/sanitize-to-ascii/sanitize-to-ascii"; import { parsePixKey } from "../parse-pix-key/parse-pix-key"; -import { AMOUNT_DECIMAL_PLACES, AMOUNT_REGEX, TLV_OVERHEAD, TXID_REGEX } from "./constants"; +import { + AMOUNT_DECIMAL_PLACES, + AMOUNT_REGEX, + AMOUNT_COMPARISON_DECIMAL_PLACES, + TLV_OVERHEAD, + TXID_REGEX, +} from "./constants"; /** The parameters `generatePixPayload` takes to build a Pix BR Code. */ export type GeneratePixPayloadParams = { @@ -52,7 +58,7 @@ export type GeneratePixPayloadParams = { 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. Not allowed together with `url`: a dynamic BR Code takes its amount from the PSP location. */ + /** Amount in BRL, with at most two decimal places. Omit it to let the payer type it. Not allowed together with `url`: a dynamic BR Code takes its amount from the PSP location. */ amount?: number; /** Transaction ID, 1 to 25 characters of `[A-Za-z0-9]` (default: the absent marker `***`). Not allowed together with `url`. */ txid?: string; @@ -114,6 +120,12 @@ const resolveFormattedAmount = ( if (amount !== undefined && Number(formattedAmount) === 0) return null; + if ( + amount !== undefined && + Number(amount.toFixed(AMOUNT_COMPARISON_DECIMAL_PLACES)) !== Number(formattedAmount) + ) + return null; + if (txid !== undefined && (typeof txid !== "string" || !TXID_REGEX.test(txid))) return null; return formattedAmount; @@ -138,6 +150,11 @@ const resolveFormattedAmount = ( * 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. * + * Object `01` is optional in the Manual do BR Code (`Uso: O`), so writing it only for a dynamic + * payload is one of the shapes the manual allows and follows its own examples; `parsePixPayload` + * accepts the others too. The Pix Saque BR Code, which announces the ISPB of the "facilitador de + * serviço de saque" in sub-object 26-03 (`fss`), is not generated here, only parsed. + * * Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code * composto" of Pix Automático (Pix recorrente) does, are out of scope: the location is always * written in the "Merchant Account Information" template. @@ -146,13 +163,18 @@ const resolveFormattedAmount = ( * (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. * + * `params.amount` is written with the two decimal places the BR Code takes, so an amount that + * does not survive that round trip (`0.005`, `123.456`) is refused rather than rounded into a + * payload that asks the payer for a different sum. + * * @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 {number} [params.amount] - The amount in BRL, with at most two decimal places. 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. 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 index ddd7136e..79e3344e 100644 --- a/src/is-valid-pix-payload/is-valid-pix-payload.test.ts +++ b/src/is-valid-pix-payload/is-valid-pix-payload.test.ts @@ -57,6 +57,38 @@ describe("isValidPixPayload", () => { ), ).toBe(true); }); + + test("when a key payload marks itself single use with the point of initiation method 12", () => { + expect( + isValidPixPayload( + "00020101021226330014br.gov.bcb.pix0111123456789095204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63043CAC", + ), + ).toBe(true); + }); + + test("when a url payload carries the point of initiation method 11, which the manual leaves optional", () => { + expect( + isValidPixPayload( + "00020101021126480014br.gov.bcb.pix2526pix.example.com/qr/v2/12345204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***6304F299", + ), + ).toBe(true); + }); + + test("when a url payload carries no point of initiation method at all", () => { + expect( + isValidPixPayload( + "00020126480014br.gov.bcb.pix2526pix.example.com/qr/v2/12345204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041420", + ), + ).toBe(true); + }); + + test("for a Pix Saque BR Code, whose fss (26-03) goes with a transaction amount of zero", () => { + expect( + isValidPixPayload( + "00020126700014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-42665544000003081234567852040000530398654040.005802BR5913Fulano de Tal6008BRASILIA62070503***63043A07", + ), + ).toBe(true); + }); }); describe("should return false", () => { @@ -122,26 +154,18 @@ describe("isValidPixPayload", () => { ).toBe(false); }); - test("when a key is announced as dynamic by the point of initiation method", () => { - expect( - isValidPixPayload( - "00020101021226330014br.gov.bcb.pix0111123456789095204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63043CAC", - ), - ).toBe(false); - }); - - test("when a url is announced as static by the point of initiation method", () => { + test("when a key payload states a transaction amount of zero without the fss of a Pix Saque", () => { expect( isValidPixPayload( - "00020101021126480014br.gov.bcb.pix2526pix.example.com/qr/v2/12345204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***6304F299", + "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-42665544000052040000530398654040.005802BR5913Fulano de Tal6008BRASILIA62070503***63042451", ), ).toBe(false); }); - test("when a static payload states a transaction amount of zero", () => { + test("when the fss of a Pix Saque is not the 8 digits of an ISPB", () => { expect( isValidPixPayload( - "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-42665544000052040000530398654040.005802BR5913Fulano de Tal6008BRASILIA62070503***63042451", + "00020126690014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400000307123456752040000530398654040.005802BR5913Fulano de Tal6008BRASILIA62070503***630450C2", ), ).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 index c08c56c1..f3203542 100644 --- a/src/is-valid-pix-payload/is-valid-pix-payload.ts +++ b/src/is-valid-pix-payload/is-valid-pix-payload.ts @@ -9,11 +9,14 @@ import { parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; * 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 "Point of Initiation Method" object (`01`) must agree with what - * that template carries: a key requires a static payload (`01` absent or `"11"`) and a URL - * requires a dynamic one (`01` set to `"12"`). A static payload that states a transaction - * amount (`54`) must state one greater than zero: `0.00` is reserved for the Pix Saque/Troco - * BR Code, which is out of scope here. + * the rest of the payload. The "Point of Initiation Method" object (`01`) is advisory: the + * Manual do BR Code marks it `Uso: O` and only assigns a meaning to the value `"12"`, so it may + * be absent from either shape and only a value outside `{"11", "12"}` makes the payload + * invalid. A payload built around a key that states a transaction amount (`54`) must state one + * greater than zero, unless it is a Pix Saque BR Code, i.e. unless it carries the ISPB of the + * "facilitador de serviço de saque" in sub-object 26-03 (`fss`) as §2.6 of the Pix manual + * prescribes; rejecting `"0"`/`"0.00"` without `fss` is a deliberate restriction of this + * library, not a rule of the manual. * * 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` diff --git a/src/parse-pix-payload/parse-pix-payload.test.ts b/src/parse-pix-payload/parse-pix-payload.test.ts index 4239afd3..73c4c1b7 100644 --- a/src/parse-pix-payload/parse-pix-payload.test.ts +++ b/src/parse-pix-payload/parse-pix-payload.test.ts @@ -21,10 +21,10 @@ const BRCODE_MANUAL = const COMMUNITY_STATIC = "00020126580014br.gov.bcb.pix0136bee05743-4291-4f3c-9259-595df1307ba1520400005303986540510.005802BR5914Alexandre Lima6019Presidente Prudente62180514Um-Id-Qualquer6304D475"; -const KEY_ANNOUNCED_AS_DYNAMIC = +const KEY_MARKED_SINGLE_USE = "00020101021226330014br.gov.bcb.pix0111123456789095204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63043CAC"; -const URL_ANNOUNCED_AS_STATIC = +const URL_WITH_STATIC_POINT_OF_INITIATION = "00020101021126480014br.gov.bcb.pix2526pix.example.com/qr/v2/12345204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***6304F299"; const URL_WITHOUT_POINT_OF_INITIATION = @@ -57,6 +57,24 @@ const buildPayload = (merchantAccountInformation: string, additionalData?: strin const MERCHANT_ACCOUNT_INFORMATION = tlv("00", "br.gov.bcb.pix") + tlv("01", "12345678909"); +const WITHDRAWAL_FACILITATOR_ISPB = "12345678"; + +const buildWithdrawalPayload = (fss: string, amount?: string): string => { + const withoutCrc = [ + tlv("00", "01"), + tlv("26", MERCHANT_ACCOUNT_INFORMATION + tlv("03", fss)), + tlv("52", "0000"), + tlv("53", "986"), + amount === undefined ? "" : tlv("54", amount), + tlv("58", "BR"), + tlv("59", "Fulano de Tal"), + tlv("60", "BRASILIA"), + "6304", + ].join(""); + + return withoutCrc + crc16Ccitt(withoutCrc); +}; + const buildPayloadWithMerchantAccountInformationTag = (tag: string): string => { const withoutCrc = [ tlv("00", "01"), @@ -225,19 +243,10 @@ describe("parsePixPayload", () => { ).toBeNull(); }); - test("when a key is announced as dynamic by the point of initiation method", () => { - expect(hasValidCrc(KEY_ANNOUNCED_AS_DYNAMIC)).toBe(true); - expect(parsePixPayload(KEY_ANNOUNCED_AS_DYNAMIC)).toBeNull(); - }); - - test("when a url is announced as static by the point of initiation method", () => { - expect(hasValidCrc(URL_ANNOUNCED_AS_STATIC)).toBe(true); - expect(parsePixPayload(URL_ANNOUNCED_AS_STATIC)).toBeNull(); - }); - - test("when a url carries no point of initiation method at all", () => { - expect(hasValidCrc(URL_WITHOUT_POINT_OF_INITIATION)).toBe(true); - expect(parsePixPayload(URL_WITHOUT_POINT_OF_INITIATION)).toBeNull(); + test("when the fss of a Pix Saque is not the 8 digits of an ISPB", () => { + expect(parsePixPayload(buildWithdrawalPayload("1234567", "0.00"))).toBeNull(); + expect(parsePixPayload(buildWithdrawalPayload("123456789", "0.00"))).toBeNull(); + expect(parsePixPayload(buildWithdrawalPayload("1234567x", "0.00"))).toBeNull(); }); test("when the additional data template is malformed", () => { @@ -279,7 +288,7 @@ describe("parsePixPayload", () => { expect(parsePixPayload(buildPayloadWithAmount("1.234"))).toBeNull(); }); - test("when a static payload states a transaction amount of zero", () => { + test("when a key payload states a transaction amount of zero without the fss of a Pix Saque", () => { expect(hasValidCrc(buildPayloadWithAmount("0.00"))).toBe(true); expect(parsePixPayload(buildPayloadWithAmount("0.00"))).toBeNull(); expect(parsePixPayload(buildPayloadWithAmount("0"))).toBeNull(); @@ -318,11 +327,54 @@ describe("parsePixPayload", () => { }); }); - test("from the static QR Code example in the Bacen 'Manual de Padrões para Iniciação do Pix'", () => { - expect(parsePixPayload(BACEN_STATIC)).toEqual({ + test("from the static QR Code example in the Bacen 'Manual de Padrões para Iniciação do Pix', with no key of its own for a field the payload does not carry", () => { + expect(parsePixPayload(BACEN_STATIC)).toStrictEqual({ key: "123e4567-e12b-12d1-a456-426655440000", merchantName: "Fulano de Tal", merchantCity: "BRASILIA", + pointOfInitiation: "static", + }); + }); + + test("for a Pix Saque BR Code, reading back the fss (26-03) of §2.6 with a transaction amount of zero", () => { + expect(parsePixPayload(buildWithdrawalPayload(WITHDRAWAL_FACILITATOR_ISPB, "0.00"))).toEqual({ + key: "12345678909", + withdrawalFacilitator: "12345678", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", + amount: 0, + pointOfInitiation: "static", + }); + }); + + test("for a Pix Saque BR Code whose amount is written as the plain '0' of the BR Code field table", () => { + expect(parsePixPayload(buildWithdrawalPayload(WITHDRAWAL_FACILITATOR_ISPB, "0"))).toEqual({ + key: "12345678909", + withdrawalFacilitator: "12345678", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", + amount: 0, + pointOfInitiation: "static", + }); + }); + + test("for a Pix Saque BR Code that states no transaction amount at all", () => { + expect(parsePixPayload(buildWithdrawalPayload(WITHDRAWAL_FACILITATOR_ISPB))).toEqual({ + key: "12345678909", + withdrawalFacilitator: "12345678", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", + pointOfInitiation: "static", + }); + }); + + test("marked single use by the point of initiation method 12, which the manual allows on any BR Code", () => { + expect(hasValidCrc(KEY_MARKED_SINGLE_USE)).toBe(true); + expect(parsePixPayload(KEY_MARKED_SINGLE_USE)).toEqual({ + key: "12345678909", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", + pointOfInitiation: "dynamic", }); }); @@ -337,6 +389,7 @@ describe("parsePixPayload", () => { merchantCity: "Presidente Prudente", amount: 10, txid: "Um-Id-Qualquer", + pointOfInitiation: "static", }); }); @@ -347,6 +400,7 @@ describe("parsePixPayload", () => { merchantCity: "BRASILIA", amount: 123.45, txid: "RP12345678-2019", + pointOfInitiation: "static", }); }); @@ -355,6 +409,7 @@ describe("parsePixPayload", () => { key: "12345678909", merchantName: "Fulano de Tal", merchantCity: "BRASILIA", + pointOfInitiation: "static", }); }); @@ -395,6 +450,7 @@ describe("parsePixPayload", () => { description: "Pedido 42", merchantName: "Fulano de Tal", merchantCity: "Brasilia", + pointOfInitiation: "static", }); }); }); @@ -421,6 +477,26 @@ describe("parsePixPayload", () => { 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"); }); + + test("when it carries no point of initiation method at all, which the manual marks optional", () => { + expect(hasValidCrc(URL_WITHOUT_POINT_OF_INITIATION)).toBe(true); + expect(parsePixPayload(URL_WITHOUT_POINT_OF_INITIATION)).toEqual({ + url: "pix.example.com/qr/v2/1234", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", + pointOfInitiation: "dynamic", + }); + }); + + test("when the point of initiation method is 11, since the PSP location is what makes it dynamic", () => { + expect(hasValidCrc(URL_WITH_STATIC_POINT_OF_INITIATION)).toBe(true); + expect(parsePixPayload(URL_WITH_STATIC_POINT_OF_INITIATION)).toEqual({ + url: "pix.example.com/qr/v2/1234", + merchantName: "Fulano de Tal", + merchantCity: "BRASILIA", + pointOfInitiation: "dynamic", + }); + }); }); describe("should round-trip with generatePixPayload", () => { @@ -434,7 +510,10 @@ describe("parsePixPayload", () => { txid: "RP123456782019", }; - expect(parsePixPayload(generatePixPayload(pix) ?? "")).toEqual(pix); + expect(parsePixPayload(generatePixPayload(pix) ?? "")).toEqual({ + ...pix, + pointOfInitiation: "static", + }); }); test("for randomized CPF keys", () => { @@ -447,7 +526,10 @@ describe("parsePixPayload", () => { txid: `TX${index}`, }; - expect(parsePixPayload(generatePixPayload(pix) ?? "")).toEqual(pix); + expect(parsePixPayload(generatePixPayload(pix) ?? "")).toEqual({ + ...pix, + pointOfInitiation: "static", + }); } }); }); @@ -505,11 +587,12 @@ describe("parsePixPayload types", () => { key?: string; url?: string; description?: string; + withdrawalFacilitator?: string; merchantName: string; merchantCity: string; amount?: number; txid?: string; - pointOfInitiation?: PixPointOfInitiation; + pointOfInitiation: PixPointOfInitiation; }>(); expectTypeOf().toEqualTypeOf<"static" | "dynamic">(); }); diff --git a/src/parse-pix-payload/parse-pix-payload.ts b/src/parse-pix-payload/parse-pix-payload.ts index e563e71f..477c0d5a 100644 --- a/src/parse-pix-payload/parse-pix-payload.ts +++ b/src/parse-pix-payload/parse-pix-payload.ts @@ -25,12 +25,17 @@ import { PIX_TRANSACTION_CURRENCY_ID, PIX_TXID_ID, PIX_URL_ID, + PIX_WITHDRAWAL_FACILITATOR_ID, } from "../_internals/constants/pix"; import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; import { isValidPixUrl } from "../_internals/is-valid-pix-url/is-valid-pix-url"; import { type TlvFields, parseTlv } from "../_internals/parse-tlv/parse-tlv"; -/** Whether a Pix BR Code may be paid many times (`"static"`) or only once (`"dynamic"`). */ +/** + * How a Pix BR Code is meant to be presented for payment: `"dynamic"` when it carries a PSP + * location or when the "Point of Initiation Method" object (`01`) is `"12"`, the value the + * Manual do BR Code reads as "só pode ser utilizado uma vez"; `"static"` otherwise. + */ export type PixPointOfInitiation = "static" | "dynamic"; /** The fields `parsePixPayload` reads out of a Pix BR Code. */ @@ -41,6 +46,11 @@ export type PixPayload = { url?: string; /** Free text the receiver wrote for the payer. */ description?: string; + /** + * The 8 digit ISPB of the "facilitador de serviço de saque" (`fss`, sub-object 26-03), + * present only in a Pix Saque BR Code. + */ + withdrawalFacilitator?: string; /** Name of the receiver, at most 25 ASCII characters. */ merchantName: string; /** City of the receiver, at most 15 ASCII characters. */ @@ -49,8 +59,8 @@ export type PixPayload = { 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; + /** Whether the payload is presented as a single use one ("dynamic") or not ("static"). */ + pointOfInitiation: PixPointOfInitiation; }; // Stryker disable next-line Regex: this is only ever tested against `checksum`, a slice of exactly PIX_CRC_LENGTH (4) characters, so dropping either anchor cannot change whether it matches @@ -58,6 +68,8 @@ const CRC_VALUE_REGEX = /^[0-9a-f]{4}$/i; const AMOUNT_REGEX = /^\d+(?:\.\d{1,2})?$/; +const WITHDRAWAL_FACILITATOR_REGEX = /^\d{8}$/; + const CRC_TAG_LENGTH = PIX_CRC_TAG.length + PIX_CRC_LENGTH; const findMerchantAccountInformation = (fields: TlvFields): TlvFields | null => { @@ -103,18 +115,22 @@ const resolvePointOfInitiation = (fields: TlvFields): string | undefined | null return pointOfInitiation; }; -const isValidAmount = (amount: string | undefined, isDynamic: boolean): boolean => { +const isValidAmount = ( + amount: string | undefined, + { url, withdrawalFacilitator }: MerchantKeyInfo, +): boolean => { if (amount === undefined) return true; if (!AMOUNT_REGEX.test(amount) || amount.length > PIX_TRANSACTION_AMOUNT_MAX_LENGTH) return false; - return isDynamic || Number(amount) > 0; + return url !== undefined || withdrawalFacilitator !== undefined || Number(amount) > 0; }; type MerchantKeyInfo = { key?: string; url?: string; description?: string; + withdrawalFacilitator?: string; }; const resolveMerchantKeyInfo = (fields: TlvFields): MerchantKeyInfo | null => { @@ -125,22 +141,21 @@ const resolveMerchantKeyInfo = (fields: TlvFields): MerchantKeyInfo | null => { const key = merchantAccountInformation[PIX_KEY_ID]; const url = merchantAccountInformation[PIX_URL_ID]; const description = merchantAccountInformation[PIX_DESCRIPTION_ID]; + const withdrawalFacilitator = merchantAccountInformation[PIX_WITHDRAWAL_FACILITATOR_ID]; if ((key === undefined) === (url === undefined)) return null; if (key !== undefined && !key) return null; if (url !== undefined && !isValidPixUrl(url)) return null; + if ( + withdrawalFacilitator !== undefined && + !WITHDRAWAL_FACILITATOR_REGEX.test(withdrawalFacilitator) + ) { + return null; + } - return { key, url, description }; + return { key, url, description, withdrawalFacilitator }; }; -const isConsistentPointOfInitiation = ( - { url }: MerchantKeyInfo, - pointOfInitiation: string | undefined, -): boolean => - url === undefined - ? pointOfInitiation !== PIX_DYNAMIC_POINT_OF_INITIATION - : pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION; - const resolveTxid = (fields: TlvFields): string | undefined | null => { const additionalData = fields[PIX_ADDITIONAL_DATA_ID]; @@ -153,10 +168,7 @@ const resolveTxid = (fields: TlvFields): string | undefined | null => { return objects[PIX_TXID_ID]; }; -type OptionalPixFields = { - key?: string; - url?: string; - description?: string; +type OptionalPixFields = MerchantKeyInfo & { amount?: string; txid?: string; pointOfInitiation?: string; @@ -167,22 +179,22 @@ const buildPixPayload = ( merchantCity: string, optional: OptionalPixFields, ): PixPayload => { - const { key, url, description, amount, txid, pointOfInitiation } = optional; - const pix: PixPayload = { merchantName, merchantCity }; + const { key, url, description, withdrawalFacilitator, amount, txid, pointOfInitiation } = + optional; + const isDynamic = url !== undefined || pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION; + const pix: PixPayload = { + merchantName, + merchantCity, + pointOfInitiation: isDynamic ? "dynamic" : "static", + }; if (key !== undefined) pix.key = key; if (url !== undefined) pix.url = url; if (description !== undefined) pix.description = description; + if (withdrawalFacilitator !== undefined) pix.withdrawalFacilitator = withdrawalFacilitator; - const isDynamic = pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION; - - if (amount !== undefined && !isDynamic) pix.amount = Number(amount); - if (txid !== undefined && txid !== PIX_ABSENT_TXID && !isDynamic) pix.txid = txid; - - if (pointOfInitiation !== undefined) { - pix.pointOfInitiation = - pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION ? "dynamic" : "static"; - } + if (amount !== undefined && url === undefined) pix.amount = Number(amount); + if (txid !== undefined && txid !== PIX_ABSENT_TXID && url === undefined) pix.txid = txid; return pix; }; @@ -209,17 +221,23 @@ const buildPixPayload = ( * * The merchant account information must carry exactly one of a Pix key (26-01) or a PSP * location (26-25); the location is checked with the same host and path rule - * `generatePixPayload` applies. The "Point of Initiation Method" object (`01`) must agree with - * it: a key belongs to a static payload, so `01` is absent or `"11"`, and a PSP location - * belongs to a dynamic one, so `01` is `"12"`. Any other pairing (a key announced as dynamic, - * a location announced as static) is rejected. In a dynamic payload the transaction amount - * (54) and the `txid` (62-05) are ignored, as the manual mandates, because the PSP location is - * the source of truth for both. + * `generatePixPayload` applies. The "Point of Initiation Method" object (`01`) is advisory, as + * the Manual do BR Code marks it `Uso: O` and only assigns a meaning to the value `"12"` + * ("Se o valor 12 estiver presente, significa que o BR Code só pode ser utilizado uma vez"): + * it may be absent from either shape, and only a value outside `{"11", "12"}` is rejected. + * `pointOfInitiation` is reported as `"dynamic"` when the payload carries a PSP location or + * when `01` is `"12"`, and as `"static"` otherwise. When the payload carries a PSP location the + * transaction amount (54) and the `txid` (62-05) are ignored, as the manual mandates, because + * the PSP location is the source of truth for both. * - * A static payload that carries the transaction amount (54) must state an amount greater than - * zero: the only BR Code the manual writes with `54` set to `0.00` is a Pix Saque/Troco one, - * which announces the withdrawal agent in a template this parser does not read, so a static - * `"0"`/`"0.00"` is rejected rather than reported as a free amount of nothing. + * A payload built around a Pix key that carries the transaction amount (54) must state an + * amount greater than zero, unless it is a Pix Saque BR Code: §2.6 of the Pix manual puts the + * ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`) of the same + * template this parser already reads, and states that "a presença do campo fss, com um ISPB + * válido […] indica que esse é um QR Code para Pix Saque", whose amount is settled at payment + * time. So `54` set to `"0"` or `"0.00"` is accepted together with `fss` and rejected without + * it; that rejection is a deliberate restriction of this library, not a rule of the manual, + * whose field table allows `"0"` in any payload. A `fss` that is not 8 digits is rejected. * * @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 @@ -235,6 +253,7 @@ const buildPixPayload = ( * // key: "123e4567-e12b-12d1-a456-426655440000", * // merchantName: "Fulano de Tal", * // merchantCity: "BRASILIA", + * // pointOfInitiation: "static", * // } * ``` * @@ -277,11 +296,9 @@ export const parsePixPayload = (value: string): PixPayload | null => { if (!merchantKeyInfo) return null; - if (!isConsistentPointOfInitiation(merchantKeyInfo, pointOfInitiation)) return null; - const amount = fields[PIX_TRANSACTION_AMOUNT_ID]; - if (!isValidAmount(amount, merchantKeyInfo.url !== undefined)) return null; + if (!isValidAmount(amount, merchantKeyInfo)) return null; const txid = resolveTxid(fields); From 60fcb46385072d915d50e824f6c3d9fec2943303 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:44 -0300 Subject: [PATCH 11/14] feat(nfe-key): accept every 44-digit DF-e model, the SVC-SP emission and the XML id prefixes --- src/format-nfe-key/format-nfe-key.ts | 6 +- src/is-valid-nfe-key/is-valid-nfe-key.test.ts | 54 +++++--- src/is-valid-nfe-key/is-valid-nfe-key.ts | 35 ++++-- src/parse-nfe-key/constants.ts | 98 +++++++++++++-- src/parse-nfe-key/parse-nfe-key.test.ts | 116 ++++++++++++++++-- src/parse-nfe-key/parse-nfe-key.ts | 114 +++++++++++++---- 6 files changed, 355 insertions(+), 68 deletions(-) diff --git a/src/format-nfe-key/format-nfe-key.ts b/src/format-nfe-key/format-nfe-key.ts index ab942b29..bc25ee87 100644 --- a/src/format-nfe-key/format-nfe-key.ts +++ b/src/format-nfe-key/format-nfe-key.ts @@ -4,8 +4,10 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d import { PATTERN } from "./constants"; /** - * Formats a DF-e (NF-e, NFC-e, CT-e, MDF-e or CT-e OS) access key (chave de acesso) into - * groups of 4 digits separated by spaces, the common display form printed on the DANFE. + * Formats a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) into groups of 4 + * digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the + * NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the + * MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. * * @param {string} value - The access key value to be formatted. * @returns {string} The formatted access key, e.g. "3520 0612 3456 ...". 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 index e6aa5ea0..ebf3790d 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts @@ -5,7 +5,7 @@ import { isValidNfeKey } from "./is-valid-nfe-key"; const VALID_A = "35120859597245000190550000000095831710040056"; const VALID_B = "35170458716523000119550010000000121000123458"; -const VALID_C = "35170358716523000119550010000000301000000300"; +const CNF_EQUAL_TO_NNF = "35170358716523000119550010000000301000000300"; const VALID_D = "43160472202112000136550000000010571048440722"; const INVALID_TYPE = "42100484684182000157550010000000020108042108"; @@ -23,10 +23,6 @@ describe("isValidNfeKey", () => { 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); }); @@ -35,6 +31,27 @@ describe("isValidNfeKey", () => { expect(isValidNfeKey(`NFe${VALID_B}`)).toBe(true); }); + test("when it has the XML Id prefix of one of the other covered documents", () => { + expect(isValidNfeKey("CTe35170458716523000119570010000000121000123455")).toBe(true); + expect(isValidNfeKey("MDFe35170458716523000119580010000000121000123459")).toBe(true); + expect(isValidNfeKey("BPe35170458716523000119630010000000121000123453")).toBe(true); + expect(isValidNfeKey("NF3e35170458716523000119660010000000121000123454")).toBe(true); + expect(isValidNfeKey("nfcom35170458716523000119620010000000121000123450")).toBe(true); + }); + + test("for the four models added beside the NF-e family: NFCom (62), BP-e (63), GTV-e (64) and NF3e (66)", () => { + expect(isValidNfeKey("35170458716523000119620010000000121000123450")).toBe(true); + expect(isValidNfeKey("35170458716523000119630010000000121000123453")).toBe(true); + expect(isValidNfeKey("35170458716523000119640010000000121000123457")).toBe(true); + expect(isValidNfeKey("35170458716523000119660010000000121000123454")).toBe(true); + }); + + test("for a CT-e, a CT-e OS and a GTV-e authorised by the SVC-SP, whose MOC assigns tpEmis 8", () => { + expect(isValidNfeKey("35170458716523000119570010000000128000123452")).toBe(true); + expect(isValidNfeKey("35170458716523000119670010000000128000123455")).toBe(true); + expect(isValidNfeKey("35170458716523000119640010000000128000123454")).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); }); @@ -99,10 +116,19 @@ describe("isValidNfeKey", () => { expect(isValidNfeKey(`00${VALID_B.slice(2)}`)).toBe(false); }); - test("when the mod is not 55, 57, 58, 65 or 67", () => { + test("when the mod is not one of the nine supported", () => { expect(isValidNfeKey(`${VALID_B.slice(0, 20)}99${VALID_B.slice(22)}`)).toBe(false); }); + test("for the NFePHP `Keys::isValid` doc example, whose cNF equals its nNF (rule B03-10)", () => { + expect(isValidNfeKey(CNF_EQUAL_TO_NNF)).toBe(false); + }); + + test("when the cNF of an NF-e is one of the codes rule B03-10 lists", () => { + expect(isValidNfeKey("35170458716523000119550010000000121000000003")).toBe(false); + expect(isValidNfeKey("35170458716523000119550010000000121123456781")).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); @@ -130,32 +156,32 @@ describe("isValidNfeKey", () => { const CASES: { name: string; key: string; expected: boolean }[] = [ { name: "an unmapped cUF (99)", - key: "99200600000000000000550010000000011000000005", + key: "99200600000000000000550010000000011000000129", expected: false, }, { name: "month 00, below the valid range", - key: "35200000000000000000550010000000011000000006", + key: "35200000000000000000550010000000011000000120", expected: false, }, { name: "month 01, the lower boundary", - key: "35200100000000000000550010000000011000000000", + key: "35200100000000000000550010000000011000000123", expected: true, }, { name: "month 12, the upper boundary", - key: "35201200000000000000550010000000011000000006", + key: "35201200000000000000550010000000011000000120", expected: true, }, { name: "month 13, above the valid range", - key: "35201300000000000000550010000000011000000000", + key: "35201300000000000000550010000000011000000123", expected: false, }, { name: "a model not in VALID_MODELS (99)", - key: "35200600000000000000990010000000011000000003", + key: "35200600000000000000990010000000011000000127", expected: false, }, { @@ -165,11 +191,11 @@ describe("isValidNfeKey", () => { }, { name: "tpEmis 9, the upper boundary", - key: "35200600000000000000550010000000019000000003", + key: "35200600000000000000550010000000019000000127", expected: true, }, { - name: "tpEmis 8, a code the MOC does not assign", + name: "tpEmis 8, a code the NF-e MOC does not assign", key: "35170458716523000119550010000000128000123455", expected: 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 index 2d07e9c5..b0edffe7 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.ts @@ -3,24 +3,43 @@ import { parseNfeKey } from "../parse-nfe-key/parse-nfe-key"; /** * 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), MDF-e (modelo 58) and CT-e OS (modelo 67, the Conhecimento de - * Transporte Eletrônico para Outros Serviços). 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. + * Covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e + * (65), CT-e (57), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para + * Outros Serviços), GTV-e (64, the CT-e Guia de Transporte de Valores), BP-e (63), NF3e (66) + * and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed + * differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, + * `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the + * document's XML (e.g. `Id="NFe3517...`), which are 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)`. - * `tpEmis` must be one of the codes the MOC assigns, 1 to 7 or 9; 8 is not assigned. + * The key is `cUF(2) AAMM(4) CNPJ/CPF(14) mod(2) serie(3) nNF(9) tpEmis(1) cNF(8) cDV(1)`, with + * NFCom and NF3e spending position 36 on `nSiteAutoriz` and leaving 7 digits for `cNF`. + * `tpEmis` must be one of the codes the MOC of that model assigns, so the accepted set changes + * with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, + * `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and + * `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is + * assigned by the CT-e MOC only, never by the NF-e one. * 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. * + * For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, + * which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to + * the document number. + * * @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 Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/aj_009_07 + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07 * Ajuste SINIEF 09/07, cláusula primeira, § 3.º, II, "b": the CT-e OS, modelo 67. + * @see Official: https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais + * CT-e MOC 4.00, Anexo I: modelo 64 (GTV-e) and the `tpEmis` domains D19, D27 and D15. + * @see Official: https://dfe-portal.svrs.rs.gov.br/BPE/Documentos + * BP-e MOC 1.00b, Visão Geral and Anexo I: modelo 63. + * @see Official: https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos + * NF3e MOC 1.00a, Visão Geral and Anexo I: modelo 66 and `nSiteAutoriz`. + * @see Official: https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos + * NFCom MOC 1.00a, Visão Geral and Anexo I: modelo 62 and `nSiteAutoriz`. * @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 diff --git a/src/parse-nfe-key/constants.ts b/src/parse-nfe-key/constants.ts index 82beff4c..95a3726d 100644 --- a/src/parse-nfe-key/constants.ts +++ b/src/parse-nfe-key/constants.ts @@ -1,20 +1,96 @@ /** - * Valid `mod` (modelo do documento) values shared by every DF-e access key: 55 NF-e, 57 CT-e, - * 58 MDF-e, 65 NFC-e and 67 CT-e OS (Conhecimento de Transporte Eletrônico para Outros - * Serviços), the model the CT-e MOC assigns to the transporte de pessoas, valores e excesso de - * bagagem, which shares the same 44 digit key. + * The `mod` (modelo do documento) values `parseNfeKey` supports, every one of them a document + * whose "chave de acesso" is the same 44 digit string built the same way: 55 NF-e, 57 CT-e, + * 58 MDF-e, 62 NFCom, 63 BP-e, 64 GTV-e (the CT-e Guia de Transporte de Valores), 65 NFC-e, + * 66 NF3e and 67 CT-e OS (Conhecimento de Transporte Eletrônico para Outros Serviços). + * + * Model 59, the CF-e-SAT, is deliberately left out: its 44 position "chave de consulta" is + * composed differently, around a 9 digit SAT serial number and with no `tpEmis` field, so the + * layout below does not describe it. */ -export const VALID_MODELS = ["55", "57", "58", "65", "67"] as const; +export const VALID_MODELS = ["55", "57", "58", "62", "63", "64", "65", "66", "67"] as const; + +/** One of the `mod` values `parseNfeKey` supports. */ +export type ValidModel = (typeof VALID_MODELS)[number]; + +/** + * The `tpEmis` (forma de emissão) codes each MOC assigns to its own document, so a code that is + * meaningful for one document does not make a key of another valid. + * + * NF-e and NFC-e (MOC 7.0 Anexo I, field B22): 1 normal, 2 contingência FS-IA, 3 contingência + * SCAN, 4 contingência EPEC, 5 contingência FS-DA, 6 contingência SVC-AN, 7 contingência SVC-RS + * and 9 contingência off-line da NFC-e. + * + * CT-e (CT-e MOC 4.00 Anexo I, field D19): 1 normal, 3 Regime Especial NFF, 4 EPEC pela SVC, + * 5 contingência FS-DA, 7 autorização pela SVC-RS and 8 autorização pela SVC-SP. CT-e OS + * (field D27) drops 3 and 4, and the GTV-e (field D15) uses 1, 2 contingência off-line, 7 and + * 8. Rule G011 of the same annex, "(7=SVC-RS e 8=SVC-SP)", is what makes 8 a real code here, + * even though the NF-e MOC never assigns it. + * + * MDF-e (MDF-e MOC 3.00 Anexo I, domain D7): 1 normal, 2 contingência off-line and 3 Regime + * Especial NFF. NFCom, BP-e and NF3e (their own Anexo I, domain D7): 1 normal and + * 2 contingência off-line. + */ +export const EMISSION_TYPES_BY_MODEL: Readonly> = { + "55": [1, 2, 3, 4, 5, 6, 7, 9], + "57": [1, 3, 4, 5, 7, 8], + "58": [1, 2, 3], + "62": [1, 2], + "63": [1, 2], + "64": [1, 2, 7, 8], + "65": [1, 2, 3, 4, 5, 6, 7, 9], + "66": [1, 2], + "67": [1, 5, 7, 8], +}; + +/** + * The models whose key spends position 36 on `nSiteAutoriz`, the site of the authorizer that + * received the document, leaving 7 digits for the numeric code: NFCom (MOC 1.00a Visão Geral + * §2.1.3) and NF3e (MOC 1.00a Visão Geral). Every other model writes an 8 digit code there. + */ +export const AUTHORIZATION_SITE_MODELS: readonly string[] = ["62", "66"]; + +/** + * The `cNF` values rule B03-10 of the NF-e MOC turns down: "cNF não pode ser igual a 00000000, + * 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888, 99999999, + * 12345678, 23456789, 34567890, 45678901, 56789012, 67890123, 78901234, 89012345, 90123456, + * 01234567". + */ +export const FORBIDDEN_CODES: readonly string[] = [ + "00000000", + "11111111", + "22222222", + "33333333", + "44444444", + "55555555", + "66666666", + "77777777", + "88888888", + "99999999", + "12345678", + "23456789", + "34567890", + "45678901", + "56789012", + "67890123", + "78901234", + "89012345", + "90123456", + "01234567", +]; + +/** The models rule B03-10 is written for, the only ones whose `cNF` it constrains. */ +export const FORBIDDEN_CODE_MODELS: readonly string[] = ["55", "65"]; /** - * The `tpEmis` (forma de emissão) codes the MOC assigns: 1 normal, 2 contingência FS-IA, - * 3 contingência SCAN, 4 contingência DPEC/EPEC, 5 contingência FS-DA, 6 contingência SVC-AN, - * 7 contingência SVC-RS and 9 contingência off-line da NFC-e. 8 is not assigned. + * The prefixes the `Id` attribute of a DF-e XML puts in front of the 44 digits, one per + * document: `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom`. Stripped before the digits are + * read, since `NF3e` carries a digit of its own. */ -export const VALID_EMISSION_TYPES: readonly number[] = [1, 2, 3, 4, 5, 6, 7, 9]; +export const XML_ID_PREFIX_REGEX = /^(?:nfe|cte|mdfe|bpe|nf3e|nfcom)/i; -/** Digits, optional whitespace between groups, optional `NFe` prefix from the XML `Id` attribute. */ -export const FORMAT_REGEX = /^(?:nfe)?[\d\s]+$/i; +/** Digits and optional whitespace between groups, what is left once the prefix is stripped. */ +export const FORMAT_REGEX = /^[\d\s]+$/; /** Start of the document number (nNF) inside the 44 digit key. */ export const NUMBER_START = 25; diff --git a/src/parse-nfe-key/parse-nfe-key.test.ts b/src/parse-nfe-key/parse-nfe-key.test.ts index b10b6a5b..bad695aa 100644 --- a/src/parse-nfe-key/parse-nfe-key.test.ts +++ b/src/parse-nfe-key/parse-nfe-key.test.ts @@ -3,7 +3,7 @@ import * as fc from "fast-check"; import { IBGE_UF_CODES } from "../_internals/constants/ibge-uf-codes"; import { type StateCode } from "../_internals/constants/states"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; -import { VALID_EMISSION_TYPES, VALID_MODELS } from "./constants"; +import { EMISSION_TYPES_BY_MODEL, FORBIDDEN_CODES, VALID_MODELS } from "./constants"; import { parseNfeKey, type NfeKey, type NfeKeyModel } from "./parse-nfe-key"; const KEY_SP = "35170458716523000119550010000000121000123458"; @@ -12,6 +12,12 @@ const KEY_CPF_PADDED = "35170400040364478829550010000000121000123457"; const CHECK_DIGITS = Array.from({ length: 10 }, (_, digit) => String(digit)); +const AUTHORIZATION_SITE_MODELS = new Set(["62", "66"]); + +const MODEL_EMISSION_TYPES: { model: string; emissionType: number }[] = VALID_MODELS.flatMap( + (model) => EMISSION_TYPES_BY_MODEL[model].map((emissionType) => ({ model, emissionType })), +); + const buildNfeKey = (base: string): string => CHECK_DIGITS.map((digit) => `${base}${digit}`).find((key) => parseNfeKey(key) !== null) ?? ""; @@ -40,7 +46,7 @@ describe("parseNfeKey", () => { expect(parseNfeKey(`${KEY_SP.slice(0, 43)}9`)).toBeNull(); }); - test("when the model is not 55, 57, 58, 65 or 67 (model 99 with a matching check digit)", () => { + test("when the model is not one of the nine supported (model 99 with a matching check digit)", () => { expect(parseNfeKey("35170458716523000119990010000000121000123453")).toBeNull(); }); @@ -48,10 +54,27 @@ describe("parseNfeKey", () => { expect(parseNfeKey("35170458716523000119550010000000001000123457")).toBeNull(); }); - test("when tpEmis is 8, a code the MOC does not assign, even with a matching check digit", () => { + test("when tpEmis is 8, which the NF-e MOC does not assign, even with a matching check digit", () => { expect(parseNfeKey("35170458716523000119550010000000128000123455")).toBeNull(); }); + test("when tpEmis belongs to another model: 2 for a CT-e, 3 for a CT-e OS, 9 for an MDF-e, 3 for a BP-e", () => { + expect(parseNfeKey("35170458716523000119570010000000122000123453")).toBeNull(); + expect(parseNfeKey("35170458716523000119670010000000123000123454")).toBeNull(); + expect(parseNfeKey("35170458716523000119580010000000129000123454")).toBeNull(); + expect(parseNfeKey("35170458716523000119630010000000123000123450")).toBeNull(); + }); + + test("when the cNF of an NF-e is one rule B03-10 of the MOC forbids", () => { + expect(parseNfeKey("35170458716523000119550010000000121000000003")).toBeNull(); + expect(parseNfeKey("35170458716523000119550010000000121111111113")).toBeNull(); + expect(parseNfeKey("35170458716523000119550010000000121123456781")).toBeNull(); + }); + + test("when the cNF of an NF-e equals its nNF, the second half of rule B03-10", () => { + expect(parseNfeKey("35170458716523000119550010000123451000123458")).toBeNull(); + }); + test("when the access key is otherwise invalid", () => { expect(parseNfeKey("not-a-key")).toBeNull(); }); @@ -95,6 +118,29 @@ describe("parseNfeKey", () => { ); }); + test("accepting the XML Id prefix of every other covered document", () => { + expect(parseNfeKey("CTe35170458716523000119570010000000121000123455")?.model).toBe("57"); + expect(parseNfeKey("MDFe35170458716523000119580010000000121000123459")?.model).toBe("58"); + expect(parseNfeKey("BPe35170458716523000119630010000000121000123453")?.model).toBe("63"); + expect(parseNfeKey("NF3e35170458716523000119660010000000121000123454")?.model).toBe("66"); + expect(parseNfeKey("NFCom35170458716523000119620010000000121000123450")?.model).toBe("62"); + }); + + test("for the CT-e models the SVC-SP authorises, whose MOC assigns tpEmis 8", () => { + expect(parseNfeKey("35170458716523000119570010000000128000123452")?.emissionType).toBe(8); + expect(parseNfeKey("35170458716523000119670010000000128000123455")?.emissionType).toBe(8); + expect(parseNfeKey("35170458716523000119640010000000128000123454")?.emissionType).toBe(8); + }); + + test("for the MDF-e contingência Regime Especial NFF, tpEmis 3", () => { + expect(parseNfeKey("35170458716523000119580010000000123000123455")?.emissionType).toBe(3); + }); + + test("keeping the cNF of a CT-e that rule B03-10 would forbid, since only the NF-e MOC states it", () => { + expect(parseNfeKey("35170458716523000119570010000000121000000000")?.code).toBe("00000000"); + expect(parseNfeKey("35170458716523000119570010000123451000123455")?.code).toBe("00012345"); + }); + 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); @@ -104,12 +150,53 @@ describe("parseNfeKey", () => { expect(parseNfeKey("35170458716523000119550010000000129000123453")?.emissionType).toBe(9); }); - test("for every other DF-e model (CT-e, MDF-e, NFC-e, CT-e OS), same shape as the SP key with the model field changed and the check digit recalculated", () => { + test("for every other DF-e model (CT-e, MDF-e, GTV-e, NFC-e, CT-e OS), 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("35170458716523000119630010000000121000123453")?.model).toBe("63"); + expect(parseNfeKey("35170458716523000119640010000000121000123457")?.model).toBe("64"); expect(parseNfeKey("35170458716523000119650010000000121000123450")?.model).toBe("65"); expect(parseNfeKey("35170458716523000119670010000000121000123458")?.model).toBe("67"); }); + + test("splitting nSiteAutoriz from the 7 digit cNF of an NFCom, per its Visão Geral §2.1.3", () => { + expect(parseNfeKey("35170458716523000119620010000000121000123450")).toEqual({ + state: "SP", + year: 2017, + month: 4, + taxId: "58716523000119", + model: "62", + series: 1, + number: 12, + emissionType: 1, + authorizationSite: 0, + code: "0012345", + checkDigit: 0, + }); + expect(parseNfeKey("35170458716523000119620010000000121700123452")?.authorizationSite).toBe( + 7, + ); + }); + + test("splitting nSiteAutoriz from the 7 digit cNF of an NF3e, per its Visão Geral", () => { + expect(parseNfeKey("35170458716523000119660010000000121000123454")).toEqual({ + state: "SP", + year: 2017, + month: 4, + taxId: "58716523000119", + model: "66", + series: 1, + number: 12, + emissionType: 1, + authorizationSite: 0, + code: "0012345", + checkDigit: 4, + }); + }); + + test("without an authorizationSite property for a model whose key has no nSiteAutoriz", () => { + expect(parseNfeKey(KEY_SP)).not.toHaveProperty("authorizationSite"); + }); }); describe("properties", () => { @@ -118,20 +205,25 @@ describe("parseNfeKey", () => { fc.stringMatching(/^[0-9]{2}$/), fc.integer({ min: 1, max: 12 }), fc.stringMatching(/^[0-9]{14}$/), - fc.constantFrom("55", "57", "58", "65", "67"), + fc.constantFrom(...MODEL_EMISSION_TYPES), fc.stringMatching(/^[0-9]{3}$/), fc.integer({ min: 1, max: 999_999_999 }), - fc.constantFrom(...VALID_EMISSION_TYPES), fc.stringMatching(/^[0-9]{8}$/), ); test("should give back every field of a well-formed access key", () => { fc.assert( fc.property(parts, (fields) => { - const [uf, year, month, taxId, model, series, number, emissionType, code] = fields; + const [uf, year, month, taxId, document, series, number, tail] = fields; + const { model, emissionType } = document; + const hasSite = AUTHORIZATION_SITE_MODELS.has(model); + const code = hasSite ? tail.slice(1) : tail; + + fc.pre(!FORBIDDEN_CODES.includes(code) && Number(code) !== number); + const issuer = `${uf}${year}${String(month).padStart(2, "0")}${taxId}`; - const document = `${model}${series}${String(number).padStart(9, "0")}`; - const key = buildNfeKey(`${issuer}${document}${emissionType}${code}`); + const numbering = `${model}${series}${String(number).padStart(9, "0")}`; + const key = buildNfeKey(`${issuer}${numbering}${emissionType}${tail}`); const parsed = parseNfeKey(key); expect(parsed?.state).toBe(IBGE_UF_CODES[uf]); @@ -142,6 +234,7 @@ describe("parseNfeKey", () => { expect(parsed?.series).toBe(Number(series)); expect(parsed?.number).toBe(number); expect(parsed?.emissionType).toBe(emissionType); + expect(parsed?.authorizationSite).toBe(hasSite ? Number(tail.charAt(0)) : undefined); expect(parsed?.code).toBe(code); expect(parsed?.checkDigit).toBe(Number(key.charAt(43))); }), @@ -173,10 +266,13 @@ describe("parseNfeKey types", () => { series: number; number: number; emissionType: number; + authorizationSite?: number; code: string; checkDigit: number; }>(); - expectTypeOf().toEqualTypeOf<"55" | "57" | "58" | "65" | "67">(); + expectTypeOf().toEqualTypeOf< + "55" | "57" | "58" | "62" | "63" | "64" | "65" | "66" | "67" + >(); expectTypeOf().toEqualTypeOf<(typeof VALID_MODELS)[number]>(); }); }); diff --git a/src/parse-nfe-key/parse-nfe-key.ts b/src/parse-nfe-key/parse-nfe-key.ts index b7f2ab05..059a17fe 100644 --- a/src/parse-nfe-key/parse-nfe-key.ts +++ b/src/parse-nfe-key/parse-nfe-key.ts @@ -5,20 +5,25 @@ import { mod11 } from "../_internals/mod11/mod11"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { ABSENT_NUMBER, + AUTHORIZATION_SITE_MODELS, + EMISSION_TYPES_BY_MODEL, + FORBIDDEN_CODES, + FORBIDDEN_CODE_MODELS, FORMAT_REGEX, NUMBER_END, NUMBER_START, - VALID_EMISSION_TYPES, VALID_MODELS, + XML_ID_PREFIX_REGEX, } from "./constants"; /** * The document models a DF-e access key can carry: `"55"` NF-e, `"57"` CT-e, `"58"` MDF-e, - * `"65"` NFC-e and `"67"` CT-e OS. Spelled out instead of derived from `VALID_MODELS` because - * the allowlist is internal and API Extractor cannot name it in the public report; the type - * test of `parse-nfe-key.test.ts` pins the two together so they cannot drift apart. + * `"62"` NFCom, `"63"` BP-e, `"64"` GTV-e, `"65"` NFC-e, `"66"` NF3e and `"67"` CT-e OS. + * Spelled out instead of derived from `VALID_MODELS` because the allowlist is internal and API + * Extractor cannot name it in the public report; the type test of `parse-nfe-key.test.ts` pins + * the two together so they cannot drift apart. */ -export type NfeKeyModel = "55" | "57" | "58" | "65" | "67"; +export type NfeKeyModel = "55" | "57" | "58" | "62" | "63" | "64" | "65" | "66" | "67"; /** The fields `parseNfeKey` reads out of a DF-e access key (chave de acesso). */ export type NfeKey = { @@ -30,36 +35,80 @@ export type NfeKey = { 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, "67" CT-e OS. */ + /** Document model: "55" NF-e, "57" CT-e, "58" MDF-e, "62" NFCom, "63" BP-e, "64" GTV-e, "65" NFC-e, "66" NF3e, "67" CT-e OS. */ model: NfeKeyModel; /** Document series, 0 to 999. */ series: number; /** Document number, 1 to 999999999. */ number: number; - /** Emission type code (tpEmis): 1 to 7 or 9, the codes the MOC assigns (8 is not one of them). */ + /** Emission type code (tpEmis), one of the codes the MOC of that model assigns. */ emissionType: number; - /** The 8 digit numeric code (cNF) drawn by the issuer. */ + /** + * Site of the authorizer that received the document (`nSiteAutoriz`), 0 to 9. Only NFCom + * (`"62"`) and NF3e (`"66"`) spend a digit of the key on it. + */ + authorizationSite?: number; + /** The numeric code (cNF) drawn by the issuer: 7 digits for NFCom and NF3e, 8 for the rest. */ code: string; /** The modulo 11 check digit of the key. */ checkDigit: number; }; +const EMISSION_TYPE_INDEX = 34; + +const AUTHORIZATION_SITE_INDEX = 35; + +const SHORT_CODE_START = 36; + +const CODE_END = 43; + +const CHECK_DIGIT_INDEX = 43; + +const isForbiddenCode = (model: string, code: string, number: number): boolean => + FORBIDDEN_CODE_MODELS.includes(model) && + (FORBIDDEN_CODES.includes(code) || Number(code) === 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), MDF-e (modelo 58) and CT-e OS (modelo 67). Accepts the same - * input forms as `isValidNfeKey` (whitespace mask, `NFe` XML `Id` prefix) and returns `null` - * when the key is not valid. The emission type (`tpEmis`) must be one of the codes the MOC - * assigns, 1 to 7 or 9; 8 is not assigned and is rejected. + * Covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e + * (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) and NFCom (62). + * Accepts the same input forms as `isValidNfeKey` (whitespace mask, the `NFe`, `CTe`, `MDFe`, + * `BPe`, `NF3e` and `NFCom` prefixes of the XML `Id` attribute) and returns `null` when the key + * is not valid. + * + * The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so + * the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` + * for the CT-e, `{1, 5, 7, 8}` for the CT-e OS and `{1, 2, 7, 8}` for the GTV-e (8 is the + * authorização pela SVC-SP of the CT-e MOC), `{1, 2, 3}` for the MDF-e and `{1, 2}` for the + * BP-e, the NF3e and the NFCom. + * + * NFCom and NF3e write `nSiteAutoriz` in position 36 and only 7 digits of `cNF` after it, so + * `authorizationSite` is filled for those two models and `code` is 7 characters long instead of + * 8; every other model leaves `authorizationSite` out and reads an 8 digit `code`. + * + * For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, + * which forbids the twenty repeated and sequential codes it lists and a `cNF` equal to the + * document number. That rule arrived with NT 2019.001, so it can turn down a key authorised + * before it, and no other MOC states it, which is why it is not applied to the other models. + * Rejecting a document number of all zeros, on the other hand, is a choice of this library: no + * MOC rule was found forbidding it. * * @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 Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/aj_009_07 + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07 * Ajuste SINIEF 09/07, cláusula primeira, § 3.º, II, "b": the CT-e OS, modelo 67. + * @see Official: https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais + * CT-e MOC 4.00, Anexo I: modelo 64 (GTV-e) and the `tpEmis` domains D19, D27 and D15. + * @see Official: https://dfe-portal.svrs.rs.gov.br/BPE/Documentos + * BP-e MOC 1.00b, Visão Geral and Anexo I: modelo 63. + * @see Official: https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos + * NF3e MOC 1.00a, Visão Geral and Anexo I: modelo 66 and `nSiteAutoriz`. + * @see Official: https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos + * NFCom MOC 1.00a, Visão Geral and Anexo I: modelo 62 and `nSiteAutoriz`. * @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 @@ -75,9 +124,13 @@ export type NfeKey = { * ``` */ export const parseNfeKey = (value: string): NfeKey | null => { - if (typeof value !== "string" || !FORMAT_REGEX.test(value.trim())) return null; + if (typeof value !== "string") return null; + + const body = value.trim().replace(XML_ID_PREFIX_REGEX, ""); - const digits = sanitizeToDigits(value); + if (!FORMAT_REGEX.test(body)) return null; + + const digits = sanitizeToDigits(body); if (digits.length !== NFE_KEY_LENGTH) return null; @@ -98,24 +151,39 @@ export const parseNfeKey = (value: string): NfeKey | null => { if (digits.slice(NUMBER_START, NUMBER_END) === ABSENT_NUMBER) return null; - const emissionType = Number(digits[34]); + const emissionType = Number(digits[EMISSION_TYPE_INDEX]); + + if (!EMISSION_TYPES_BY_MODEL[model].includes(emissionType)) return null; - if (!VALID_EMISSION_TYPES.includes(emissionType)) return null; + const hasAuthorizationSite = AUTHORIZATION_SITE_MODELS.includes(model); + const code = digits.slice( + hasAuthorizationSite ? SHORT_CODE_START : AUTHORIZATION_SITE_INDEX, + CODE_END, + ); + const number = Number(digits.slice(NUMBER_START, NUMBER_END)); - const checkDigit = Number(digits[43]); + if (isForbiddenCode(model, code, number)) return null; - if (mod11(digits.slice(0, 43), { variant: "arrecadacao" }) !== checkDigit) return null; + const checkDigit = Number(digits[CHECK_DIGIT_INDEX]); - return { + if (mod11(digits.slice(0, CHECK_DIGIT_INDEX), { variant: "arrecadacao" }) !== checkDigit) { + return null; + } + + const parsed: NfeKey = { state, year: 2000 + Number(digits.slice(2, 4)), month, taxId: digits.slice(6, 20), model, series: Number(digits.slice(22, 25)), - number: Number(digits.slice(NUMBER_START, NUMBER_END)), + number, emissionType, - code: digits.slice(35, 43), + code, checkDigit, }; + + if (hasAuthorizationSite) parsed.authorizationSite = Number(digits[AUTHORIZATION_SITE_INDEX]); + + return parsed; }; From b688fd0b427ffc32ef0a26ce18d2242d172efcb2 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:44 -0300 Subject: [PATCH 12/14] fix(iban): reject 0 as the holder indicator and apply the printed character set to format and parse --- src/_internals/constants/iban.ts | 6 ++++-- src/format-iban/format-iban.ts | 17 ++++++++++------- src/is-valid-iban/is-valid-iban.test.ts | 6 +++++- src/is-valid-iban/is-valid-iban.ts | 14 ++++++++++---- src/parse-iban/parse-iban.test.ts | 6 +++++- src/parse-iban/parse-iban.ts | 15 ++++++++++----- 6 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/_internals/constants/iban.ts b/src/_internals/constants/iban.ts index 53cb3056..b1b83350 100644 --- a/src/_internals/constants/iban.ts +++ b/src/_internals/constants/iban.ts @@ -5,14 +5,16 @@ * alphanumeric owner indicator = 29 characters. The registry pattern `BR2!n8!n5!n10!n1!a1!c` * allows any letter as the account type, drawn from the "Dicionário de Tipos" of the Catálogo * de Mensagens e de Arquivos do SFN; `C` (conta corrente) and `P` (conta poupança) are the - * usual values. + * usual values. Circular BCB nº 3.625/2013 art. 2º § 1º numbers the owner indicator `1` for + * the first or only holder, `2` for the second and so on up to the ninth, then `A` to `Z` from + * the tenth, so `0` is not a valid owner indicator. * Only Brazilian IBANs follow this layout; every other ISO 13616 country has its own. * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf Circular BCB nº 3.625/2013 * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf 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}[A-Z][A-Z0-9]$/; +export const BR_IBAN_REGEX = /^BR\d{2}\d{8}\d{5}\d{10}[A-Z][A-Z1-9]$/; /** * Shape an IBAN has to be written in: the ISO 13616 print format, letters and digits in diff --git a/src/format-iban/format-iban.ts b/src/format-iban/format-iban.ts index ad6870d8..2a4b6d70 100644 --- a/src/format-iban/format-iban.ts +++ b/src/format-iban/format-iban.ts @@ -3,17 +3,20 @@ import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/s 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. + * Formats an IBAN in the ISO 13616 print grouping, blocks of 4 characters, the 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. + * used as an input mask, and an IBAN of another country is grouped the same way up to that + * length. Use `isValidIban` to check validity. * - * The value still has to be written in the ISO 13616 print format: letters and digits in - * groups separated by a single space, with optional surrounding whitespace. Any other - * character makes the value something other than an IBAN, so it returns an empty string - * instead of quietly dropping the character and presenting the rest as an IBAN. + * The value may be compact (`"BR1500000000000010932840814P2"`), already in the ISO 13616 print + * format (letters and digits in groups separated by a single space) or a partial value still + * being typed, in every case with optional surrounding whitespace. Only a character outside + * letters and digits, or a separator other than a single space, makes the value something + * other than an IBAN, and then the function returns an empty string instead of quietly + * dropping the character and presenting the rest as an IBAN. * * @param {string} value - The IBAN to be formatted. * @returns {string} The IBAN uppercased and grouped in blocks of 4 characters, or an empty diff --git a/src/is-valid-iban/is-valid-iban.test.ts b/src/is-valid-iban/is-valid-iban.test.ts index b3b7da33..e6c2d396 100644 --- a/src/is-valid-iban/is-valid-iban.test.ts +++ b/src/is-valid-iban/is-valid-iban.test.ts @@ -69,6 +69,10 @@ describe("isValidIban", () => { expect(isValidIban("BR150000000000001093284081412")).toBe(false); }); + test("when the owner indicator is 0, which Circular 3.625 art. 2 § 1 does not assign, even though the check digits match", () => { + expect(isValidIban("BR6900000000000010932840814P0")).toBe(false); + }); + test("when the account type letter does not match the check digits", () => { expect(isValidIban("BR1500000000000010932840814X2")).toBe(false); }); @@ -129,7 +133,7 @@ describe("isValidIban", () => { }); describe("properties", () => { - const bodies = fc.stringMatching(/^[0-9]{23}[A-Z][A-Z0-9]$/); + const bodies = fc.stringMatching(/^[0-9]{23}[A-Z][A-Z1-9]$/); test("should accept exactly one pair of check digits for any account", () => { fc.assert( diff --git a/src/is-valid-iban/is-valid-iban.ts b/src/is-valid-iban/is-valid-iban.ts index a3dfe940..dfbbc033 100644 --- a/src/is-valid-iban/is-valid-iban.ts +++ b/src/is-valid-iban/is-valid-iban.ts @@ -24,10 +24,16 @@ const hasValidCheckDigits = (iban: string): boolean => { * 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. * - * The value has to be written in the ISO 13616 print format: letters and digits in groups - * separated by a single space, with optional surrounding whitespace. Any other character - * makes the value something other than an IBAN, so `"BR1500000000000010932840814P-2"` is - * rejected instead of having its hyphen stripped. + * Both accepted forms are the ones an IBAN is written in: compact, + * `"BR1500000000000010932840814P2"`, or the ISO 13616 print format, letters and digits in + * groups separated by a single space, with optional surrounding whitespace either way. Only a + * character outside letters and digits, or a separator other than a single space, makes the + * value something other than an IBAN, so `"BR1500000000000010932840814P-2"` and a double space + * are rejected instead of having the offending character stripped. + * + * The last character is the owner indicator, `1` for the first or only holder up to `9` for the + * ninth and then `A` to `Z` from the tenth, per Circular BCB nº 3.625/2013 art. 2º § 1º, so a + * value ending in `0` is rejected. * * @param {string} value - The IBAN to be validated. * @returns {boolean} True when `value` is a structurally valid Brazilian IBAN whose ISO 7064 diff --git a/src/parse-iban/parse-iban.test.ts b/src/parse-iban/parse-iban.test.ts index 83bea941..9c7e554a 100644 --- a/src/parse-iban/parse-iban.test.ts +++ b/src/parse-iban/parse-iban.test.ts @@ -111,6 +111,10 @@ describe("parseIban", () => { expect(parseIban("BR150000000000001093284081412")).toBeNull(); }); + test("when the owner indicator is 0, which Circular 3.625 art. 2 § 1 does not assign, even though the check digits match", () => { + expect(parseIban("BR6900000000000010932840814P0")).toBeNull(); + }); + test("when the account type letter does not match the check digits", () => { expect(parseIban("BR1500000000000010932840814X2")).toBeNull(); }); @@ -164,7 +168,7 @@ describe("parseIban", () => { }); describe("properties", () => { - const bodies = fc.stringMatching(/^[0-9]{23}[A-Z][A-Z0-9]$/); + const bodies = fc.stringMatching(/^[0-9]{23}[A-Z][A-Z1-9]$/); test("should split an IBAN into fields that spell it back", () => { fc.assert( diff --git a/src/parse-iban/parse-iban.ts b/src/parse-iban/parse-iban.ts index ff596660..a6646406 100644 --- a/src/parse-iban/parse-iban.ts +++ b/src/parse-iban/parse-iban.ts @@ -19,7 +19,10 @@ export type Iban = { * usual values, but any letter is allowed. */ accountType: string; - /** The 1 character alphanumeric owner indicator, distinguishing co-owners of the same account. */ + /** + * The 1 character owner indicator, distinguishing co-owners of the same account: `"1"` for + * the first or only holder up to `"9"` for the ninth, then `"A"` to `"Z"` from the tenth. + */ owner: string; }; @@ -42,13 +45,15 @@ const ACCOUNT_TYPE_END = ACCOUNT_END + ACCOUNT_TYPE_LENGTH; * * 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, any letter, - * usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator). Only + * usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` + * then `A` to `Z`). 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`, including a value carrying any character other - * than letters, digits and the grouping spaces of the ISO 13616 print format. + * Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format + * (groups separated by a single space), in either case with optional surrounding whitespace and + * in any case, and returns `null` whenever `isValidIban` would return `false`, including a value + * carrying any character other than letters, digits and those single grouping spaces. * * @param {string} value - The IBAN to be parsed. * @returns {Iban|null} The parsed IBAN, or `null` when it is not a valid Brazilian IBAN. From 602800c37e8c8ece251bb7761affe34f0cb8ff6c Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:44 -0300 Subject: [PATCH 13/14] chore(data): refresh CFOP from the consolidated annex, CBO from the official CSV and the CST table --- scripts/cbo.ts | 83 ++-- scripts/cfop.ts | 116 +++-- scripts/cnae.ts | 4 +- scripts/ncm.ts | 6 + src/_internals/constants/cbo.ts | 788 ++++++++++++++++++------------- src/_internals/constants/cfop.ts | 498 ++++++++++++------- src/_internals/constants/cnae.ts | 4 +- src/is-valid-csosn/constants.ts | 18 +- src/is-valid-cst/constants.ts | 41 +- src/is-valid-ncm/constants.ts | 6 + 10 files changed, 987 insertions(+), 577 deletions(-) diff --git a/scripts/cbo.ts b/scripts/cbo.ts index a0767571..41d2fdcc 100644 --- a/scripts/cbo.ts +++ b/scripts/cbo.ts @@ -7,38 +7,52 @@ import { fetchSortedRecord } from "../src/_internals/fetch-sorted-record/fetch-s const scriptsDir = import.meta.dirname; -type CboEntry = { - cbo: string; - descricao: string; -}; +/** The official CSV is published in ISO-8859-1, not UTF-8. */ +const CBO_CSV_DECODER = new TextDecoder("iso-8859-1"); -const isCboEntry = (value: unknown): value is CboEntry => - typeof value === "object" && - value !== null && - "cbo" in value && - typeof value.cbo === "string" && - "descricao" in value && - typeof value.descricao === "string"; +const CBO_CSV_HEADER = "CODIGO;TITULO"; -const main = async (): Promise => { - const sorted = await fetchSortedRecord( - "https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json", - "CBO mirror", - async (response) => { - const json: unknown = await response.json(); +const CBO_CODE_REGEX = /^\d{6}$/; - if (!Array.isArray(json) || !json.every((entry) => isCboEntry(entry))) { - throw new Error("CBO mirror payload is not an array of cbo and descricao entries"); - } +/** + * Reads the `CODIGO;TITULO` rows of the official CBO 2002 occupation CSV. + * @param {string} csv - The decoded CSV. + * @returns {Record} One entry per 6 digit occupation code. + */ +const parseCsv = (csv: string): Record => { + const [header, ...rows] = csv.split(/\r?\n/); + + if (header?.trim() !== CBO_CSV_HEADER) { + throw new Error(`CBO CSV header is not "${CBO_CSV_HEADER}"`); + } + + const data: Record = {}; - const data: Record = {}; + for (const row of rows) { + const separator = row.indexOf(";"); - for (const entry of json) { - const code = /^\d{5}$/.test(entry.cbo) ? `0${entry.cbo}` : entry.cbo; + if (separator === -1) continue; - if (!/^\d{6}$/.test(code)) continue; + const code = row.slice(0, separator).trim(); + const title = row.slice(separator + 1).trim(); + + if (!CBO_CODE_REGEX.test(code) || title === "") continue; + + data[code] = title; + } + + return data; +}; + +const main = async (): Promise => { + const sorted = await fetchSortedRecord( + "https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv", + "CBO CSV", + async (response) => { + const data = parseCsv(CBO_CSV_DECODER.decode(await response.arrayBuffer())); - data[code] = entry.descricao; + if (Object.keys(data).length === 0) { + throw new Error("CBO CSV holds no occupation"); } return data; @@ -50,25 +64,26 @@ const main = async (): Promise => { `/** * 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. + * Built from the official \`cbo2002-ocupacao.csv\` the Ministério do Trabalho e Emprego publishes + * on gov.br, a plain \`CODIGO;TITULO\` CSV in ISO-8859-1 whose codes are already the 6 digit + * form \`Cbo.code\` accepts, so no normalization is applied to them. The titles are the official + * sentence case spelling. * * Generated by \`node ./scripts/cbo.ts\`. Do not edit by hand. * + * @see Official: https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv + * The CBO 2002 occupation table, as published by the Ministério do Trabalho e Emprego. * @see Based on: https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json - * @see Official: http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf + * Community mirror of the same table, the fallback this script was generated from before the + * official CSV was used. */ export const CBO_TITLES: Record = ${JSON.stringify(sorted)}; /** * Shape a CBO code has to be written in: the 6 digits, optionally split into the printed - * groups of 4 and 2 by whitespace or the usual mask characters. + * groups of 4 and 2 by a single whitespace or mask character. */ -export const CBO_FORMAT_REGEX = /^\\d{4}[\\s.\\-/]*\\d{2}$/; +export const CBO_FORMAT_REGEX = /^\\d{4}[\\s.\\-/]?\\d{2}$/; `, ); }; diff --git a/scripts/cfop.ts b/scripts/cfop.ts index 3fa95823..bc616d46 100644 --- a/scripts/cfop.ts +++ b/scripts/cfop.ts @@ -7,58 +7,76 @@ import { fetchSortedRecord } from "../src/_internals/fetch-sorted-record/fetch-s const scriptsDir = import.meta.dirname; -const EMBEDDED_ENTRY_REGEX = /\s+(\d)\.(\d{3})\s+-\s+/g; +/** + * CONFAZ prints the text in force with the `A5-1TextoAcordo` class and keeps every superseded + * wording alongside it under `A8-3RedacaoAnt`, with the amendment notes under `Remisso`, + * `A8-1Remissao` and `A8-2RemissaoAnt`. Reading only the first class is what keeps the previous + * redaction of a re-worded code (7.667, re-worded by Ajuste SINIEF 39/25) out of the table. + */ +const CURRENT_TEXT_PARAGRAPH_REGEX = /

([^<]*)<\/p>/g; + +/** A code line, e.g. `1.101 - Compra para industrialização ou produção rural.`. */ +const CODE_LINE_REGEX = /^(\d)\.(\d{3})\s*[-–]\s*(.+)$/; /** - * Some rows of the mirror glue the next code into the description, e.g. - * `1305;"... energia elétrica 1.306 - Aquisição de serviço ..."`, which both corrupts the - * `1305` description and drops `1306`. Splits such a row into one entry per code. - * @param {string} code - The CFOP code the row started with. - * @param {string} description - The row description, possibly containing embedded codes. - * @returns {[string, string][]} One `[code, description]` entry per code found in the row. + * The sentence that opens the body of every operable code. Group and subgroup headings + * (1.000, 1.100, 1.150, ...) are printed in upper case and followed by "Classificam-se neste + * grupo" instead, so they carry no such body and are left out of the table. */ -const splitEmbeddedEntries = (code: string, description: string): [string, string][] => { - const entries: [string, string][] = []; - let currentCode = code; - let lastIndex = 0; - - for (const match of description.matchAll(EMBEDDED_ENTRY_REGEX)) { - entries.push([ - currentCode, - description.slice(lastIndex, match.index).replaceAll(/\s+/g, " ").trim(), - ]); - currentCode = `${match[1]}${match[2]}`; - lastIndex = match.index + match[0].length; - } +const OPERABLE_BODY = "Classificam-se neste código"; - entries.push([currentCode, description.slice(lastIndex).replaceAll(/\s+/g, " ").trim()]); +const TRAILING_PUNCTUATION_REGEX = /[.\s]+$/; - return entries; -}; +/** + * Reads the consolidated Anexo II out of the CONFAZ page. + * + * A handful of rows glue the body into the same paragraph as the code line, e.g. + * `1.255 - Compra de energia elétrica ... Classificam-se neste código as compras ...`, so the + * body sentence is looked for inside the code line first and only then in the paragraph that + * follows it. + * @param {string} html - The annex page. + * @returns {Record} One entry per operable code, keyed by the 4 digits. + */ +const parseAnnex = (html: string): Record => { + const paragraphs = [...html.matchAll(CURRENT_TEXT_PARAGRAPH_REGEX)].map((match) => + (match[1] ?? "").replaceAll(/\s+/g, " ").trim(), + ); -const main = async (): Promise => { - const sorted = await fetchSortedRecord( - "https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv", - "CFOP mirror", - async (response) => { - const csv = await response.text(); + const data: Record = {}; + + for (const [index, paragraph] of paragraphs.entries()) { + const match = CODE_LINE_REGEX.exec(paragraph); - const data: Record = {}; + if (!match) continue; - for (const line of csv.split("\n")) { - const match = /^(\d{4});"(.*)"\s*$/.exec(line); + const [, series, code, line] = match; - if (!match) continue; + if (series === undefined || code === undefined || line === undefined) continue; - const [, code, description] = match; + const bodyIndex = line.indexOf(OPERABLE_BODY); + const isOperable = bodyIndex !== -1 || (paragraphs[index + 1] ?? "").startsWith(OPERABLE_BODY); - if (code === undefined || description === undefined) continue; + if (!isOperable) continue; - for (const [entryCode, entryDescription] of splitEmbeddedEntries(code, description)) { - if (entryCode.endsWith("00") || entryCode.endsWith("50")) continue; + const description = (bodyIndex === -1 ? line : line.slice(0, bodyIndex)) + .trim() + .replace(TRAILING_PUNCTUATION_REGEX, ""); - data[entryCode] = entryDescription; - } + data[`${series}${code}`] = description; + } + + return data; +}; + +const main = async (): Promise => { + const sorted = await fetchSortedRecord( + "https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24", + "CFOP annex", + async (response) => { + const data = parseAnnex(await response.text()); + + if (Object.keys(data).length === 0) { + throw new Error("CFOP annex page holds no operable code"); } return data; @@ -70,17 +88,31 @@ const main = async (): Promise => { `/** * CFOP (Código Fiscal de Operações e Prestações) table, indexed by the 4 digit code. * + * Built from the consolidated Anexo II of Convênio SINIEF s/nº of 15 December 1970, the text in + * force (given its current wording by Ajuste SINIEF 03/24 and last amended by Ajuste SINIEF + * 39/25), not from the frozen 2001 text of Ajuste SINIEF 07/01. + * * Group and subgroup headers (codes ending in "00" or "50", e.g. "1000", "1100", "1150") * are section titles from the official nomenclature rather than operable codes, so they - * are excluded: the Ajuste SINIEF 07/01 prints them in upper case with no "Classificam-se - * neste código" body, unlike the operable codes they head (1151, 1152, ...). + * are excluded: the annex prints them in upper case with no "Classificam-se neste código" + * body, unlike the operable codes they head (1151, 1152, ...). * * Generated by \`node ./scripts/cfop.ts\`. Do not edit by hand. * - * @see Based on: https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24 + * Anexo II of Convênio SINIEF s/nº 1970, the CFOP table in force. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 + * Convênio SINIEF s/nº 1970, the consolidated text the annex belongs to. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 + * Ajuste SINIEF 07/01, the historical text that gave the CFOP its 4 digit form. */ export const CFOP_TABLE: Record = ${JSON.stringify(sorted)}; + +/** + * Shape a CFOP code has to be written in: the 4 digits, optionally split after the series + * digit by a single whitespace or mask character, the way the annex prints them ("1.101"). + */ +export const CFOP_FORMAT_REGEX = /^\\d[\\s.\\-/]?\\d{3}$/; `, ); }; diff --git a/scripts/cnae.ts b/scripts/cnae.ts index 9ea398a7..864ecd4e 100644 --- a/scripts/cnae.ts +++ b/scripts/cnae.ts @@ -58,9 +58,9 @@ export const CNAE_SUBCLASSES: Record = ${JSON.stringify(data)}; /** * Shape a CNAE subclass code has to be written in: the 7 digits, optionally split into the - * printed \`NNNN-N/NN\` groups by whitespace or the usual mask characters. + * printed \`NNNN-N/NN\` groups by a single whitespace or mask character. */ -export const CNAE_FORMAT_REGEX = /^\\d{4}[\\s.\\-/]*\\d[\\s.\\-/]*\\d{2}$/; +export const CNAE_FORMAT_REGEX = /^\\d{4}[\\s.\\-/]?\\d[\\s.\\-/]?\\d{2}$/; `, ); }; diff --git a/scripts/ncm.ts b/scripts/ncm.ts index 45828f35..8f8bca5c 100644 --- a/scripts/ncm.ts +++ b/scripts/ncm.ts @@ -96,6 +96,12 @@ const main = async (): Promise => { * @see Official: https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json */ export const NCM_CODES: readonly string[] = ${JSON.stringify(uniqueSortedCodes)}; + +/** + * Shape a complete NCM code has to be written in: the 8 digits, optionally split into the + * printed \`NNNN.NN.NN\` groups by a single whitespace or mask character. + */ +export const NCM_FORMAT_REGEX = /^\\d{4}[\\s.\\-/]?\\d{2}[\\s.\\-/]?\\d{2}$/; `, ); }; diff --git a/src/_internals/constants/cbo.ts b/src/_internals/constants/cbo.ts index 35f57dd0..91888487 100644 --- a/src/_internals/constants/cbo.ts +++ b/src/_internals/constants/cbo.ts @@ -1,54 +1,55 @@ /** * 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. + * Built from the official `cbo2002-ocupacao.csv` the Ministério do Trabalho e Emprego publishes + * on gov.br, a plain `CODIGO;TITULO` CSV in ISO-8859-1 whose codes are already the 6 digit + * form `Cbo.code` accepts, so no normalization is applied to them. The titles are the official + * sentence case spelling. * * Generated by `node ./scripts/cbo.ts`. Do not edit by hand. * + * @see Official: https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv + * The CBO 2002 occupation table, as published by the Ministério do Trabalho e Emprego. * @see Based on: https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json - * @see Official: http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf + * Community mirror of the same table, the fallback this script was generated from before the + * official CSV was used. */ export const CBO_TITLES: Record = { "111105": "Senador", - "111110": "Deputado Federal", - "111115": "Deputado Estadual e distrital", + "111110": "Deputado federal", + "111115": "Deputado estadual e distrital", "111120": "Vereador", - "111205": "Presidente da República", - "111210": "Vice-Presidente da República", + "111205": "Presidente da república", + "111210": "Vice-presidente da república", "111215": "Ministro de estado", - "111220": "Secretário-Executivo", + "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", + "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", + "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", + "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", + "114205": "Dirigente de entidade de trabalhadores", + "114210": "Dirigente de entidade patronal", "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", @@ -57,12 +58,13 @@ export const CBO_TITLES: Record = { "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", + "122205": + "Diretor de produção e operações da indústria de transformação, extração mineral e utilidades", "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", + "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", @@ -88,8 +90,8 @@ export const CBO_TITLES: Record = { "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)", + "123605": "Diretor de tecnologia da informação", + "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", @@ -98,12 +100,14 @@ export const CBO_TITLES: Record = { "131205": "Diretor de serviços de saúde", "131210": "Gerente de serviços de saúde", "131215": "Tecnólogo em gestão hospitalar", + "131220": "Gerontólogo", + "131225": "Sanitarista", "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", + "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", @@ -127,10 +131,21 @@ export const CBO_TITLES: Record = { "141725": "Gerente de crédito imobiliário", "141730": "Gerente de crédito rural", "141735": "Gerente de recuperação de crédito", + "141805": "Gerente de administração em aeroportos", + "141810": + "Gerente de empresa aérea e empresa de serviços auxiliares ao transporte aéreo (esata) em aeroportos", + "141815": "Gerente de operações em aeroportos", + "141820": "Gerente de operações de cargas", + "141825": "Gerente de segurança da aviação civil", + "141830": "Gerente de segurança operacional (aviação civil)", "142105": "Gerente administrativo", "142110": "Gerente de riscos", "142115": "Gerente financeiro", "142120": "Tecnólogo em gestão administrativo- financeira", + "142125": "Analista de compliance", + "142130": "Analista de riscos", + "142135": "Oficial de proteção de dados pessoais (dpo)", + "142140": "Gerente de facility management", "142205": "Gerente de recursos humanos", "142210": "Gerente de departamento pessoal", "142305": "Gerente comercial", @@ -141,17 +156,20 @@ export const CBO_TITLES: Record = { "142330": "Analista de negócios", "142335": "Analista de pesquisa de mercado", "142340": "Ouvidor", + "142345": "Profissional de relações institucionais e governamentais", + "142350": "Profissonal de relações internacionais", + "142355": "Analista de e-commerce", "142405": "Gerente de compras", "142410": "Gerente de suprimentos", "142415": "Gerente de almoxarifado", - "142505": "Gerente de rede", + "142505": "Gerente de infraestrutura de tecnologia da informação", "142510": "Gerente de desenvolvimento de sistemas", - "142515": "Gerente de produção de tecnologia da informação", + "142515": "Gerente de operaçã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", + "142525": "Gerente de segurança 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)", + "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", @@ -181,7 +199,7 @@ export const CBO_TITLES: Record = { "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", + "203225": "Pesquisador de engenharia metalúrgica, de minas e de materiais", "203230": "Pesquisador de engenharia química", "203305": "Pesquisador de clínica médica", "203310": "Pesquisador de medicina básica", @@ -197,6 +215,7 @@ export const CBO_TITLES: Record = { "203520": "Pesquisador em história", "203525": "Pesquisador em psicologia", "204105": "Perito criminal", + "204110": "Perito judicial", "211105": "Atuário", "211110": "Especialista em pesquisa operacional", "211115": "Matemático", @@ -204,6 +223,7 @@ export const CBO_TITLES: Record = { "211205": "Estatístico", "211210": "Estatístico (estatística aplicada)", "211215": "Estatístico teórico", + "211220": "Cientista de dados", "212205": "Engenheiro de aplicativos em computação", "212210": "Engenheiro de equipamentos em computação", "212215": "Engenheiros de sistemas operacionais em computação", @@ -215,6 +235,8 @@ export const CBO_TITLES: Record = { "212410": "Analista de redes e de comunicação de dados", "212415": "Analista de sistemas de automação", "212420": "Analista de suporte computacional", + "212425": "Arquiteto de soluções de tecnologia da informação", + "212430": "Analista de testes de tecnologia da informação", "213105": "Físico", "213110": "Físico (acústica)", "213115": "Físico (atômica e molecular)", @@ -280,6 +302,8 @@ export const CBO_TITLES: Record = { "214360": "Tecnólogo em eletricidade", "214365": "Tecnólogo em eletrônica", "214370": "Tecnólogo em telecomunicações", + "214375": "Engenheiro de energia", + "214380": "Engenheiro biomédico", "214405": "Engenheiro mecânico", "214410": "Engenheiro mecânico automotivo", "214415": "Engenheiro mecânico (energia nuclear)", @@ -289,11 +313,12 @@ export const CBO_TITLES: Record = { "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", + "214515": "Engenheiro químico (mineração, metalurgia, siderurgia, cimenteira e cerâmica)", "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", + "214540": "Engenheiro têxtil", "214605": "Engenheiro de materiais", "214610": "Engenheiro metalurgista", "214615": "Tecnólogo em metalurgia", @@ -316,19 +341,21 @@ export const CBO_TITLES: Record = { "214925": "Engenheiro de tempos e movimentos", "214930": "Tecnólogo em produção industrial", "214935": "Tecnólogo em segurança do trabalho", + "214940": "Higienista ocupacional", + "214945": "Engenheiro de logistica", "215105": "Agente de manobra e docagem", - "215110": "Capitão de manobra da Marinha Mercante", - "215115": "Comandante da Marinha Mercante", + "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", + "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", + "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", + "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", @@ -339,10 +366,9 @@ export const CBO_TITLES: Record = { "222110": "Engenheiro agrônomo", "222115": "Engenheiro de pesca", "222120": "Engenheiro florestal", + "222125": "Tecnólogo em agronegócio", "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", @@ -390,6 +416,9 @@ export const CBO_TITLES: Record = { "223560": "Enfermeiro sanitarista", "223565": "Enfermeiro da estratégia de saúde da família", "223570": "Perfusionista", + "223575": "Obstetriz", + "223580": "Enfermeiro estomaterapeuta", + "223585": "Enfermeiro forense", "223605": "Fisioterapeuta geral", "223625": "Fisioterapeuta respiratória", "223630": "Fisioterapeuta neurofuncional", @@ -398,10 +427,10 @@ export const CBO_TITLES: Record = { "223645": "Fisioterapeuta quiropraxista", "223650": "Fisioterapeuta acupunturista", "223655": "Fisioterapeuta esportivo", - "223660": "Fisioterapeuta do trabalho", + "223660": "Fisioterapeuta do trabalho", "223705": "Dietista", "223710": "Nutricionista", - "223810": "Fonoaudiólogo", + "223810": "Fonoaudiólogo geral", "223815": "Fonoaudiólogo educacional", "223820": "Fonoaudiólogo em audiologia", "223825": "Fonoaudiólogo em disfagia", @@ -411,6 +440,7 @@ export const CBO_TITLES: Record = { "223845": "Fonoaudiólogo em voz", "223905": "Terapeuta ocupacional", "223910": "Ortoptista", + "223915": "Psicomotricista", "224105": "Avaliador físico", "224110": "Ludomotricista", "224115": "Preparador de atleta", @@ -418,6 +448,7 @@ export const CBO_TITLES: Record = { "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", + "224140": "Profissional de educação física na saúde", "225103": "Médico infectologista", "225105": "Médico acupunturista", "225106": "Médico legista", @@ -443,6 +474,7 @@ export const CBO_TITLES: Record = { "225148": "Médico anatomopatologista", "225150": "Médico em medicina intensiva", "225151": "Médico anestesiologista", + "225154": "Médico antroposófico", "225155": "Médico endocrinologista e metabologista", "225160": "Médico fisiatra", "225165": "Médico gastroenterologista", @@ -479,15 +511,17 @@ export const CBO_TITLES: Record = { "225340": "Médico hemoterapeuta", "225345": "Médico hiperbarista", "225350": "Médico neurofisiologista clínico", + "225355": "Médico radiologista intervencionista", "226105": "Quiropraxista", "226110": "Osteopata", "226305": "Musicoterapeuta", "226310": "Arteterapeuta", "226315": "Equoterapeuta", + "226320": "Naturólogo", "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)", + "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", @@ -520,7 +554,7 @@ export const CBO_TITLES: Record = { "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", + "233215": "Instrutor 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)", @@ -551,7 +585,7 @@ export const CBO_TITLES: Record = { "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", + "234604": "Professor de língua alemã", "234608": "Professor de língua italiana", "234612": "Professor de língua francesa", "234616": "Professor de língua inglesa", @@ -559,7 +593,7 @@ export const CBO_TITLES: Record = { "234624": "Professor de língua portuguesa", "234628": "Professor de literatura brasileira", "234632": "Professor de literatura portuguesa", - "234636": "Professor de literatura alema", + "234636": "Professor de literatura alemã", "234640": "Professor de literatura comparada", "234644": "Professor de literatura espanhola", "234648": "Professor de literatura francesa", @@ -572,9 +606,10 @@ export const CBO_TITLES: Record = { "234676": "Professor de filologia e crítica textual", "234680": "Professor de semiótica", "234684": "Professor de teoria da literatura", + "234689": "Professor de língua brasileira de sinais (libras)", "234705": "Professor de antropologia do ensino superior", "234710": "Professor de arquivologia do ensino superior", - "234715": "Professor de biblioteconomia do ensio superior", + "234715": "Professor de biblioteconomia do ensino 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", @@ -604,6 +639,8 @@ export const CBO_TITLES: Record = { "239425": "Psicopedagogo", "239430": "Supervisor de ensino", "239435": "Designer educacional", + "239440": "Neuropsicopedagogo clinico", + "239445": "Neuropsicopedagogo institucional", "241005": "Advogado", "241010": "Advogado de empresa", "241015": "Advogado (direito civil)", @@ -617,16 +654,17 @@ export const CBO_TITLES: Record = { "241215": "Procurador da fazenda nacional", "241220": "Procurador do estado", "241225": "Procurador do município", - "241230": "Procurador Federal", + "241230": "Procurador federal", "241235": "Procurador fundacional", "241305": "Oficial de registro de contratos marítimos", - "241310": "Oficial do registro civil de pessoas juridicas", + "241310": "Oficial do registro civil de pessoas jurídicas", "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", + "241405": "Conselheiro julgador", "242205": "Procurador da república", "242210": "Procurador de justiça", "242215": "Procurador de justiça militar", @@ -665,15 +703,15 @@ export const CBO_TITLES: Record = { "251540": "Psicólogo do trabalho", "251545": "Neuropsicólogo", "251550": "Psicanalista", - "251555": "Psicólogo Acupunturista", + "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", + "252305": "Secretária(o) executiva(o)", + "252310": "Secretário bilíngüe", "252315": "Secretária trilíngüe", "252320": "Tecnólogo em secretariado escolar", "252405": "Analista de recursos humanos", @@ -685,17 +723,30 @@ export const CBO_TITLES: Record = { "252535": "Analista de leasing", "252540": "Analista de produtos bancários", "252545": "Analista financeiro (instituições financeiras)", + "252550": "Profissional de relações com investidores", "252605": "Gestor em segurança", + "252705": "Analista de pcp (programação e controle da produção)", + "252710": "Analista de planejamento de materias", + "252715": "Analista de logistica", + "252720": "Analista de projetos logisticos", + "252725": "Analista de gestão de estoque", "253110": "Redator de publicidade", - "253115": "Agente publicitário", + "253115": "Publicitário", + "253120": "Diretor de mídia (publicidade)", + "253125": "Diretor de arte (publicidade)", + "253130": "Diretor de criação", + "253135": "Diretor de contas (publicidade)", + "253140": "Agenciador de propaganda", "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", + "253305": "Corretor de valores, ativos financeiros, mercadorias e derivativos", + "253405": "Analista de mídias sociais", + "253410": "Influenciador digital", + "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", @@ -703,6 +754,7 @@ export const CBO_TITLES: Record = { "254410": "Fiscal de tributos municipal", "254415": "Técnico de tributos estadual", "254420": "Técnico de tributos municipal", + "254505": "Fiscal de atividades urbanas", "261105": "Arquivista pesquisador (jornalismo)", "261110": "Assessor de imprensa", "261115": "Diretor de redação", @@ -721,6 +773,7 @@ export const CBO_TITLES: Record = { "261415": "Lingüista", "261420": "Tradutor", "261425": "Intérprete de língua de sinais", + "261430": "Audiodescritor", "261505": "Autor-roteirista", "261510": "Crítico", "261515": "Escritor de ficção", @@ -732,17 +785,17 @@ export const CBO_TITLES: Record = { "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", + "261705": "Âncora de mídias audiovisuais", + "261710": "Comentarista de mídias audiovisuais", + "261715": "Locutor de mídias audiovisuais", + "261730": "Repórter de mídias audiovisuais", "261805": "Fotógrafo", "261810": "Fotógrafo publicitário", "261815": "Fotógrafo retratista", - "261820": "Repóter fotográfico", - "262105": "Empresário de espetáculo", + "261820": "Repórter fotográfico", + "261905": "Continuista", + "261910": "Assistente de direção (tv)", + "262105": "Produtor cultural", "262110": "Produtor cinematográfico", "262115": "Produtor de rádio", "262120": "Produtor de teatro", @@ -753,15 +806,18 @@ export const CBO_TITLES: Record = { "262210": "Diretor de programas de rádio", "262215": "Diretor de programas de televisão", "262220": "Diretor teatral", + "262225": "Diretor de programação", + "262230": "Diretor de produção", + "262235": "Diretor artistíco", "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", + "262325": "Cenógrafo de tv", "262330": "Diretor de arte", "262405": "Artista (artes visuais)", - "262410": "Desenhista industrial (designer)", - "262415": "Conservador-restaurador de bens culturais", + "262410": "Desenhista industrial gráfico (designer gráfico)", + "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", @@ -794,12 +850,13 @@ export const CBO_TITLES: Record = { "311110": "Técnico de celulose e papel", "311115": "Técnico em curtimento", "311205": "Técnico em petroquímica", - "311305": "Técnico em materiais", + "311305": "Técnico em materiais, produtos cerâmicos e vidros", "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", + "311515": + "Técnico de utilidade (produção e distribuição de vapor, gases, óleos, combustíveis, energia)", "311520": "Técnico em tratamento de efluentes", "311605": "Técnico têxtil", "311610": "Técnico têxtil (tratamentos químicos)", @@ -820,7 +877,7 @@ export const CBO_TITLES: Record = { "312320": "Topógrafo", "313105": "Eletrotécnico", "313110": "Eletrotécnico (produção de energia)", - "313115": "Eletroténico na fabricação", + "313115": "Eletrotécnico na fabricação, montagem e instalação de máquinas e equipamentos", "313120": "Técnico de manutenção elétrica", "313125": "Técnico de manutenção elétrica de máquina", "313130": "Técnico eletricista", @@ -834,11 +891,11 @@ export const CBO_TITLES: Record = { "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", + "313415": "Encarregado de manutenção de instrumentos de controle, medição e similares", "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", + "314115": "Técnico mecânico (calefação, ventilação e refrigeração)", "314120": "Técnico mecânico (máquinas)", "314125": "Técnico mecânico (motores)", "314205": "Técnico mecânico na fabricação de ferramentas", @@ -848,16 +905,24 @@ export const CBO_TITLES: Record = { "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", + "314625": "Tecnólogo 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", + "314805": "Inspetor de equipamentos", + "314810": "Inspetor de fabricação", + "314815": "Inspetor de ensaios não destrutivos", + "314825": "Inspetor de dutos", + "314830": "Inspetor de controle dimensional", + "314835": "Inspetor de pintura", + "314840": "Inspetor de manutenção", + "314845": "Inspetor de soldagem", "316105": "Técnico em geofísica", "316110": "Técnico em geologia", "316115": "Técnico em geoquímica", @@ -870,12 +935,12 @@ export const CBO_TITLES: Record = { "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", + "317105": "Desenvolvedor web (técnico)", + "317110": "Desenvolvedor de sistemas de tecnologia da informação (técnico)", "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)", + "317120": "Desenvolvedor de multimídia", + "317205": "Operador de computador", + "317210": "Técnico de suporte ao usuário de tecnologia da informação", "318005": "Desenhista técnico", "318010": "Desenhista copista", "318015": "Desenhista detalhista", @@ -887,13 +952,13 @@ export const CBO_TITLES: Record = { "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", + "318310": "Desenhista técnico (calefação, ventilação e refrigeraçã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", + "318430": "Desenhista técnico de embalagens, maquetes e leiautes", "318505": "Desenhista projetista de arquitetura", "318510": "Desenhista projetista de construção civil", "318605": "Desenhista projetista de máquinas", @@ -923,6 +988,7 @@ export const CBO_TITLES: Record = { "322125": "Terapeuta holístico", "322130": "Esteticista", "322135": "Doula", + "322140": "Instrutor de pilates", "322205": "Técnico de enfermagem", "322210": "Técnico de enfermagem de terapia intensiva", "322215": "Técnico de enfermagem do trabalho", @@ -933,6 +999,7 @@ export const CBO_TITLES: Record = { "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", + "322255": "Técnico em agente comunitário de saúde", "322305": "Técnico em óptica e optometria", "322405": "Técnico em saúde bucal", "322410": "Protético dentário", @@ -946,10 +1013,14 @@ export const CBO_TITLES: Record = { "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", + "324120": "Tecnólogo em radiologia", "324125": "Tecnólogo oftálmico", + "324130": "Técnico em espirometria", + "324135": "Técnico em polissonografia", + "324140": "Dosimetrista clínico", "324205": "Técnico em patologia clínica", - "324210": "Auxiliar técnico em patologia clínica", + "324215": "Citotécnico", + "324220": "Técnico em hemoterapia", "325005": "Enólogo", "325010": "Aromista", "325015": "Perfumista", @@ -984,9 +1055,15 @@ export const CBO_TITLES: Record = { "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", + "341235": "Técnico em sinalização náutica", + "341240": "Técnicos em manobras em equipamentos de convés", + "341245": "Técnico em sinais navais", + "341250": "Auxiliar técnico de sinalização nautica", + "341305": "Condutor maquinista motorista fluvial", + "341310": "Condutor de máquinas", "341315": "Eletricista de bordo", + "341320": "Condutor de máquinas (bombeador)", + "341325": "Condutor de máquinas (mecânico)", "342105": "Analista de transporte em comércio exterior", "342110": "Operador de transporte multimodal", "342115": "Controlador de serviços de máquinas e veículos", @@ -994,6 +1071,7 @@ export const CBO_TITLES: Record = { "342125": "Tecnólogo em logística de transporte", "342205": "Ajudante de despachante aduaneiro", "342210": "Despachante aduaneiro", + "342215": "Analista de desembaraço 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", @@ -1002,13 +1080,13 @@ export const CBO_TITLES: Record = { "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", + "342555": "Fiscal de pista de aeroporto", + "342560": "Operador de rampa ( transporte aéreo)", "342605": "Chefe de estação portuária", "342610": "Supervisor de operações portuárias", "351105": "Técnico de contabilidade", @@ -1023,10 +1101,13 @@ export const CBO_TITLES: Record = { "351420": "Escrivão de polícia", "351425": "Oficial de justiça", "351430": "Auxiliar de serviços jurídicos", + "351435": "Mediador de conflitos", + "351440": "Árbitro extrajudicial", "351505": "Técnico em secretariado", "351510": "Taquígrafo", "351515": "Estenotipista", - "351605": "Técnico em segurança no trabalho", + "351605": "Técnico em segurança do trabalho", + "351610": "Técnico em higiene ocupacional", "351705": "Analista de seguros (técnico)", "351710": "Analista de sinistros", "351715": "Assistente comercial de seguros", @@ -1047,7 +1128,7 @@ export const CBO_TITLES: Record = { "352315": "Agente fiscal metrológico", "352320": "Agente fiscal têxtil", "352405": "Agente de direitos autorais", - "352410": "Avaliador de produtos do meio de comunicação", + "352410": "Avaliador de produtos dos meios 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", @@ -1056,7 +1137,6 @@ export const CBO_TITLES: Record = { "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", @@ -1072,11 +1152,13 @@ export const CBO_TITLES: Record = { "354415": "Avaliador de bens móveis", "354505": "Corretor de seguros", "354605": "Corretor de imóveis", + "354610": "Corretor de grãos", "354705": "Representante comercial autônomo", "354805": "Técnico em turismo", "354810": "Operador de turismo", "354815": "Agente de viagem", "354820": "Organizador de evento", + "354825": "Cerimonialista", "371105": "Auxiliar de biblioteca", "371110": "Técnico em biblioteconomia", "371205": "Colecionador de selos e moedas", @@ -1090,15 +1172,14 @@ export const CBO_TITLES: Record = { "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", + "373105": "Operador de mídia audiovisual", + "373130": "Técnico de sistemas audiovisuais", + "373135": "Operador de controle mestre", + "373140": "Coordenador de programação", + "373145": "Assistente de operações audiovisuais", "373220": "Supervisor técnico operacional de sistemas de televisão e produtoras de vídeo", + "373225": "Supervisor de operações (mídias audiovisuais)", + "373230": "Supervisor técnico (mídias audiovisuais)", "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", @@ -1108,19 +1189,24 @@ export const CBO_TITLES: Record = { "374135": "Projetista de sistemas de áudio", "374140": "Microfonista", "374145": "Dj (disc jockey)", - "374205": "Cenotécnico (cinema", + "374150": "Sonoplasta", + "374155": "Analista musical", + "374205": "Cenotécnico (cinema, vídeo, televisão, teatro e espetáculos)", "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", + "374405": "Editor de mídia audiovisual", "374410": "Finalizador de filmes", "374415": "Finalizador de vídeo", "374420": "Montador de filmes", + "374425": "Diretor de imagens (tv)", "375105": "Designer de interiores", "375110": "Designer de vitrines", "375115": "Visual merchandiser", "375120": "Decorador de eventos", + "375125": "Produtor de moda", + "375130": "Profissional de organização (personal organizer)", "376105": "Dançarino tradicional", "376110": "Dançarino popular", "376205": "Acrobata", @@ -1139,6 +1225,7 @@ export const CBO_TITLES: Record = { "376315": "Apresentador de programas de rádio", "376320": "Apresentador de programas de televisão", "376325": "Apresentador de circo", + "376330": "Mestre de cerimonias", "376405": "Modelo artístico", "376410": "Modelo de modas", "376415": "Modelo publicitário", @@ -1151,15 +1238,15 @@ export const CBO_TITLES: Record = { "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", + "377205": "Árbitro desportivo", + "377210": "Árbitro de atletismo", + "377215": "Árbitro de basquete", + "377220": "Árbitro de futebol", + "377225": "Árbitro de futebol de salão", + "377230": "Árbitro de judô", + "377235": "Árbitro de karatê", + "377240": "Árbitro de poló aquático", + "377245": "Árbitro de vôlei", "391105": "Cronoanalista", "391110": "Cronometrista", "391115": "Controlador de entrada e saída", @@ -1167,6 +1254,8 @@ export const CBO_TITLES: Record = { "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", + "391140": "Analista de manutenção (equipamentos aéreos)", + "391145": "Analista de planejamento de manutenção", "391205": "Inspetor de qualidade", "391210": "Técnico de garantia da qualidade", "391215": "Operador de inspeção de qualidade", @@ -1183,6 +1272,7 @@ export const CBO_TITLES: Record = { "410225": "Supervisor de crédito e cobrança", "410230": "Supervisor de orçamento", "410235": "Supervisor de tesouraria", + "410240": "Supervisor de logística", "411005": "Auxiliar de escritório", "411010": "Assistente administrativo", "411015": "Atendente de judiciário", @@ -1193,6 +1283,7 @@ export const CBO_TITLES: Record = { "411040": "Auxiliar de seguros", "411045": "Auxiliar de serviços de importação e exportação", "411050": "Agente de microcrédito", + "411055": "Captador de recursos", "412105": "Datilógrafo", "412110": "Digitador", "412115": "Operador de mensagens de telecomunicações (correios)", @@ -1210,6 +1301,10 @@ export const CBO_TITLES: Record = { "414105": "Almoxarife", "414110": "Armazenista", "414115": "Balanceiro", + "414120": "Conferente mercadoria (exceto carga e descarga)", + "414125": "Estoquista", + "414135": "Expedidor de mercadorias", + "414140": "Auxiliar de logistica", "414205": "Apontador de mão-de-obra", "414210": "Apontador de produção", "414215": "Conferente de carga e descarga", @@ -1220,6 +1315,9 @@ export const CBO_TITLES: Record = { "415130": "Operador de máquina copiadora (exceto operador de gráfica rápida)", "415205": "Carteiro", "415210": "Operador de triagem e transbordo", + "415215": "Entregador de publicações", + "415305": "Registrador de câncer", + "415310": "Analista de informação em saúde", "420105": "Supervisor de caixas e bilheteiros (exceto caixa de banco)", "420110": "Supervisor de cobrança", "420115": "Supervisor de coletadores de apostas e de jogos", @@ -1237,26 +1335,32 @@ export const CBO_TITLES: Record = { "421305": "Cobrador externo", "421310": "Cobrador interno", "421315": "Localizador (cobrador)", - "422105": "Recepcionista", + "422105": "Recepcionista, em geral", "422110": "Recepcionista de consultório médico ou dentário", "422115": "Recepcionista de seguro saúde", "422120": "Recepcionista de hotel", "422125": "Recepcionista de banco", + "422130": "Concierge", "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", + "422330": "Teleatendente de emergência", + "422335": "Monitor de teleatendimento", "423105": "Despachante documentalista", "423110": "Despachante de trânsito", + "423115": "Vistoriador veicular", "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", + "424125": "Escriturário em estatística", + "424130": "Entrevistador social", + "424205": "Coordenador de provas (concurso, avaliação, exame)", + "424210": "Aplicador de provas (concurso, avaliação,exame)", "510105": "Supervisor de transportes", "510110": "Administrador de edifícios", "510115": "Supervisor de andar", @@ -1272,11 +1376,14 @@ export const CBO_TITLES: Record = { "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ô", + "511220": "Bilheteiro (estações de metrô, ferroviárias e assemelhadas)", "511405": "Guia de turismo", - "512105": "Empregado doméstico nos serviços gerais", - "512110": "Empregado doméstico arrumador", - "512115": "Empregado doméstico faxineiro", + "511505": "Condutor de turismo de aventura", + "511510": "Condutor de turismo de pesca", + "511515": "Condutor turismo náutico", + "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", @@ -1286,13 +1393,13 @@ export const CBO_TITLES: Record = { "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", + "513305": "Camareiro de teatro", + "513310": "Camareiro de televisão", + "513315": "Camareiro de hotel", "513320": "Camareiro de embarcações", - "513325": "Guarda-roupeira de cinema", + "513325": "Guarda-roupeiro de cinema", "513405": "Garçom", - "513410": "Garçom (serviços de vinhos)", + "513410": "Sommelier", "513415": "Cumim", "513420": "Barman", "513425": "Copeiro", @@ -1324,6 +1431,7 @@ export const CBO_TITLES: Record = { "515125": "Agente indígena de saúde", "515130": "Agente indígena de saneamento", "515135": "Socorrista (exceto médicos e enfermeiros)", + "515140": "Agente de combate às endemias", "515205": "Auxiliar de banco de sangue", "515210": "Auxiliar de farmácia de manipulação", "515215": "Auxiliar de laboratório de análises clínicas", @@ -1334,28 +1442,31 @@ export const CBO_TITLES: Record = { "515315": "Monitor de dependente químico", "515320": "Conselheiro tutelar", "515325": "Sócioeducador", + "515330": "Monitor de ressocialização prisional", "516105": "Barbeiro", "516110": "Cabeleireiro", - "516120": "Manicure", + "516120": "Manicure/pedicure", "516125": "Maquiador", "516130": "Maquiador de caracterização", - "516140": "Pedicure", + "516145": "Designer de unhas", + "516150": "Designer de sobrancelhas", + "516155": "Designer de cílios", "516205": "Babá", "516210": "Cuidador de idosos", - "516215": "Mae social", + "516215": "Mãe social", "516220": "Cuidador em saúde", - "516305": "Lavadeiro", - "516310": "Lavador de roupas a maquina", + "516305": "Lavadeiro, em geral", + "516310": "Lavador de roupas a maquina", "516315": "Lavador de artefatos de tapeçaria", - "516320": "Limpador a seco", + "516320": "Limpador a seco, à máquina", "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", + "516410": "Limpador de roupas a seco, à mão", + "516415": "Passador de roupas, à mão", "516505": "Agente funerário", "516605": "Operador de forno (serviços funerários)", "516610": "Sepultador", @@ -1363,26 +1474,35 @@ export const CBO_TITLES: Record = { "516710": "Numerólogo", "516805": "Esotérico", "516810": "Paranormal", + "516905": "Tatuador", + "516910": "Micropigmentador", + "516915": "Body piercer", "517105": "Bombeiro de aeródromo", - "517110": "Bombeiro de segurança do trabalho", + "517110": "Bombeiro civil", "517115": "Salva-vidas", + "517120": "Brigadista florestal", + "517125": "Chefe de brigada", + "517130": "Agente de proteção e defesa civil", "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", + "517225": "Policial legislativo", + "517230": "Policial penal", + "517235": "Guarda portuário", + "517305": "Vigilante 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", + "517415": "Agente de portaria", "517420": "Vigia", + "517425": "Fiscal de loja", "519105": "Ciclista mensageiro", - "519110": "Motociclista no transporte de documentos e pequenos volumes", + "519110": "Motofretista", + "519115": "Mototaxista", "519205": "Catador de material reciclável", "519210": "Selecionador de material reciclável", "519215": "Operador de prensa de material reciclável", @@ -1396,7 +1516,7 @@ export const CBO_TITLES: Record = { "519915": "Engraxate", "519920": "Gandula", "519925": "Guardador de veículos", - "519930": "Lavador de garrafas", + "519930": "Lavador de garrafas, vidros e outros utensílios", "519935": "Lavador de veículos", "519940": "Leiturista", "519945": "Recepcionista de casas de espetáculos", @@ -1409,16 +1529,20 @@ export const CBO_TITLES: Record = { "521125": "Repositor de mercadorias", "521130": "Atendente de farmácia - balconista", "521135": "Frentista", - "523105": "Instalador de cortinas e persianas", + "521140": "Atendente de lojas e mercados", + "523105": "Instalador de cortinas e persianas, portas sanfonadas e boxe", "523110": "Instalador de som e acessórios de veículos", "523115": "Chaveiro", + "523120": "Aplicador de vinil autoadesivo", + "523125": "Estampador de placa de identificação veicular (epiv)", "524105": "Vendedor em domicílio", "524205": "Feirante", "524210": "Jornaleiro (em banca de jornal)", - "524215": "Vendedor permissionário", + "524215": "Vendedor permissionário", "524305": "Vendedor ambulante", "524310": "Pipoqueiro ambulante", - "611005": "Produtor agropecuário", + "524315": "Baiana de acarajé", + "611005": "Produtor agropecuário, em geral", "612005": "Produtor agrícola polivalente", "612105": "Produtor de arroz", "612110": "Produtor de cana-de-açúcar", @@ -1431,8 +1555,8 @@ export const CBO_TITLES: Record = { "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", + "612310": "Produtor na olericultura de raízes, bulbos e tubérculos", + "612315": "Produtor na olericultura de talos, folhas e flores", "612320": "Produtor na olericultura de frutos e sementes", "612405": "Produtor de flores de corte", "612410": "Produtor de flores em vaso", @@ -1484,14 +1608,14 @@ export const CBO_TITLES: Record = { "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", + "622120": "Trabalhador da cultura de trigo, aveia, cevada e triticale", "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", + "622315": "Trabalhador na olericultura (raízes, bulbos e tubérculos)", + "622320": "Trabalhador na olericultura (talos, folhas e flores)", "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", @@ -1519,6 +1643,8 @@ export const CBO_TITLES: Record = { "623010": "Inseminador", "623015": "Trabalhador de pecuária polivalente", "623020": "Tratador de animais", + "623025": "Casqueador de animais", + "623030": "Ferrador de animais", "623105": "Trabalhador da pecuária (asininos e muares)", "623110": "Trabalhador da pecuária (bovinos corte)", "623115": "Trabalhador da pecuária (bovinos leite)", @@ -1541,17 +1667,17 @@ export const CBO_TITLES: Record = { "631005": "Catador de caranguejos e siris", "631010": "Catador de mariscos", "631015": "Pescador artesanal de lagostas", - "631020": "Pescador artesanal de peixes e camaroes", + "631020": "Pescador artesanal de peixes e camarões", "631105": "Pescador artesanal de água doce", "631205": "Pescador industrial", "631210": "Pescador profissional", - "631305": "Criador de camaroes", + "631305": "Criador de camarões", "631310": "Criador de jacarés", - "631315": "Criador de mexilhoes", + "631315": "Criador de mexilhões", "631320": "Criador de ostras", "631325": "Criador de peixes", "631330": "Criador de quelônios", - "631335": "Criador de ras", + "631335": "Criador de rãs", "631405": "Gelador industrial", "631410": "Gelador profissional", "631415": "Proeiro", @@ -1563,7 +1689,7 @@ export const CBO_TITLES: Record = { "632110": "Cubador de madeira", "632115": "Identificador florestal", "632120": "Operador de motosserra", - "632125": "Trabalhador de extração florestal", + "632125": "Trabalhador de extração florestal, em geral", "632205": "Seringueiro", "632210": "Trabalhador da exploração de espécies produtoras de gomas não elásticas", "632215": "Trabalhador da exploração de resinas", @@ -1574,7 +1700,7 @@ export const CBO_TITLES: Record = { "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)", + "632340": "Trabalhador da exploração de malva (pãina)", "632345": "Trabalhador da exploração de murumuru", "632350": "Trabalhador da exploração de oiticica", "632355": "Trabalhador da exploração de ouricuri", @@ -1585,11 +1711,14 @@ export const CBO_TITLES: Record = { "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", + "632505": + "Trabalhador da exploração de árvores e arbustos produtores de substâncias aromát., Medic. E tóxicas", + "632510": + "Trabalhador da exploração de cipós produtores de substâncias aromáticas, medicinais e tóxicas", "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", + "632520": + "Trabalhador da exploração de raízes produtoras de substâncias aromáticas, medicinais e tóxicas", + "632525": "Trabalhador da extração de substâncias aromáticas, medicinais e tóxicas, em geral", "632605": "Carvoeiro", "632610": "Carbonizador", "632615": "Ajudante de carvoaria", @@ -1658,7 +1787,7 @@ export const CBO_TITLES: Record = { "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", + "715140": "Operador de pavimentadora (asfalto, concreto e materiais similares)", "715145": "Operador de trator de lâmina", "715205": "Calceteiro", "715210": "Pedreiro", @@ -1672,6 +1801,7 @@ export const CBO_TITLES: Record = { "715405": "Operador de betoneira", "715410": "Operador de bomba de concreto", "715415": "Operador de central de concreto", + "715420": "Operador de usina de asfalto", "715505": "Carpinteiro", "715510": "Carpinteiro (esquadrias)", "715515": "Carpinteiro (cenários)", @@ -1679,7 +1809,7 @@ export const CBO_TITLES: Record = { "715525": "Carpinteiro de obras", "715530": "Carpinteiro (telhados)", "715535": "Carpinteiro de fôrmas para concreto", - "715540": "Carpinteiro de obras civis de arte (pontes", + "715540": "Carpinteiro de obras civis de arte (pontes, túneis, barragens)", "715545": "Montador de andaimes (edificações)", "715605": "Eletricista de instalações (cenários)", "715610": "Eletricista de instalações (edifícios)", @@ -1688,28 +1818,29 @@ export const CBO_TITLES: Record = { "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", + "715725": "Instalador de material isolante, a mão (edificações)", + "715730": "Instalador de material isolante, à máquina (edificações)", "716105": "Acabador de superfícies de concreto", "716110": "Revestidor de superfícies de concreto", - "716205": "Telhador (telhas de argila e materias similares)", + "716205": "Telhador (telhas de argila e materiais similares)", "716210": "Telhador (telhas de cimento-amianto)", "716215": "Telhador (telhas metálicas)", - "716220": "Telhador (telhas pláticas)", + "716220": "Telhador (telhas plásticas)", "716305": "Vidraceiro", "716310": "Vidraceiro (edificações)", "716315": "Vidraceiro (vitrais)", "716405": "Gesseiro", "716505": "Assoalhador", - "716510": "Ladrilheiro", + "716510": "Assentador de revestimentos cerâmicos", "716515": "Pastilheiro", "716520": "Lustrador de piso", "716525": "Marmorista (construção)", "716530": "Mosaísta", "716535": "Taqueiro", + "716540": "Rejuntador de revestimentos", "716605": "Calafetador", "716610": "Pintor de obras", - "716615": "Revestidor de interiores (papel", + "716615": "Revestidor de interiores (papel, material plástico e emborrachados)", "717005": "Demolidor de edificações", "717010": "Operador de martelete", "717015": "Poceiro (edificações)", @@ -1732,7 +1863,7 @@ export const CBO_TITLES: Record = { "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", + "721110": "Ferramenteiro de mandris, calibradores e outros dispositivos", "721115": "Modelador de metais (fundição)", "721205": "Operador de máquina de eletroerosão", "721210": "Operador de máquinas operatrizes", @@ -1747,28 +1878,27 @@ export const CBO_TITLES: Record = { "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", + "721420": "Operador de máquina eletroerosão, à fio, com comando numérico", "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", + "722210": "Operador de lingotamento", "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", + "722220": "Operador de máquina de fundição", + "722230": "Operador de vazamento", + "722235": "Preparador de panelas", + "722305": "Macheiro, a mão", + "722310": "Macheiro, a máquina", + "722315": "Moldador, a mão", + "722320": "Moldador, a máquina", "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", + "722415": "Trefilador de metais, à máquina", "723105": "Cementador de metais", "723110": "Normalizador de metais e de compósitos", "723115": "Operador de equipamento para resfriamento", @@ -1787,7 +1917,7 @@ export const CBO_TITLES: Record = { "723315": "Pintor de estruturas metálicas", "723320": "Pintor de veículos (fabricação)", "723325": "Pintor por imersão", - "723330": "Pintor", + "723330": "Pintor, a pistola (exceto obras e estruturas metálicas)", "724105": "Assentador de canalização (edificações)", "724110": "Encanador", "724115": "Instalador de tubulações", @@ -1797,14 +1927,14 @@ export const CBO_TITLES: Record = { "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", + "724215": "Rebitador a martelo pneumático", "724220": "Preparador de estruturas metálicas", "724225": "Riscador de estruturas metálicas", - "724230": "Rebitador", + "724230": "Rebitador, a mão", "724305": "Brasador", - "724310": "Oxicortador a mão e a máquina", + "724310": "Oxicortador a mão e a máquina", "724315": "Soldador", - "724320": "Soldador a oxigás", + "724320": "Soldador a oxigás", "724325": "Soldador elétrico", "724405": "Caldeireiro (chapas de cobre)", "724410": "Caldeireiro (chapas de ferro e aço)", @@ -1824,7 +1954,8 @@ export const CBO_TITLES: Record = { "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", + "725030": "Operador de manutenção e recarga de extintor de incêndio", + "725105": "Montador de máquinas, motores e acessórios (montagem em série)", "725205": "Montador de máquinas", "725210": "Montador de máquinas gráficas", "725215": "Montador de máquinas operatrizes para madeira", @@ -1856,24 +1987,25 @@ export const CBO_TITLES: Record = { "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", + "731165": "Bobinador eletricista, à mão", + "731170": "Bobinador eletricista, à máquina", "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", + "731205": "Montador de equipamentos eletrônicos (estação de rádio, tv e equipamentos de radar)", "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", + "732105": "Eletricista de manutenção de linhas elétricas, telefônicas e de comunicação de dados", "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)", + "732115": "Examinador de cabos, linhas elétricas e telefônicas", + "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", + "732140": "Instalador de sistemas fotovoltaicos", "740105": "Supervisor da mecânica de precisão", "740110": "Supervisor de fabricação de instrumentos musicais", "741105": "Ajustador de instrumentos de precisão", @@ -1884,10 +2016,10 @@ export const CBO_TITLES: Record = { "742105": "Afinador de instrumentos musicais", "742110": "Confeccionador de acordeão", "742115": "Confeccionador de instrumentos de corda", - "742120": "Confeccionador de instrumentos de percussão (pele", + "742120": "Confeccionador de instrumentos de percussão (pele, couro ou plástico)", "742125": "Confeccionador de instrumentos de sopro (madeira)", "742130": "Confeccionador de instrumentos de sopro (metal)", - "742135": "Confeccionador de orgão", + "742135": "Confeccionador de órgão", "742140": "Confeccionador de piano", "750105": "Supervisor de joalheria", "750205": @@ -1896,10 +2028,10 @@ export const CBO_TITLES: Record = { "751010": "Joalheiro", "751015": "Joalheiro (reparações)", "751020": "Lapidador (jóias)", - "751105": "Bate-folha a máquina", + "751105": "Bate-folha a máquina", "751110": "Fundidor (joalheria e ourivesaria)", "751115": "Gravador (joalheria e ourivesaria)", - "751120": "Laminador de metais preciosos a mão", + "751120": "Laminador de metais preciosos a mão", "751125": "Ourives", "751130": "Trefilador (joalheria e ourivesaria)", "752105": "Artesão modelador (vidros)", @@ -1908,9 +2040,9 @@ export const CBO_TITLES: Record = { "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", + "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", @@ -1924,7 +2056,7 @@ export const CBO_TITLES: Record = { "752415": "Decorador de vidro à pincel", "752420": "Operador de esmaltadeira", "752425": "Operador de espelhamento", - "752430": "Pintor de cerâmica", + "752430": "Pintor de cerâmica, a pincel", "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)", @@ -1933,12 +2065,12 @@ export const CBO_TITLES: Record = { "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)", + "760405": "Supervisor (indústria de calçados e artefatos de couro)", + "760505": "Supervisor da confecção de artefatos de tecidos, couros e afins", + "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", + "761110": "Lavador de lã", "761205": "Operador de abertura (fiação)", "761210": "Operador de binadeira", "761215": "Operador de bobinadeira", @@ -1958,21 +2090,21 @@ export const CBO_TITLES: Record = { "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", + "761324": "Tecelão (tear mecânico, exceto jacquard)", + "761327": "Tecelão de malhas, a máquina", "761330": "Tecelão de malhas (máquina circular)", "761333": "Tecelão de malhas (máquina retilínea)", - "761336": "Tecelão de meias", + "761336": "Tecelão de meias, a máquina", "761339": "Tecelão de meias (máquina circular)", "761342": "Tecelão de meias (máquina retilínea)", - "761345": "Tecelão de tapetes", + "761345": "Tecelão de tapetes, a máquina", "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", + "761360": "Passamaneiro a máquina", "761363": "Remetedor de fios", - "761366": "Picotador de cartoes jacquard", + "761366": "Picotador de cartões jacquard", "761405": "Alvejador (tecidos)", "761410": "Estampador de tecido", "761415": "Operador de calandras (tecidos)", @@ -1986,7 +2118,7 @@ export const CBO_TITLES: Record = { "761820": "Revisor de tecidos crus", "762005": "Trabalhador polivalente do curtimento de couros e peles", "762105": "Classificador de peles", - "762110": "Descarnador de couros e peles", + "762110": "Descarnador de couros e peles, à maquina", "762115": "Estirador de couros e peles (preparação)", "762120": "Fuloneiro", "762125": "Rachador de couros e peles", @@ -2012,20 +2144,20 @@ export const CBO_TITLES: Record = { "763115": "Enfestador de roupas", "763120": "Riscador de roupas", "763125": "Ajudante de confecção", - "763205": "Costureiro de roupas de couro e pele", + "763205": "Costureiro de roupas de couro e pele, a máquina na confecção em série", "763210": "Costureiro na confecção em série", - "763215": "Costureiro", + "763215": "Costureiro, a máquina na confecção em série", "763305": "Arrematadeira", - "763310": "Bordador", + "763310": "Bordador, à máquina", "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", + "764105": "Cortador de calçados, a máquina (exceto solas e palmilhas)", + "764110": "Cortador de solas e palmilhas, a máquina", "764115": "Preparador de calçados", "764120": "Preparador de solas e palmilhas", - "764205": "Costurador de calçados", + "764205": "Costurador de calçados, a máquina", "764210": "Montador de calçados", "764305": "Acabador de calçados", "765005": "Confeccionador de artefatos de couro (exceto sapatos)", @@ -2033,12 +2165,13 @@ export const CBO_TITLES: Record = { "765015": "Boneleiro", "765105": "Cortador de artefatos de couro (exceto roupas e calçados)", "765110": "Cortador de tapeçaria", - "765205": "Colchoeiro (confecção de colchoes)", + "765205": "Colchoeiro (confecção de colchões)", "765215": "Confeccionador de brinquedos de pano", - "765225": "Confeccionador de velas náuticas", - "765230": "Estofador de avioes", + "765225": "Confeccionador de velas náuticas, barracas e toldos", + "765230": "Estofador de aviões", "765235": "Estofador de móveis", - "765310": "Costurador de artefatos de couro", + "765240": "Tapeceiro de autos", + "765310": "Costurador de artefatos de couro, a máquina (exceto roupas e calçados)", "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", @@ -2067,20 +2200,20 @@ export const CBO_TITLES: Record = { "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", + "766410": "Revelador de filmes fotográficos, em preto e branco", + "766415": "Revelador de filmes fotográficos, em cores", "766420": "Auxiliar de radiologia (revelação fotográfica)", "768105": "Tecelão (tear manual)", - "768110": "Tecelão de tapetes", - "768115": "Tricoteiro", + "768110": "Tecelão de tapetes, a mão", + "768115": "Tricoteiro, à mão", "768120": "Redeiro", "768125": "Chapeleiro (chapéus de palha)", - "768130": "Crocheteiro", - "768205": "Bordador", + "768130": "Crocheteiro, a mão", + "768205": "Bordador, a mão", "768210": "Cerzidor", "768305": "Artífice do couro", - "768310": "Cortador de calçados", - "768315": "Costurador de artefatos de couro", + "768310": "Cortador de calçados, a mão (exceto solas)", + "768315": "Costurador de artefatos de couro, a mão (exceto roupas e calçados)", "768320": "Sapateiro (calçados sob medida)", "768325": "Seleiro", "768605": "Tipógrafo", @@ -2089,7 +2222,7 @@ export const CBO_TITLES: Record = { "768620": "Paginador", "768625": "Pintor de letreiros", "768630": "Confeccionador de carimbos de borracha", - "768705": "Gravador", + "768705": "Gravador, à mão (encadernação)", "768710": "Restaurador de livros", "770105": "Mestre (indústria de madeira e mobiliário)", "770110": "Mestre carpinteiro", @@ -2106,7 +2239,7 @@ export const CBO_TITLES: Record = { "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)", + "773205": "Operador de máquina intercaladora de placas (compensados)", "773210": "Prensista de aglomerados", "773215": "Prensista de compensados", "773220": "Preparador de aglomerantes", @@ -2114,7 +2247,7 @@ export const CBO_TITLES: Record = { "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", + "773325": "Operador de máquina de usinagem madeira, em geral", "773330": "Operador de molduradora (usinagem de madeira)", "773335": "Operador de plaina desengrossadeira", "773340": "Operador de serras (usinagem de madeira)", @@ -2122,20 +2255,20 @@ export const CBO_TITLES: Record = { "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)", + "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", + "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", + "776410": "Confeccionador de escovas, pincéis e produtos similares (a mão)", + "776415": "Confeccionador de escovas, pincéis e produtos similares (a máquina)", + "776420": "Confeccionador de móveis de vime, junco e bambu", "776425": "Esteireiro", "776430": "Vassoureiro", "777105": "Carpinteiro naval (construção de pequenas embarcações)", @@ -2147,6 +2280,7 @@ export const CBO_TITLES: Record = { "781105": "Condutor de processos robotizados de pintura", "781110": "Condutor de processos robotizados de soldagem", "781305": "Operador de veículos subaquáticos controlados remotamente", + "781310": "Operador de aeronaves não tripuladas", "781705": "Mergulhador profissional (raso e profundo)", "782105": "Operador de draga", "782110": "Operador de guindaste (fixo)", @@ -2163,6 +2297,8 @@ export const CBO_TITLES: Record = { "782305": "Motorista de carro de passeio", "782310": "Motorista de furgão ou veículo similar", "782315": "Motorista de táxi", + "782320": "Condutor de ambulância", + "782405": "Motorista de ônibus rodoviário", "782410": "Motorista de ônibus urbano", "782415": "Motorista de trólebus", "782505": "Caminhoneiro autônomo (rotas regionais e internacionais)", @@ -2179,6 +2315,8 @@ export const CBO_TITLES: Record = { "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", + "782730": "Marinheiro auxiliar de convés (marítimo e aquaviario)", + "782735": "Marinheiro auxiliar de máquinas (marítimo e aquaviário)", "782805": "Condutor de veículos de tração animal (ruas e estradas)", "782810": "Tropeiro", "782815": "Boiadeiro", @@ -2191,8 +2329,10 @@ export const CBO_TITLES: Record = { "783220": "Estivador", "783225": "Ajudante de motorista", "783230": "Bloqueiro (trabalhador portuário)", - "784105": "Embalador", - "784110": "Embalador", + "783235": "Trabalhador portuário de capatazia", + "783240": "Amarrador e desamarrado de embarcações", + "784105": "Embalador, a mão", + "784110": "Embalador, a máquina", "784115": "Operador de máquina de etiquetar", "784120": "Operador de máquina de envasar líquidos", "784125": "Operador de prensa de enfardamento", @@ -2214,7 +2354,7 @@ export const CBO_TITLES: Record = { "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", + "811010": "Operador de sala de controle de instalações químicas, petroquímicas e afins", "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)", @@ -2232,8 +2372,8 @@ export const CBO_TITLES: Record = { "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", + "811415": "Operador de alambique de funcionamento contínuo (produtos químicos, exceto petróleo)", + "811420": "Operador de aparelho de reação e conversão (produtos químicos, exceto petróleo)", "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)", @@ -2261,15 +2401,16 @@ export const CBO_TITLES: Record = { "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", + "811820": + "Operador de máquina de fabricação de produtos de higiene e limpeza (sabão, sabonete, detergente, absorvente, fraldas cotonetes e outros)", "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", + "813110": "Operador de calandra (química, petroquímica e afins)", + "813115": "Operador de extrusora (química, petroquímica e afins)", + "813120": "Operador de processo (química, petroquímica e afins)", + "813125": "Operador de produção (química, petroquímica e afins)", + "813130": "Técnico de operação (química, petroquímica e afins)", "818105": "Assistente de laboratório industrial", "818110": "Auxiliar de laboratório de análises físico-químicas", "820105": "Mestre de siderurgia", @@ -2277,9 +2418,9 @@ export const CBO_TITLES: Record = { "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", + "820205": "Supervisor de fabricação de produtos cerâmicos, porcelanatos e afins", "820210": "Supervisor de fabricação de produtos de vidro", - "821105": "Operador de centro de controle", + "821105": "Operador de centro de controle ( sinterização)", "821110": "Operador de máquina de sinterizar", "821205": "Forneiro e operador (alto-forno)", "821210": "Forneiro e operador (conversor a oxigênio)", @@ -2299,14 +2440,14 @@ export const CBO_TITLES: Record = { "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)", + "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", + "821420": "Operador de bobinadeira de tiras a quente, no acabamento de chapas e metais", "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", + "821440": "Operador de tesoura mecânica e máquina de corte, no acabamento de chapas e metais", "821445": "Preparador de sucata e aparas", "821450": "Rebarbador de metal", "822105": "Forneiro de cubilô", @@ -2324,7 +2465,7 @@ export const CBO_TITLES: Record = { "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", + "823230": "Moldador de abrasivos na fabricação de cerâmica, vidro e porcelana", "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", @@ -2338,7 +2479,7 @@ export const CBO_TITLES: Record = { "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", + "830105": "Mestre (indústria de celulose, papel e papelão)", "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", @@ -2346,16 +2487,16 @@ export const CBO_TITLES: Record = { "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)", + "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", + "833105": "Cartonageiro, a máquina", + "833110": "Confeccionador de bolsas, sacos e sacolas e papel, a máquina", + "833115": "Confeccionador de sacos de celofane, a máquina", "833120": "Operador de máquina de cortar e dobrar papelão", "833125": "Operador de prensa de embutir papelão", - "833205": "Cartonageiro", + "833205": "Cartonageiro, a mão (caixas de papelão)", "840105": "Supervisor de produção da indústria alimentícia", "840110": "Supervisor da indústria de bebidas", "840115": "Supervisor da indústria de fumo", @@ -2400,14 +2541,14 @@ export const CBO_TITLES: Record = { "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", + "841805": "Operador de forno (fabricação de pães, biscoitos e similares)", + "841810": "Operador de máquinas de fabricação de doces, salgados e massas alimentícias", "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", + "842125": "Operador de máquina (fabricação de 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", @@ -2418,7 +2559,7 @@ export const CBO_TITLES: Record = { "842235": "Degustador de charutos", "848105": "Defumador de carnes e pescados", "848110": "Salgador de alimentos", - "848115": "Salsicheiro (fabricação de lingüiça", + "848115": "Salsicheiro (fabricação de lingüiça, salsicha e produtos similares)", "848205": "Pasteurizador", "848210": "Queijeiro na fabricação de laticínio", "848215": "Manteigueiro na fabricação de laticínio", @@ -2438,8 +2579,10 @@ export const CBO_TITLES: Record = { "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", + "860110": + "Supervisor de operação de fluidos (distribuição, captação, tratamento de água, gases, vapor)", + "860115": + "Supervisor de operação elétrica (geração, transmissão e distribuição de energia elétrica)", "861105": "Operador de central hidrelétrica", "861110": "Operador de quadro de distribuição de energia elétrica", "861115": "Operador de central termoelétrica", @@ -2451,18 +2594,23 @@ export const CBO_TITLES: Record = { "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", + "862150": "Operador de máquinas fixas, em geral", + "862155": + "Operador de utilidade (produção e distribuição de vapor, gás, óleo, combustível, energia, oxigênio)", + "862160": "Operador de abastecimento de combustível de aeronave", + "862205": "Operador de estação de captação, tratamento e distribuição de água", "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", + "862310": + "Operador de forno de incineração no tratamento de água, efluentes e resíduos industriais", + "862405": + "Operador de instalação de extração, processamento, envasamento e distribuição de gases", "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", + "910110": "Supervisor de manutenção de aparelhos térmicos, de climatização e de refrigeração", + "910115": + "Supervisor de manutenção de bombas, motores, compressores e equipamentos de transmissão", "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", @@ -2477,8 +2625,8 @@ export const CBO_TITLES: Record = { "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", + "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, em geral", "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", @@ -2487,12 +2635,13 @@ export const CBO_TITLES: Record = { "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", + "913125": "Vulcanizador de correias transportadoras", + "914105": "Mecânico de manutenção de aeronaves, em geral", "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", + "914405": "Mecânico de manutenção de automóveis, motocicletas e veículos similares", "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", @@ -2508,27 +2657,31 @@ export const CBO_TITLES: Record = { "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", + "919205": + "Mecânico de manutenção de máquinas cortadoras de grama, roçadeiras, motosserras e similares", "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", + "950110": "Supervisor de manutenção eletromecânica industrial, comercial e predial", "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", + "951315": "Monitor de sistemas eletrônicos de segurança interno", + "951320": "Monitor de sistemas eletrônicos de segurança externo", "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", + "953115": + "Eletricista de instalações (veículos automotores e máquinas operatrizes, exceto aeronaves e embarcações)", "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", + "954210": "Reparador de rádio, tv e som", "954305": "Reparador de equipamentos de escritório", "991105": "Conservador de via permanente (trilhos)", "991110": "Inspetor de via permanente (trilhos)", @@ -2547,37 +2700,38 @@ export const CBO_TITLES: Record = { "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", + "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 policia militar", + "021110": "Sargento da policia 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", }; /** * Shape a CBO code has to be written in: the 6 digits, optionally split into the printed - * groups of 4 and 2 by whitespace or the usual mask characters. + * groups of 4 and 2 by a single whitespace or mask character. */ -export const CBO_FORMAT_REGEX = /^\d{4}[\s.\-/]*\d{2}$/; +export const CBO_FORMAT_REGEX = /^\d{4}[\s.\-/]?\d{2}$/; diff --git a/src/_internals/constants/cfop.ts b/src/_internals/constants/cfop.ts index c9342349..7184c7fe 100644 --- a/src/_internals/constants/cfop.ts +++ b/src/_internals/constants/cfop.ts @@ -1,15 +1,23 @@ /** * CFOP (Código Fiscal de Operações e Prestações) table, indexed by the 4 digit code. * + * Built from the consolidated Anexo II of Convênio SINIEF s/nº of 15 December 1970, the text in + * force (given its current wording by Ajuste SINIEF 03/24 and last amended by Ajuste SINIEF + * 39/25), not from the frozen 2001 text of Ajuste SINIEF 07/01. + * * Group and subgroup headers (codes ending in "00" or "50", e.g. "1000", "1100", "1150") * are section titles from the official nomenclature rather than operable codes, so they - * are excluded: the Ajuste SINIEF 07/01 prints them in upper case with no "Classificam-se - * neste código" body, unlike the operable codes they head (1151, 1152, ...). + * are excluded: the annex prints them in upper case with no "Classificam-se neste código" + * body, unlike the operable codes they head (1151, 1152, ...). * * Generated by `node ./scripts/cfop.ts`. Do not edit by hand. * - * @see Based on: https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24 + * Anexo II of Convênio SINIEF s/nº 1970, the CFOP table in force. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 + * Convênio SINIEF s/nº 1970, the consolidated text the annex belongs to. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 + * Ajuste SINIEF 07/01, the historical text that gave the CFOP its 4 digit form. */ export const CFOP_TABLE: Record = { "1101": "Compra para industrialização ou produção rural", @@ -23,21 +31,30 @@ export const CFOP_TABLE: Record = { "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", + "1120": + "Compra para industrialização ou produção rural, 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", + "1126": "Compra para utilização na prestação de serviço sujeita ao ICMS", + "1128": "Compra para utilização na prestação de serviço sujeita ao ISSQN", + "1131": + "Entrada de mercadoria, com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", + "1132": + "Fixação de preço de produção do estabelecimento produtor, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo, para comercialização", + "1135": + "Fixação de preço de produção do estabelecimento produtor, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo, para industrialização", "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", + "1159": "Entrada decorrente do fornecimento de produto ou mercadoria em ato cooperativo", "1201": "Devolução de venda de produção do estabelecimento", - "1202": "Devolução de venda de mercadoria adquirida ou recebida de terceiros", + "1202": + "Devolução de venda de mercadoria adquirida ou recebida de terceiros, ou qualquer devolução de mercadoria efetuada pelo MEI com exceção das classificadas nos códigos 1.503, 1.504, 1.505 e 1.506", "1203": "Devolução de venda de produção do estabelecimento, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", "1204": @@ -47,6 +64,15 @@ export const CFOP_TABLE: Record = { "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", + "1212": + "Devolução de venda no mercado interno de mercadoria industrializada e insumo importado sob o Regime Aduaneiro Especial de Entreposto Industrial sob Controle Informatizado do Sistema Público de Escrituração Digital (Recof-Sped)", + "1213": + "Devolução de remessa de produção do estabelecimento, com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", + "1214": + "Devolução referente à fixação de preço de produção do estabelecimento produtor em ato cooperativo", + "1215": "Devolução de fornecimento de produção do estabelecimento em ato cooperativo", + "1216": + "Devolução de fornecimento de mercadoria adquirida ou recebida de terceiros em ato cooperativo", "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", @@ -92,17 +118,21 @@ export const CFOP_TABLE: Record = { "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", - "1451": "Retorno de animal do estabelecimento produtor", - "1452": "Retorno de insumo não utilizado na produção", + "1451": "Entrada de animal - Sistema de Integração e Parceria Rural", + "1452": "Entrada de insumo - Sistema de Integração e Parceria Rural", + "1453": "Retorno do animal ou da produção - Sistema de Integração e Parceria Rural", + "1454": "Retorno simbólico do animal ou da produção - Sistema de Integração e Parceria Rural", + "1455": "Retorno de insumo não utilizado na produção - Sistema de Integração e Parceria Rural", + "1456": "Entrada referente à remuneração do produtor no Sistema de Integração e Parceria Rural", "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", + "Entrada decorrente de devolução, inclusive simbólica, 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", + "Entrada decorrente de devolução, inclusive 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", + "Entrada decorrente de devolução, inclusive simbólica, de mercadorias adquiridas ou recebidas de terceiros, remetidas para formação de lote de exportação", "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", @@ -117,31 +147,37 @@ export const CFOP_TABLE: Record = { "1604": "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", + "1651": "Compra de combustíveis ou lubrificantes para industrialização subsequente", + "1652": "Compra de combustíveis ou lubrificantes para comercialização", + "1653": "Compra de combustíveis ou lubrificantes por consumidor ou usuário final", + "1657": "Retorno de remessa de combustíveis ou lubrificantes para venda fora do estabelecimento", + "1658": "Transferência de combustíveis ou lubrificantes para industrialização", + "1659": "Transferência de combustíveis ou lubrificantes 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", + "Devolução de venda de combustíveis ou lubrificantes destinados à industrialização subsequente", + "1661": "Devolução de venda de combustíveis ou lubrificantes destinados à 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", + "Devolução de venda de combustíveis ou lubrificantes destinados a consumidor ou usuário final", + "1663": "Entrada de combustíveis ou lubrificantes para armazenagem", + "1664": "Retorno de combustíveis ou lubrificantes remetidos 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", + "1904": + "Retorno de remessa para venda fora do estabelecimento, ou qualquer entrada e retorno de remessa efetuada pelo MEI com exceção dos classificados nos códigos 1.202, 1.503, 1.504, 1.505 e 1.506", + "1905": + "Entrada de mercadoria recebida para depósito em depósito fechado, armazém geral ou outro estabelecimentoda mesma empresa ou de terceiro", + "1906": + "Retorno de mercadoria remetida para depósito fechado, armazém geral ou outro estabelecimento, da mesma empresa ou de terceiro", + "1907": + "Retorno simbólico de mercadoria remetida para depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "1908": "Entrada de bem por conta de contrato de comodato ou locação", + "1909": "Retorno de bem remetido por conta de contrato de comodato ou locação", "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", + "1912": "Entrada de mercadoria ou bem recebido para demonstração ou mostruário", + "1913": "Retorno de mercadoria ou bem remetido para demonstração, mostruário ou treinamento", "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", @@ -149,8 +185,8 @@ export const CFOP_TABLE: Record = { "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", + "1920": "Entrada de embalagens, bombonas, vasilhames, sacarias, pallets ou assemelhados", + "1921": "Retorno de embalagens, bombonas, vasilhames, sacarias, pallets ou assemelhados", "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", @@ -164,7 +200,9 @@ export const CFOP_TABLE: Record = { "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", + "1933": "Aquisição de serviço sujeito ao ISSQN", + "1934": + "Entrada simbólica de mercadoria recebida para depósito em depósito fechado ou armazém geral", "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", @@ -177,20 +215,30 @@ export const CFOP_TABLE: Record = { "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", + "2120": + "Compra para industrialização ou produção rural, em venda à ordem, já recebida do vendedor remetente", "2121": "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", + "2126": "Compra para utilização na prestação de serviço sujeita ao ICMS", + "2128": "Compra para utilização na prestação de serviço sujeita ao ISSQN", + "2131": + "Entrada de mercadoria, com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", + "2132": + "Fixação de preço de produção do estabelecimento produtor, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo, para comercialização", + "2135": + "Fixação de preço de produção do estabelecimento produtor, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo, para industrialização", "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", + "2159": "Entrada decorrente do fornecimento de produto ou mercadoria em ato cooperativo", "2201": "Devolução de venda de produção do estabelecimento", - "2202": "Devolução de venda de mercadoria adquirida ou recebida de terceiros", + "2202": + "Devolução de venda de mercadoria adquirida ou recebida de terceiros, ou qualquer devolução de mercadoria efetuada pelo MEI com exceção das classificadas nos códigos 2.503, 2.504, 2.505 e 2.506", "2203": "Devolução de venda de produção do estabelecimento, destinada à Zona Franca de Manaus ou Áreas de Livre Comércio", "2204": @@ -200,6 +248,15 @@ export const CFOP_TABLE: Record = { "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", + "2212": + "Devolução de venda no mercado interno de mercadoria industrializada e insumo importado sob o Regime Aduaneiro Especial de Entreposto Industrial sob Controle Informatizado do Sistema Público de Escrituração Digital (Recof-Sped)", + "2213": + "Devolução de remessa de produção do estabelecimento, com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", + "2214": + "Devolução referente à fixação de preço de produção do estabelecimento produtor, em ato cooperativo", + "2215": "Devolução de fornecimento de produção do estabelecimento em ato cooperativo", + "2216": + "Devolução de fornecimento de mercadoria adquirida ou recebida de terceiros em ato cooperativo", "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", @@ -224,7 +281,7 @@ export const CFOP_TABLE: Record = { "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", + "Compra para industrialização ou produção rural em operação com mercadoria sujeita ao regime de substituição tributária", "2403": "Compra para comercialização em operação com mercadoria sujeita ao regime de substituição tributária", "2406": @@ -236,22 +293,28 @@ export const CFOP_TABLE: Record = { "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", + "Devolução de venda de produção do estabelecimento em operação com produto sujeito ao regime de substituiçã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", + "2451": "Entrada de animal - Sistema de Integração e Parceria Rural", + "2452": "Entrada de insumo - Sistema de Integração e Parceria Rural", + "2453": "Retorno do animal ou da produção - Sistema de Integração e Parceria Rural", + "2454": "Retorno simbólico do animal ou da produção - Sistema de Integração e Parceria Rural", + "2455": "Retorno de insumo não utilizado na produção - Sistema de Integração e Parceria Rural", + "2456": "Entrada referente à remuneração do produtor no Sistema de Integração e Parceria Rural", "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", + "Entrada decorrente de devolução, inclusive simbólica, 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", + "Entrada decorrente de devolução, inclusive 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", + "Entrada decorrente de devolução, inclusive simbólica, de mercadorias adquiridas ou recebidas de terceiros, remetidas para formação de lote de exportação", "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", @@ -260,32 +323,37 @@ export const CFOP_TABLE: Record = { "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", + "2651": "Compra de combustíveis ou lubrificantes para industrialização subsequente", + "2652": "Compra de combustíveis ou lubrificantes para comercialização", + "2653": "Compra de combustíveis ou lubrificantes por consumidor ou usuário final", + "2657": "Retorno de remessa de combustíveis ou lubrificantes para venda fora do estabelecimento", + "2658": "Transferência de combustíveis ou lubrificantes para industrialização", + "2659": "Transferência de combustíveis ou lubrificantes 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", + "Devolução de venda de combustíveis ou lubrificantes destinados à industrialização subsequente", + "2661": "Devolução de venda de combustíveis ou lubrificantes destinados à 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", + "Devolução de venda de combustíveis ou lubrificantes destinados a consumidor ou usuário final", + "2663": "Entrada de combustíveis ou lubrificantes para armazenagem", + "2664": "Retorno de combustíveis ou lubrificantes remetidos 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", + "2904": + "Retorno de remessa para venda fora do estabelecimento, ou qualquer entrada e retorno de remessa efetuada pelo MEI com exceção dos classificados nos códigos 2.202, 2.503, 2.504, 2.505 e 2.506", + "2905": + "Entrada de mercadoria recebida para depósito em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "2906": + "Retorno de mercadoria remetida para depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "2907": + "Retorno simbólico de mercadoria remetida para depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "2908": "Entrada de bem por conta de contrato de comodato ou locação", + "2909": "Retorno de bem remetido por conta de contrato de comodato ou locação", "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", + "2912": "Entrada de mercadoria ou bem recebido para demonstração ou mostruário", + "2913": "Retorno de mercadoria ou bem remetido para demonstração, mostruário ou treinamento", "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", @@ -293,8 +361,8 @@ export const CFOP_TABLE: Record = { "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", + "2920": "Entrada de embalagens, bombonas, vasilhames, sacarias, pallets ou assemelhados", + "2921": "Retorno de embalagens, bombonas, vasilhames, sacarias, pallets ou assemelhados", "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", @@ -306,18 +374,25 @@ export const CFOP_TABLE: Record = { "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", + "2933": "Aquisição de serviço sujeito ao ISSQN", + "2934": + "Entrada simbólica de mercadoria recebida para depósito em depósito fechado ou armazém geral", + "2949": "Outra entrada de mercadoria ou prestação de serviço não especificada", "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"', + "3126": "Compra para utilização na prestação de serviço sujeita ao ICMS", + "3127": "Compra para industrialização sob o regime de “drawback”", + "3128": "Compra para utilização na prestação de serviço sujeita ao ISSQN", + "3129": + "Compra para industrialização sob o Regime Aduaneiro Especial de Entreposto Industrial sob Controle Informatizado do Sistema Público de Escrituração Digital (Recof-Sped)", "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"', + "3211": "Devolução de venda de produção do estabelecimento sob o regime de “drawback”", + "3212": + "Devolução de venda no mercado externo de mercadoria industrializada sob o Regime Aduaneiro Especial de Entreposto Industrial sob Controle Informatizado do Sistema Público de Escrituração Digital (Recof-Sped)", "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", "3351": "Aquisição de serviço de transporte para execução de serviço da mesma natureza", @@ -331,16 +406,21 @@ export const CFOP_TABLE: Record = { "3503": "Devolução de mercadoria exportada que tenha sido recebida com fim específico de exportação", "3551": "Compra de bem para o ativo imobilizado", + "3552": + "Entrada de produto destinado para uso ou consumo de bordo, em embarcações ou aeronaves exclusivamente em tráfego internacional com destino ao exterior", "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", + "3651": "Compra de combustíveis ou lubrificantes para industrialização subsequente", + "3652": "Compra de combustíveis ou lubrificantes para comercialização", + "3653": "Compra de combustíveis ou lubrificantes por consumidor ou usuário final", + "3667": + "Entrada de combustíveis ou lubrificantes para consumo final, em embarcações ou aeronaves exclusivamente em tráfego internacional com destino ao exterior", "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", + "3949": "Outra entrada de mercadoria ou prestação de serviço não especificada", "5101": "Venda de produção do estabelecimento", - "5102": "Venda de mercadoria adquirida ou recebida de terceiros", + "5102": + "Venda de mercadoria adquirida ou recebida de terceiros, ou qualquer venda de mercadoria efetuada pelo MEI com exceção das saídas classificadas nos códigos 5.501, 5.502, 5.504 e 5.505", "5103": "Venda de produção do estabelecimento, efetuada fora do estabelecimento", "5104": "Venda de mercadoria adquirida ou recebida de terceiros, efetuada fora do estabelecimento", @@ -362,7 +442,7 @@ export const CFOP_TABLE: Record = { "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", + "Venda de produção do estabelecimento industrial ou rural, 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": @@ -374,21 +454,39 @@ export const CFOP_TABLE: Record = { "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", + "5129": + "Venda de mercadoria industrializada e de insumo importado sob o amparo do Regime Aduaneiro Especial de Entreposto Industrial sob Controle Informatizado do Sistema Público de Escrituração Digital (Recof-Sped)", + "5131": + "Remessa de produção do estabelecimento, com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", + "5132": + "Fixação de preço de produção do estabelecimento, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", "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", + "5155": + "Transferência de produção do estabelecimento para depósito em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro, que não deva por ele transitar", "5156": - "Transferência de mercadoria adquirida ou recebida de terceiros, que não deva por ele transitar", + "Transferência de mercadoria adquirida ou recebida de terceiros para depósito em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro, que não deva por ele transitar", + "5159": "Fornecimento de produção do estabelecimento em ato cooperativo", + "5160": "Fornecimento de mercadoria adquirida ou recebida de terceiros em ato cooperativo", "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", + "5202": + "Devolução de compra para comercialização, ou qualquer devolução de mercadorias efetuada pelo MEI com exceção das classificadas no código 5.503", + "5205": "Anulação de valor relativo à aquisição de serviço de comunicação", + "5206": "Anulação de valor relativo à 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", + "5210": "Devolução de compra para utilização na prestação de serviço", + "5213": + "Devolução de entrada, inclusive simbólica, de mercadoria com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", + "5214": + "Devolução referente à fixação de preço de produção do estabelecimento produtor, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo, para comercialização", + "5215": + "Devolução referente à fixação de preço de produção do estabelecimento produtor, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo, para industrialização", + "5216": + "Devolução de entrada decorrente do fornecimento de produto ou mercadoria em ato cooperativo", "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", @@ -438,12 +536,17 @@ export const CFOP_TABLE: Record = { "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", + "Devolução de mercadoria destinada para 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", - "5451": "Remessa de animal e de insumo para estabelecimento produtor", + "5451": "Remessa de animal - Sistema de Integração e Parceria Rural", + "5452": "Remessa de insumo - Sistema de Integração e Parceria Rural", + "5453": "Retorno de animal ou da produção - Sistema de Integração e Parceria Rural", + "5454": "Retorno simbólico de animal ou da produção - Sistema de Integração e Parceria Rural", + "5455": "Retorno de insumos não utilizados na produção - Sistema de Integração e Parceria Rural", + "5456": "Saída referente à remuneração do produtor - Sistema de Integração e Parceria Rural", "5501": "Remessa de produção do estabelecimento, com fim específico de exportação", "5502": "Remessa de mercadoria adquirida ou recebida de terceiros, com fim específico de exportação", @@ -457,8 +560,8 @@ export const CFOP_TABLE: Record = { "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", + "5556": "Devolução de compra de material para uso ou consumo", + "5557": "Transferência de material para 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", @@ -466,45 +569,51 @@ export const CFOP_TABLE: Record = { "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", + "Venda de combustíveis ou lubrificantes de produção do estabelecimento destinados à industrialização subsequente", "5652": - "Venda de combustível ou lubrificante de produção do estabelecimento destinado à comercialização", + "Venda de combustíveis ou lubrificantes de produção do estabelecimento destinados à comercialização", "5653": - "Venda de combustível ou lubrificante de produção do estabelecimento destinado a consumidor ou usuário final", + "Venda de combustíveis ou lubrificantes de produção do estabelecimento destinados a consumidor ou usuário final", "5654": - "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado à industrialização subseqüente", + "Venda de combustíveis ou lubrificantes adquiridos ou recebidos de terceiros destinados à industrialização subsequente", "5655": - "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado à comercialização", + "Venda de combustíveis ou lubrificantes adquiridos ou recebidos de terceiros destinados à comercialização", "5656": - "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado a consumidor ou usuário final", + "Venda de combustíveis ou lubrificantes adquiridos ou recebidos de terceiros destinados 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", + "Remessa de combustíveis ou lubrificantes adquiridos ou recebidos de terceiros para venda fora do estabelecimento", + "5658": "Transferência de combustíveis ou lubrificantes de produção do estabelecimento", + "5659": "Transferência de combustíveis ou lubrificantes adquiridos ou recebidos 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", + "Devolução de compra de combustíveis ou lubrificantes adquiridos para industrialização subsequente", + "5661": "Devolução de compra de combustíveis ou lubrificantes adquiridos 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", + "Devolução de compra de combustíveis ou lubrificantes adquiridos por consumidor ou usuário final", + "5663": "Remessa para armazenagem de combustíveis ou lubrificantes", + "5664": "Retorno de combustíveis ou lubrificantes recebidos para armazenagem", + "5665": "Retorno simbólico de combustíveis ou lubrificantes recebidos para armazenagem", "5666": - "Remessa por conta e ordem de terceiros de combustível ou lubrificante recebido para armazenagem", + "Remessa por conta e ordem de terceiros de combustíveis ou lubrificantes recebidos para armazenagem", + "5667": + "Venda de combustíveis ou lubrificantes a consumidor ou usuário final estabelecido em outra unidade da Federação", "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", + "5904": + "Remessa para venda fora do estabelecimento, ou qualquer remessa efetuada pelo MEI com exceção das classificadas nos códigos 5.502 e 5.505", + "5905": + "Remessa para depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "5906": + "Retorno de mercadoria depositada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "5907": + "Retorno simbólico de mercadoria depositada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "5908": "Remessa de bem por conta de contrato de comodato ou locação", + "5909": "Retorno de bem recebido por conta de contrato de comodato ou locação", "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", + "5912": "Remessa de mercadoria ou bem para demonstração, mostruário ou treinamento", + "5913": "Retorno de mercadoria ou bem recebido para demonstração ou mostruário", "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", @@ -512,11 +621,14 @@ export const CFOP_TABLE: Record = { "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", + "5920": + "Remessa de embalagens, bombonas, vasilhames, sacarias, pallets, containers ou assemelhados", + "5921": + "Devolução de embalagens, bombonas, vasilhames, sacarias, pallets, containers ou assemelhados", "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", + "5923": + "Remessa de mercadoria por conta e ordem de terceiros, em venda à ordem ou em operações com armazém geral ou depósito fechado", "5924": "Remessa para industrialização por conta e ordem do adquirente da mercadoria, quando esta não transitar pelo estabelecimento do adquirente", "5925": @@ -524,24 +636,28 @@ export const CFOP_TABLE: Record = { "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", + "Lançamento efetuado a título de baixa de estoque decorrente de perda, roubo, deterioração", "5928": - "Lançamento efetuado a título de baixa de estoque decorrente do encerramento da atividade da empresa", + "Lançamento efetuado a título de baixa de estoque decorrente do encerramento da atividade do estabelecimento da empresa ou transferência por venda do fundo de comércio", "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", + "Lançamento efetuado em decorrência de emissão de documento fiscal relativo à operação ou prestação também acobertada por documento fiscal do varejo", "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", + "5933": "Prestação de serviço sujeita ao ISSQN", + "5934": "Remessa simbólica de mercadoria depositada em armazém geral ou depósito fechado", + "5949": "Outra saída de mercadoria ou prestação de serviço não especificada", "6101": "Venda de produção do estabelecimento", - "6102": "Venda de mercadoria adquirida ou recebida de terceiros", + "6102": + "Venda de mercadoria adquirida ou recebida de terceiros, ou qualquer venda de mercadoria efetuada pelo MEI com exceção das saídas classificadas nos códigos 6.501, 6.502, 6.504 e 6.505", "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", + "6105": + "Venda de produção do estabelecimento armazenada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro, que não deva por ele transitar", + "6106": + "Venda de mercadoria adquirida ou recebida de terceiros armazenada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro, 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": @@ -560,7 +676,7 @@ export const CFOP_TABLE: Record = { "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", + "Venda de produção do estabelecimento industrial ou rural, 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": @@ -572,21 +688,39 @@ export const CFOP_TABLE: Record = { "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", + "6129": + "Venda de mercadoria industrializada e de insumo importado sob o amparo do Regime Aduaneiro Especial de Entreposto Industrial sob Controle Informatizado do Sistema Público de Escrituração Digital (Recof-Sped)", + "6131": + "Remessa de produção do estabelecimento, com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", + "6132": + "Fixação de preço de produção do estabelecimento, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", "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", - "6155": "Transferência de produção do estabelecimento, que não deva por ele transitar", + "6155": + "Transferência de produção do estabelecimento armazenada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro, que não deva por ele transitar", "6156": - "Transferência de mercadoria adquirida ou recebida de terceiros, que não deva por ele transitar", + "Transferência de mercadoria adquirida ou recebida de terceiros armazenada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro, que não deva por ele transitar", + "6159": "Fornecimento de produção do estabelecimento em ato cooperativo", + "6160": "Fornecimento de mercadoria adquirida ou recebida de terceiros em ato cooperativo", "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", + "6202": + "Devolução de compra para comercialização, ou qualquer devolução de mercadoria efetuada pelo MEI com exceção das classificadas no código 6.503", + "6205": "Anulação de valor relativo à aquisição de serviço de comunicação", + "6206": "Anulação de valor relativo à 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", + "6213": + "Devolução de entrada, inclusive simbólica, de mercadoria, com previsão de posterior ajuste ou fixação de preço, em ato cooperativo", + "6214": + "Devolução referente à fixação de preço de produção do estabelecimento produtor, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo, para comercialização", + "6215": + "Devolução referente à fixação de preço de produção do estabelecimento produtor, inclusive quando remetidas anteriormente com previsão de posterior ajuste ou fixação de preço, em ato cooperativo, para industrialização", + "6216": + "Devolução de entrada decorrente do fornecimento de produto ou mercadoria em ato cooperativo", "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", @@ -615,6 +749,8 @@ export const CFOP_TABLE: Record = { "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", + "6360": + "Prestação de serviço de transporte a contribuinte substituto em relação ao serviço de transporte", "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": @@ -634,11 +770,17 @@ export const CFOP_TABLE: Record = { "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", + "Devolução de mercadoria destinada para 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", + "6451": "Remessa de animal - Sistema de Integração e Parceria Rural", + "6452": "Remessa de insumo - Sistema de Integração e Parceria Rural", + "6453": "Retorno de animal ou da produção - Sistema de Integração e Parceria Rural", + "6454": "Retorno simbólico de animal ou da produção - Sistema de Integração e Parceria Rural", + "6455": "Retorno de insumos não utilizados na produção - Sistema de Integração e Parceria Rural", + "6456": "Saída referente à remuneração do produtor - Sistema de Integração e Parceria Rural", "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", @@ -652,49 +794,55 @@ export const CFOP_TABLE: Record = { "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", + "6556": "Devolução de compra de material para uso ou consumo", + "6557": "Transferência de material para 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", + "Venda de combustíveis ou lubrificantes de produção do estabelecimento destinados à industrialização subsequente", "6652": - "Venda de combustível ou lubrificante de produção do estabelecimento destinado à comercialização", + "Venda de combustíveis ou lubrificantes de produção do estabelecimento destinados à comercialização", "6653": - "Venda de combustível ou lubrificante de produção do estabelecimento destinado a consumidor ou usuário final", + "Venda de combustíveis ou lubrificantes de produção do estabelecimento destinados a consumidor ou usuário final", "6654": - "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado à industrialização subseqüente", + "Venda de combustíveis ou lubrificantes adquiridos ou recebidos de terceiros destinados à industrialização subsequente", "6655": - "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado à comercialização", + "Venda de combustíveis ou lubrificantes adquiridos ou recebidos de terceiros destinados à comercialização", "6656": - "Venda de combustível ou lubrificante adquirido ou recebido de terceiros destinado a consumidor ou usuário final", + "Venda de combustíveis ou lubrificantes adquiridos ou recebidos de terceiros destinados 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", + "Remessa de combustíveis ou lubrificantes adquiridos ou recebidos de terceiros para venda fora do estabelecimento", + "6658": "Transferência de combustíveis ou lubrificantes de produção do estabelecimento", + "6659": "Transferência de combustíveis ou lubrificantes adquiridos ou recebidos 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", + "Devolução de compra de combustíveis ou lubrificantes adquiridos para industrialização subsequente", + "6661": "Devolução de compra de combustíveis ou lubrificantes adquiridos 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", + "Devolução de compra de combustíveis ou lubrificantes adquiridos por consumidor ou usuário final", + "6663": "Remessa para armazenagem de combustíveis ou lubrificantes", + "6664": "Retorno de combustíveis ou lubrificantes recebidos para armazenagem", + "6665": "Retorno simbólico de combustíveis ou lubrificantes recebidos para armazenagem", "6666": - "Remessa por conta e ordem de terceiros de combustível ou lubrificante recebido para armazenagem", + "Remessa por conta e ordem de terceiros de combustíveis ou lubrificantes recebidos para armazenagem", + "6667": + "Venda de combustíveis ou lubrificantes a consumidor ou usuário final estabelecido em outra unidade da Federação diferente da que ocorrer o consumo", "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", + "6904": + "Remessa para venda fora do estabelecimento, ou qualquer remessa efetuada pelo MEI com exceção das classificadas nos códigos 6.502 e 6.505", + "6905": + "Remessa para depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "6906": + "Retorno de mercadoria depositada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "6907": + "Retorno simbólico de mercadoria depositada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro", + "6908": "Remessa de bem por conta de contrato de comodato ou locação", + "6909": "Retorno de bem recebido por conta de contrato de comodato ou locação", "6910": "Remessa em bonificação, doação ou brinde", "6911": "Remessa de amostra grátis", - "6912": "Remessa de mercadoria ou bem para demonstração", - "6913": "Retorno de mercadoria ou bem recebido para demonstração", + "6912": "Remessa de mercadoria ou bem para demonstração, mostruário ou treinamento", + "6913": "Retorno de mercadoria ou bem recebido para demonstração ou mostruário", "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", @@ -702,45 +850,65 @@ export const CFOP_TABLE: Record = { "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", + "6920": + "Remessa de embalagens, bombonas, vasilhames, sacarias, pallets, containers ou assemelhados", + "6921": + "Devolução de embalagens, bombonas, vasilhames, sacarias, pallets, containers ou assemelhados", "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", + "6923": + "Remessa de mercadoria por conta e ordem de terceiros, em venda à ordem ou em operações com armazém geral ou depósito fechado", "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", + "Lançamento efetuado em decorrência de emissão de documento fiscal relativo à operação ou prestação também acobertada por documento fiscal do varejo", "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", + "6933": "Prestação de serviço sujeita ao ISSQN", + "6934": "Remessa simbólica de mercadoria depositada em armazém geral ou depósito fechado", + "6949": "Outra saída de mercadoria ou prestação de serviço não especificada", "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"', + "7105": + "Venda de produção do estabelecimento armazenada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro, que não deva por ele transitar", + "7106": + "Venda de mercadoria adquirida ou recebida de terceiros armazenada em depósito fechado, armazém geral ou outro estabelecimento da mesma empresa ou de terceiro, que não deva por ele transitar", + "7127": "Venda de produção do estabelecimento sob o regime de “drawback”", + "7129": + "Venda de produção do estabelecimento ao mercado externo de mercadoria industrializada sob o amparo do Regime Aduaneiro Especial de Entreposto Industrial sob Controle Informatizado do Sistema Público de Escrituração Digital (Recof-Sped)", "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", + "7206": "Anulação de valor relativo à 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"', + "7211": "Devolução de compras para industrialização sob o regime de “drawback”", + "7212": + "Devolução de compras para industrialização sob o regime de Regime Aduaneiro Especial de Entreposto Industrial sob Controle Informatizado do Sistema Público de Escrituração Digital (Recof-Sped)", "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", "7358": "Prestação de serviço de transporte", "7501": "Exportação de mercadorias recebidas com fim específico de exportação", + "7504": "Exportação de mercadoria que foi objeto de formação de lote de exportação", "7551": "Venda de bem do ativo imobilizado", + "7552": + "Saída de produto destinado para uso ou consumo de bordo, em embarcações ou aeronaves exclusivamente em tráfego internacional com destino ao exterior", "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", + "7556": "Devolução de compra de material para uso ou consumo", + "7651": "Venda de combustíveis ou lubrificantes de produção do estabelecimento", + "7654": "Venda de combustíveis ou lubrificantes adquiridos ou recebidos de terceiros", + "7667": "Saída de combustíveis ou lubrificantes a consumidor ou usuário final", "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", + "Lançamento efetuado a tí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 especificada", }; + +/** + * Shape a CFOP code has to be written in: the 4 digits, optionally split after the series + * digit by a single whitespace or mask character, the way the annex prints them ("1.101"). + */ +export const CFOP_FORMAT_REGEX = /^\d[\s.\-/]?\d{3}$/; diff --git a/src/_internals/constants/cnae.ts b/src/_internals/constants/cnae.ts index 41027437..3ae058dd 100644 --- a/src/_internals/constants/cnae.ts +++ b/src/_internals/constants/cnae.ts @@ -1521,6 +1521,6 @@ export const CNAE_SUBCLASSES: Record = { /** * Shape a CNAE subclass code has to be written in: the 7 digits, optionally split into the - * printed `NNNN-N/NN` groups by whitespace or the usual mask characters. + * printed `NNNN-N/NN` groups by a single whitespace or mask character. */ -export const CNAE_FORMAT_REGEX = /^\d{4}[\s.\-/]*\d[\s.\-/]*\d{2}$/; +export const CNAE_FORMAT_REGEX = /^\d{4}[\s.\-/]?\d[\s.\-/]?\d{2}$/; diff --git a/src/is-valid-csosn/constants.ts b/src/is-valid-csosn/constants.ts index f6f9add4..565ac087 100644 --- a/src/is-valid-csosn/constants.ts +++ b/src/is-valid-csosn/constants.ts @@ -1,9 +1,13 @@ /** - * CSOSN (Código de Situação da Operação no Simples Nacional) codes, per Ajuste SINIEF - * 03/2010, which instituted the Código de Situação da Operação no Simples Nacional. + * CSOSN (Código de Situação da Operação no Simples Nacional) codes, as the consolidated Anexo + * III-A of Convênio SINIEF s/nº 1970 carries them (added by Ajuste SINIEF 39/23, effective + * from 01.10.24). Ajuste SINIEF 03/2010 instituted the table and is kept as the historical + * citation. * - * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10 Ajuste - * SINIEF 03/2010, which instituted the CSOSN table. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 + * Convênio SINIEF s/nº 1970, whose Anexo III-A carries the CSOSN table in force. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10 + * Ajuste SINIEF 03/2010, which instituted the CSOSN table. */ export const CSOSN_CODES = [ "101", @@ -17,3 +21,9 @@ export const CSOSN_CODES = [ "500", "900", ] as const; + +/** + * Shape a CSOSN code has to be written in: the 3 digits, optionally split by a single + * whitespace or mask character. + */ +export const CSOSN_FORMAT_REGEX = /^\d[\s.\-/]?\d[\s.\-/]?\d$/; diff --git a/src/is-valid-cst/constants.ts b/src/is-valid-cst/constants.ts index d8fd698a..84ea07aa 100644 --- a/src/is-valid-cst/constants.ts +++ b/src/is-valid-cst/constants.ts @@ -1,29 +1,41 @@ /** - * CST (Código de Situação Tributária) code tables per tax: Ajuste SINIEF 03/1994 with the - * Tabela A of Ajuste SINIEF 20/2012 and 15/2013 and the Tabela B of Ajuste SINIEF 06/2000 for - * ICMS, and Instrução Normativa RFB nº 1.009/2010 (Tabelas I to III) for IPI, PIS and COFINS. + * CST (Código de Situação Tributária) code tables per tax: the consolidated Anexo I of + * Convênio SINIEF s/nº 1970 for ICMS (Tabela A given by Ajuste SINIEF 20/2012 and 15/2013, + * Tabela B given by Ajuste SINIEF 39/23 and amended by Ajuste SINIEF 20/24), and Instrução + * Normativa RFB nº 1.009/2010 (Tabelas I to III) for IPI, PIS and COFINS. * - * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/1994/aj_003_94 Ajuste - * SINIEF 03/1994, which instituted the ICMS CST as the two digit code AB. - * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2000/aj_006_00 Ajuste - * SINIEF 06/2000, which gives the current Tabela B (the tributação pelo ICMS digits). - * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2012/aj_020_12 Ajuste - * SINIEF 20/2012, which gives Tabela A (origem da mercadoria, 0 to 7). - * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2013/aj_015_13 Ajuste - * SINIEF 15/2013, which added origem 8 to Tabela A. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 + * Convênio SINIEF s/nº 1970, whose Anexo I carries the CST tables in force. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23 + * Ajuste SINIEF 39/23, which gave Tabela B its current wording with effect from 01.12.23. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24 + * Ajuste SINIEF 20/24, which revoked items 12, 13, 52, 72 and 74 of Tabela B with effect from + * 09.07.24. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/1994/aj_003_94 + * Ajuste SINIEF 03/1994, which instituted the ICMS CST as the two digit code AB. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2000/AJ_006_00 + * Ajuste SINIEF 06/2000, the historical Tabela B superseded by Ajuste SINIEF 39/23. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2012/aj_020_12 + * Ajuste SINIEF 20/2012, which gives Tabela A (origem da mercadoria, 0 to 7). + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2013/aj_015_13 + * Ajuste SINIEF 15/2013, which added origem 8 to Tabela A. * @see Official: https://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=15974 * Instrução Normativa RFB nº 1.009/2010, Tabelas I to III (CST-IPI, CST-PIS and CST-COFINS). */ export const ICMS_CST_CODES = [ "00", + "02", "10", + "15", "20", "30", "40", "41", "50", "51", + "53", "60", + "61", "70", "90", ] as const; @@ -80,3 +92,10 @@ export const PIS_COFINS_CST_CODES = [ "98", "99", ] as const; + +/** + * Shape a CST code has to be written in: the 2 digits of the IPI, PIS and COFINS tables or + * the 3 digits of the ICMS form (origin digit + Tabela B code), optionally split by a + * single whitespace or mask character, the way documents print the origin apart ("0 10"). + */ +export const CST_FORMAT_REGEX = /^\d[\s.\-/]?\d[\s.\-/]?\d?$/; diff --git a/src/is-valid-ncm/constants.ts b/src/is-valid-ncm/constants.ts index 59bc6398..08c88c25 100644 --- a/src/is-valid-ncm/constants.ts +++ b/src/is-valid-ncm/constants.ts @@ -10526,3 +10526,9 @@ export const NCM_CODES: readonly string[] = [ "97061000", "97069000", ]; + +/** + * Shape a complete NCM code has to be written in: the 8 digits, optionally split into the + * printed `NNNN.NN.NN` groups by a single whitespace or mask character. + */ +export const NCM_FORMAT_REGEX = /^\d{4}[\s.\-/]?\d{2}[\s.\-/]?\d{2}$/; From 7cdd6c8c721d00a32dd012b37c8d4ccebb4996fb Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:44 -0300 Subject: [PATCH 14/14] fix(tables): validate the input of every code lookup the same way --- src/_internals/constants/cei.ts | 2 +- src/_internals/constants/cns.ts | 9 +- .../fetch-sorted-record.ts | 2 +- src/get-cbo/get-cbo.test.ts | 27 +++++- src/get-cbo/get-cbo.ts | 10 +- src/get-cfop/get-cfop.test.ts | 93 +++++++++++++++---- src/get-cfop/get-cfop.ts | 36 +++++-- src/get-cnae/get-cnae.ts | 2 +- src/is-valid-cbo/is-valid-cbo.test.ts | 14 +++ src/is-valid-cbo/is-valid-cbo.ts | 8 +- src/is-valid-cfop/is-valid-cfop.test.ts | 35 ++++++- src/is-valid-cfop/is-valid-cfop.ts | 39 ++++++-- src/is-valid-cnae/is-valid-cnae.test.ts | 5 + src/is-valid-cnae/is-valid-cnae.ts | 2 +- src/is-valid-csosn/is-valid-csosn.test.ts | 15 +++ src/is-valid-csosn/is-valid-csosn.ts | 31 +++++-- src/is-valid-cst/is-valid-cst.test.ts | 36 ++++++- src/is-valid-cst/is-valid-cst.ts | 50 +++++++--- src/is-valid-ncm/is-valid-ncm.test.ts | 15 +++ src/is-valid-ncm/is-valid-ncm.ts | 21 ++++- 20 files changed, 369 insertions(+), 83 deletions(-) diff --git a/src/_internals/constants/cei.ts b/src/_internals/constants/cei.ts index 20b90d95..d11fbce1 100644 --- a/src/_internals/constants/cei.ts +++ b/src/_internals/constants/cei.ts @@ -23,6 +23,6 @@ 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}$/; +export const CEI_FORMAT_REGEX = /^\d{2}[\s.\-/]?\d{3}[\s.\-/]?\d{5}[\s.\-/]?\d{2}$/; export const CEI_PATTERN = "00.000.00000/00"; diff --git a/src/_internals/constants/cns.ts b/src/_internals/constants/cns.ts index 3d238f5a..6d9b9e79 100644 --- a/src/_internals/constants/cns.ts +++ b/src/_internals/constants/cns.ts @@ -2,15 +2,20 @@ * 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/ + * ANVISA's two validation routines, the ones implemented here. The page sits behind a bot filter + * and answers HTTP 403 to every non-browser client, so it has to be opened in a browser. * @see Based on: https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html - * e-SUS APS documentation of the same DATASUS algorithm, reachable without a browser. + * e-SUS APS documentation of the same DATASUS algorithm, reachable without a browser. It applies + * the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows the + * ANVISA page, which restricts it to 7, 8 and 9, so a 5 prefixed number is rejected even when its + * weighted sum checks out. */ /** * Shape a CNS number has to be written in: the 15 digits, optionally split into the printed * groups of 3, 4, 4 and 4 by whitespace or the usual mask characters. */ -export const CNS_FORMAT_REGEX = /^\d{3}[\s.\-/]*\d{4}[\s.\-/]*\d{4}[\s.\-/]*\d{4}$/; +export const CNS_FORMAT_REGEX = /^\d{3}[\s.\-/]?\d{4}[\s.\-/]?\d{4}[\s.\-/]?\d{4}$/; /** 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; diff --git a/src/_internals/fetch-sorted-record/fetch-sorted-record.ts b/src/_internals/fetch-sorted-record/fetch-sorted-record.ts index 0d638a1f..9cbeda34 100644 --- a/src/_internals/fetch-sorted-record/fetch-sorted-record.ts +++ b/src/_internals/fetch-sorted-record/fetch-sorted-record.ts @@ -1,4 +1,4 @@ -import { fetchWithRetry } from "../fetch-with-retry/fetch-with-retry"; +import { fetchWithRetry } from "../fetch-with-retry/fetch-with-retry.ts"; /** * Fetches a dataset with retry, fails when the response is not ok, and returns the parsed diff --git a/src/get-cbo/get-cbo.test.ts b/src/get-cbo/get-cbo.test.ts index 3512e909..d0c2b070 100644 --- a/src/get-cbo/get-cbo.test.ts +++ b/src/get-cbo/get-cbo.test.ts @@ -29,11 +29,34 @@ describe("getCbo", () => { }); }); - it("should pad a number to six digits so codes starting with zero resolve (0102-05, Oficial da Aeronáutica)", () => { - expect(getCbo(10_205)).toEqual({ code: "010205", title: "Oficial da Aeronáutica" }); + it("should pad a number to six digits so codes starting with zero resolve (0102-05, Oficial da aeronáutica)", () => { + expect(getCbo(10_205)).toEqual({ code: "010205", title: "Oficial da aeronáutica" }); expect(getCbo("10205")).toBeNull(); }); + it("should resolve a code the official CSV carries and the community mirror did not (142135)", () => { + expect(getCbo("142135")).toEqual({ + code: "142135", + title: "Oficial de proteção de dados pessoais (dpo)", + }); + }); + + it("should return null for a code the official CSV no longer carries (223150)", () => { + expect(getCbo("223150")).toBeNull(); + }); + + it("should reject a group boundary written with more than one separator (2124--05)", () => { + expect(getCbo("2124--05")).toBeNull(); + expect(getCbo("2124-05")).toEqual({ + code: "212405", + title: "Analista de desenvolvimento de sistemas", + }); + expect(getCbo("2124 05")).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"); diff --git a/src/get-cbo/get-cbo.ts b/src/get-cbo/get-cbo.ts index 617c4edb..9e7cfade 100644 --- a/src/get-cbo/get-cbo.ts +++ b/src/get-cbo/get-cbo.ts @@ -19,7 +19,7 @@ export type Cbo = { * table. * * A string is only read as a code when it is written in one of the documented forms: the 6 - * digits, or the `NNNN-NN` mask, with the usual separators between the groups and optional + * digits, or the `NNNN-NN` mask, with a single separator between the groups and optional * surrounding whitespace. Anything else (`"2124abc05"`) is rejected instead of having its * digits picked out. A number is only read as a code when it is a non-negative safe integer, * since a sign, a decimal point or a rounded magnitude would otherwise be read as a code the @@ -32,15 +32,17 @@ export type Cbo = { * @example * ```typescript * getCbo("2124-05"); // { code: "212405", title: "Analista de desenvolvimento de sistemas" } - * getCbo(10205); // { code: "010205", title: "Oficial da Aeronáutica" } (a number is padded to 6 digits) + * getCbo(10205); // { code: "010205", title: "Oficial da aeronáutica" } (a number is padded to 6 digits) * getCbo("999999"); // null * getCbo("2124abc05"); // null (not a documented form) * getCbo(-212405); // null (not a non-negative safe integer) * ``` * - * @see Official: http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf + * @see Official: https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv + * The CBO 2002 occupation table, as published by the Ministério do Trabalho e Emprego. * @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`. + * Community mirror of the same table, the fallback `CBO_TITLES` was built from before the + * official CSV was used. */ export const getCbo = (value: string | number): Cbo | null => { if (!isLookupCode(value)) return null; diff --git a/src/get-cfop/get-cfop.test.ts b/src/get-cfop/get-cfop.test.ts index 12fe4859..c7ae83e2 100644 --- a/src/get-cfop/get-cfop.test.ts +++ b/src/get-cfop/get-cfop.test.ts @@ -11,43 +11,89 @@ 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", + description: + "Venda de mercadoria adquirida ou recebida de terceiros, ou qualquer venda de mercadoria efetuada pelo MEI com exceção das saídas classificadas nos códigos 5.501, 5.502, 5.504 e 5.505", }); }); 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", + description: + "Venda de mercadoria adquirida ou recebida de terceiros, ou qualquer venda de mercadoria efetuada pelo MEI com exceção das saídas classificadas nos códigos 5.501, 5.502, 5.504 e 5.505", }); }); 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", + description: + "Venda de mercadoria adquirida ou recebida de terceiros, ou qualquer venda de mercadoria efetuada pelo MEI com exceção das saídas classificadas nos códigos 5.501, 5.502, 5.504 e 5.505", + }); + }); + + it("should accept the whitespace around a code, one of the documented forms", () => { + expect(getCfop(" 5102 ")?.code).toBe("5102"); + expect(getCfop("\t5.102\n")?.code).toBe("5102"); + }); + + it("should return one entry per code even when the annex glues the body into the code line (1255 and 1256)", () => { + expect(getCfop("1255")).toEqual({ + code: "1255", + description: + "Compra de energia elétrica por estabelecimento prestador de serviço de comunicação", + }); + expect(getCfop("1256")).toEqual({ + code: "1256", + description: "Compra de energia elétrica por estabelecimento de produtor rural", }); }); - it("should return the entries the mirror glues into the previous row (1306, 1414 and 6913)", () => { - expect(getCfop("1306")).toEqual({ - code: "1306", - description: "Aquisição de serviço de comunicação por estabelecimento de produtor rural", + it("should resolve the codes the 2022 and 2024 rewrites of the annex added (7504, 6360, 2128 and 1934)", () => { + expect(getCfop("7504")).toEqual({ + code: "7504", + description: "Exportação de mercadoria que foi objeto de formação de lote de exportação", }); - expect(getCfop("1414")).toEqual({ - code: "1414", + expect(getCfop("6360")).toEqual({ + code: "6360", description: - "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", + "Prestação de serviço de transporte a contribuinte substituto em relação ao serviço de transporte", }); - expect(getCfop("6913")).toEqual({ - code: "6913", - description: "Retorno de mercadoria ou bem recebido para demonstração", + expect(getCfop("2128")).toEqual({ + code: "2128", + description: "Compra para utilização na prestação de serviço sujeita ao ISSQN", + }); + expect(getCfop("1934")).toEqual({ + code: "1934", + description: + "Entrada simbólica de mercadoria recebida para depósito em depósito fechado ou armazém geral", }); }); - it("should not carry the next entry inside a description (1305 ends before 1.306)", () => { - expect(getCfop("1305")?.description).toBe( - "Aquisição de serviço de comunicação por estabelecimento de geradora ou de distribuidora de energia elétrica", + it("should carry the wording in force for the repurposed Sistema de Integração e Parceria Rural codes (1451 and 1452)", () => { + expect(getCfop("1451")?.description).toBe( + "Entrada de animal - Sistema de Integração e Parceria Rural", + ); + expect(getCfop("1452")?.description).toBe( + "Entrada de insumo - Sistema de Integração e Parceria Rural", + ); + }); + + it("should keep the annex punctuation intact (7211 quotes drawback, 1126 and 1128 are capitalised)", () => { + expect(getCfop("7211")?.description).toBe( + "Devolução de compras para industrialização sob o regime de “drawback”", + ); + expect(getCfop("1126")?.description).toBe( + "Compra para utilização na prestação de serviço sujeita ao ICMS", + ); + expect(getCfop("1128")?.description).toBe( + "Compra para utilização na prestação de serviço sujeita ao ISSQN", + ); + }); + + it("should keep only the wording in force for a re-worded code (7667, Ajuste SINIEF 39/25)", () => { + expect(getCfop("7667")?.description).toBe( + "Saída de combustíveis ou lubrificantes a consumidor ou usuário final", ); }); @@ -100,6 +146,21 @@ describe("getCfop", () => { expect(getCfop()).toBeNull(); }); + it("should return null for a string that is not a documented form", () => { + expect(getCfop("abc5102")).toBeNull(); + expect(getCfop("5..102")).toBeNull(); + }); + + it("should return null for a number that is not a non-negative safe integer", () => { + expect(getCfop(-5102)).toBeNull(); + expect(getCfop(51.02)).toBeNull(); + expect(getCfop(2 ** 53)).toBeNull(); + }); + + it("should return null for a null-prototype object", () => { + expect(getCfop(Object.create(null))).toBeNull(); + }); + describe("properties", () => { const codeArbitrary = fc.constantFrom(...Object.keys(CFOP_TABLE)); diff --git a/src/get-cfop/get-cfop.ts b/src/get-cfop/get-cfop.ts index ad0d7170..b7ddca6c 100644 --- a/src/get-cfop/get-cfop.ts +++ b/src/get-cfop/get-cfop.ts @@ -1,5 +1,5 @@ -import { CFOP_TABLE } from "../_internals/constants/cfop"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { CFOP_FORMAT_REGEX, CFOP_TABLE } from "../_internals/constants/cfop"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; /** @@ -15,30 +15,50 @@ export type Cfop = { /** * Looks a CFOP (Código Fiscal de Operações e Prestações) code up in the official table. * + * The table is the consolidated Anexo II of Convênio SINIEF s/nº 1970, the text in force + * (current wording given by Ajuste SINIEF 03/24, last amended by Ajuste SINIEF 39/25). + * * Only operable codes are in the table: the group and subgroup headings of the official * nomenclature, the codes ending in "00" and "50" (1000, 1100, 1150, 5350, ...), are section * titles rather than codes a document can carry, so they give `null`. * - * @param {string|number} value - The CFOP code to look up. + * A string is only read as a code when it is written in one of the documented forms: the 4 + * digits, or the `N.NNN` form the annex prints, with a single separator between the groups + * and optional surrounding whitespace. Anything else (`"abc5102"`) is rejected instead of + * having its digits picked out. A number is only read as a code when it is a non-negative + * safe integer, since a sign, a decimal point or a rounded magnitude would otherwise be read + * as a code the caller never wrote. + * + * @param {string|number} value - The CFOP code to look up, with or without the `N.NNN` mask, + * e.g. `"1.101"`, `"1101"` or `1101`. * @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("1101"); // { code: "1101", description: "Compra para industrialização ou produção rural" } + * getCfop("1.101"); // { code: "1101", description: "Compra para industrialização ou produção rural" } * getCfop("0000"); // null * getCfop("5350"); // null (a subgroup heading, not an operable code) + * getCfop("abc5102"); // null (not a documented form) + * getCfop(-5102); // null (not a non-negative safe integer) * ``` * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24 + * Anexo II of Convênio SINIEF s/nº 1970, the CFOP table in force. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 + * Convênio SINIEF s/nº 1970, the consolidated text the annex belongs to. * @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`. + * Ajuste SINIEF 07/01, the historical text that gave the CFOP its 4 digit form. */ export const getCfop = (value: string | number): Cfop | null => { - if (isNullish(value)) return null; + if (!isLookupCode(value)) return null; + + const code = typeof value === "number" ? String(value) : value.trim(); - const digits = sanitizeToDigits(value); + if (!CFOP_FORMAT_REGEX.test(code)) return null; + const digits = sanitizeToDigits(code); const description = CFOP_TABLE[digits]; if (description === undefined) return null; diff --git a/src/get-cnae/get-cnae.ts b/src/get-cnae/get-cnae.ts index cd7c4d96..ac822352 100644 --- a/src/get-cnae/get-cnae.ts +++ b/src/get-cnae/get-cnae.ts @@ -20,7 +20,7 @@ export type Cnae = { * official CNAE 2.3 table. * * A string is only read as a code when it is written in one of the documented forms: the 7 - * digits, or the `NNNN-N/NN` mask, with the usual separators between the groups and optional + * digits, or the `NNNN-N/NN` mask, with a single separator (space, `.`, `-` or `/`) between the groups and optional * surrounding whitespace. Anything else (`"0111abc301"`) is rejected instead of having its * digits picked out. A number is only read as a code when it is a non-negative safe integer, * since a sign, a decimal point or a rounded magnitude would otherwise be read as a code the diff --git a/src/is-valid-cbo/is-valid-cbo.test.ts b/src/is-valid-cbo/is-valid-cbo.test.ts index a155360c..1fb0f7f3 100644 --- a/src/is-valid-cbo/is-valid-cbo.test.ts +++ b/src/is-valid-cbo/is-valid-cbo.test.ts @@ -29,6 +29,20 @@ describe("isValidCbo", () => { expect(isValidCbo(" 212405 ")).toBe(true); }); + it("should accept a code the official CSV carries and the community mirror did not (142135)", () => { + expect(isValidCbo("142135")).toBe(true); + }); + + it("should reject a code the official CSV no longer carries (223150)", () => { + expect(isValidCbo("223150")).toBe(false); + }); + + it("should reject a group boundary written with more than one separator (2124--05)", () => { + expect(isValidCbo("2124--05")).toBe(false); + expect(isValidCbo("2124-05")).toBe(true); + expect(isValidCbo("2124 05")).toBe(true); + }); + it("should return false for an unknown six digit code", () => { expect(isValidCbo("000000")).toBe(false); }); diff --git a/src/is-valid-cbo/is-valid-cbo.ts b/src/is-valid-cbo/is-valid-cbo.ts index db634cf1..676b56b3 100644 --- a/src/is-valid-cbo/is-valid-cbo.ts +++ b/src/is-valid-cbo/is-valid-cbo.ts @@ -5,7 +5,7 @@ import { getCbo } from "../get-cbo/get-cbo"; * CBO 2002 table. * * A string is only read as a code when it is written in one of the documented forms: the 6 - * digits, or the `NNNN-NN` mask, with the usual separators between the groups and optional + * digits, or the `NNNN-NN` mask, with a single separator between the groups and optional * surrounding whitespace. A number is only read as a code when it is a non-negative safe * integer. * @@ -24,8 +24,10 @@ import { getCbo } from "../get-cbo/get-cbo"; * isValidCbo(-212405); // false (not a non-negative safe integer) * ``` * - * @see Official: http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf + * @see Official: https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv + * The CBO 2002 occupation table, as published by the Ministério do Trabalho e Emprego. * @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`. + * Community mirror of the same table, the fallback `CBO_TITLES` was built from before the + * official CSV was used. */ export const isValidCbo = (value: string | number): boolean => getCbo(value) !== null; diff --git a/src/is-valid-cfop/is-valid-cfop.test.ts b/src/is-valid-cfop/is-valid-cfop.test.ts index 9bf8228a..9c296478 100644 --- a/src/is-valid-cfop/is-valid-cfop.test.ts +++ b/src/is-valid-cfop/is-valid-cfop.test.ts @@ -23,14 +23,24 @@ describe("isValidCfop", () => { expect(isValidCfop(" 5102 ")).toBe(true); }); - it("should validate the sale of goods acquired from third parties (CFOP 5102, Ajuste SINIEF 07/2001)", () => { + it("should validate the sale of goods acquired from third parties (CFOP 5102)", () => { expect(isValidCfop("5102")).toBe(true); }); - it("should accept the codes the mirror glues into the previous row (1306, 1414 and 6913)", () => { - expect(isValidCfop("1306")).toBe(true); - expect(isValidCfop("1414")).toBe(true); - expect(isValidCfop("6913")).toBe(true); + it("should accept the codes the 2022 and 2024 rewrites of the annex added (7504, 6360, 2128 and 1934)", () => { + expect(isValidCfop("7504")).toBe(true); + expect(isValidCfop("6360")).toBe(true); + expect(isValidCfop("2128")).toBe(true); + expect(isValidCfop("1934")).toBe(true); + }); + + it("should accept the ato cooperativo and Sistema de Integração e Parceria Rural series (1131 and 1453)", () => { + expect(isValidCfop("1131")).toBe(true); + expect(isValidCfop("1453")).toBe(true); + }); + + it("should accept a code whose body the annex glues into the code line (1255)", () => { + expect(isValidCfop("1255")).toBe(true); }); it("should return false for an unknown 4 digit code", () => { @@ -75,6 +85,21 @@ describe("isValidCfop", () => { expect(isValidCfop("abcd")).toBe(false); }); + it("should return false for a string that is not a documented form", () => { + expect(isValidCfop("abc5102")).toBe(false); + expect(isValidCfop("5..102")).toBe(false); + }); + + it("should return false for a number that is not a non-negative safe integer", () => { + expect(isValidCfop(-5102)).toBe(false); + expect(isValidCfop(51.02)).toBe(false); + expect(isValidCfop(2 ** 53)).toBe(false); + }); + + it("should return false for a null-prototype object", () => { + expect(isValidCfop(Object.create(null))).toBe(false); + }); + describe("properties", () => { const codeArbitrary = fc.constantFrom(...Object.keys(CFOP_TABLE)); diff --git a/src/is-valid-cfop/is-valid-cfop.ts b/src/is-valid-cfop/is-valid-cfop.ts index c1155962..d04a0f4a 100644 --- a/src/is-valid-cfop/is-valid-cfop.ts +++ b/src/is-valid-cfop/is-valid-cfop.ts @@ -1,34 +1,53 @@ -import { CFOP_TABLE } from "../_internals/constants/cfop"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { CFOP_FORMAT_REGEX, CFOP_TABLE } from "../_internals/constants/cfop"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; 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. * + * The table is the consolidated Anexo II of Convênio SINIEF s/nº 1970, the text in force + * (current wording given by Ajuste SINIEF 03/24, last amended by Ajuste SINIEF 39/25). + * * Only operable codes count: the group and subgroup headings of the official nomenclature, * the codes ending in "00" and "50" (1000, 1100, 1150, 5350, ...), are section titles rather * than codes a document can carry, so they are rejected. * - * @param {string|number} value - The CFOP code to be validated. + * A string is only read as a code when it is written in one of the documented forms: the 4 + * digits, or the `N.NNN` form the annex prints, with a single separator between the groups + * and optional surrounding whitespace. Anything else (`"abc5102"`) is rejected instead of + * having its digits picked out. A number is only read as a code when it is a non-negative + * safe integer, since a sign, a decimal point or a rounded magnitude would otherwise be read + * as a code the caller never wrote. + * + * @param {string|number} value - The CFOP code to be validated, with or without the `N.NNN` + * mask, e.g. `"1.101"`, `"1101"` or `1101`. * @returns {boolean} True when the code is a known 4 digit CFOP code, false otherwise. * * @example * ```typescript - * isValidCfop("5102"); // true - * isValidCfop(5102); // true + * isValidCfop("1101"); // true + * isValidCfop("1.101"); // true + * isValidCfop(1101); // true * isValidCfop("0000"); // false * isValidCfop("1150"); // false (a subgroup heading, not an operable code) + * isValidCfop("abc5102"); // false (not a documented form) + * isValidCfop(-5102); // false (not a non-negative safe integer) * ``` * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24 + * Anexo II of Convênio SINIEF s/nº 1970, the CFOP table in force. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 + * Convênio SINIEF s/nº 1970, the consolidated text the annex belongs to. * @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`. + * Ajuste SINIEF 07/01, the historical text that gave the CFOP its 4 digit form. */ export const isValidCfop = (value: string | number): boolean => { - if (isNullish(value)) return false; + if (!isLookupCode(value)) return false; + + const code = typeof value === "number" ? String(value) : value.trim(); - const digits = sanitizeToDigits(value); + if (!CFOP_FORMAT_REGEX.test(code)) return false; - return digits in CFOP_TABLE; + return sanitizeToDigits(code) in CFOP_TABLE; }; diff --git a/src/is-valid-cnae/is-valid-cnae.test.ts b/src/is-valid-cnae/is-valid-cnae.test.ts index 41b7b305..4ca593ea 100644 --- a/src/is-valid-cnae/is-valid-cnae.test.ts +++ b/src/is-valid-cnae/is-valid-cnae.test.ts @@ -29,6 +29,11 @@ describe("isValidCnae", () => { expect(isValidCnae(" 6201501 ")).toBe(true); }); + it("should reject a group boundary written with more than one separator (6201--5//01)", () => { + expect(isValidCnae("6201--5//01")).toBe(false); + expect(isValidCnae("6201-5/01")).toBe(true); + }); + it("should return false for an unknown seven digit code", () => { expect(isValidCnae("0000000")).toBe(false); }); diff --git a/src/is-valid-cnae/is-valid-cnae.ts b/src/is-valid-cnae/is-valid-cnae.ts index e6c6b566..2257c1f6 100644 --- a/src/is-valid-cnae/is-valid-cnae.ts +++ b/src/is-valid-cnae/is-valid-cnae.ts @@ -5,7 +5,7 @@ import { getCnae } from "../get-cnae/get-cnae"; * exists in the official CNAE 2.3 table. * * A string is only read as a code when it is written in one of the documented forms: the 7 - * digits, or the `NNNN-N/NN` mask, with the usual separators between the groups and optional + * digits, or the `NNNN-N/NN` mask, with a single separator (space, `.`, `-` or `/`) between the groups and optional * surrounding whitespace. A number is only read as a code when it is a non-negative safe * integer. * diff --git a/src/is-valid-csosn/is-valid-csosn.test.ts b/src/is-valid-csosn/is-valid-csosn.test.ts index 5a620d23..c699ebb2 100644 --- a/src/is-valid-csosn/is-valid-csosn.test.ts +++ b/src/is-valid-csosn/is-valid-csosn.test.ts @@ -48,6 +48,21 @@ describe("isValidCsosn", () => { expect(isValidCsosn("abc")).toBe(false); }); + it("should return false for a string that is not a documented form", () => { + expect(isValidCsosn("abc101")).toBe(false); + expect(isValidCsosn("1--01")).toBe(false); + }); + + it("should return false for a number that is not a non-negative safe integer", () => { + expect(isValidCsosn(-101)).toBe(false); + expect(isValidCsosn(10.1)).toBe(false); + expect(isValidCsosn(2 ** 53)).toBe(false); + }); + + it("should return false for a null-prototype object", () => { + expect(isValidCsosn(Object.create(null))).toBe(false); + }); + describe("properties", () => { const codeArbitrary = fc.constantFrom(...CSOSN_CODES); diff --git a/src/is-valid-csosn/is-valid-csosn.ts b/src/is-valid-csosn/is-valid-csosn.ts index 01a7c4b0..a983076d 100644 --- a/src/is-valid-csosn/is-valid-csosn.ts +++ b/src/is-valid-csosn/is-valid-csosn.ts @@ -1,29 +1,42 @@ -import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -import { CSOSN_CODES } from "./constants"; +import { CSOSN_CODES, CSOSN_FORMAT_REGEX } 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`. + * Accepted codes are `101, 102, 103, 201, 202, 203, 300, 400, 500, 900`, the table the + * consolidated Anexo III-A of Convênio SINIEF s/nº 1970 carries. * - * @param {string|number} value - The CSOSN code to be validated. + * A string is only read as a code when it is written in one of the documented forms: the 3 + * digits, with a single separator between them and optional surrounding whitespace. Anything + * else (`"abc101"`) is rejected instead of having its digits picked out. A number is only read + * as a code when it is a non-negative safe integer, since a sign, a decimal point or a rounded + * magnitude would otherwise be read as a code the caller never wrote. + * + * @param {string|number} value - The CSOSN code to be validated, e.g. `"101"` or `101`. * @returns {boolean} True when the code is a known CSOSN code, false otherwise. * - * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10 Ajuste - * SINIEF 03/2010, which instituted the CSOSN table. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 + * Convênio SINIEF s/nº 1970, whose Anexo III-A carries the CSOSN table in force. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10 + * Ajuste SINIEF 03/2010, which instituted the CSOSN table. * * @example * ```typescript * isValidCsosn("101"); // true * isValidCsosn(900); // true * isValidCsosn("999"); // false + * isValidCsosn("abc101"); // false (not a documented form) + * isValidCsosn(-101); // false (not a non-negative safe integer) * ``` */ export const isValidCsosn = (value: string | number): boolean => { - if (isNullish(value)) return false; + if (!isLookupCode(value)) return false; + + const code = typeof value === "number" ? String(value) : value.trim(); - const digits = sanitizeToDigits(value); + if (!CSOSN_FORMAT_REGEX.test(code)) return false; - return (CSOSN_CODES as readonly string[]).includes(digits); + return (CSOSN_CODES as readonly string[]).includes(sanitizeToDigits(code)); }; diff --git a/src/is-valid-cst/is-valid-cst.test.ts b/src/is-valid-cst/is-valid-cst.test.ts index d6620c4e..7a861ffd 100644 --- a/src/is-valid-cst/is-valid-cst.test.ts +++ b/src/is-valid-cst/is-valid-cst.test.ts @@ -26,6 +26,21 @@ describe("isValidCst", () => { expect(isValidCst("199", { tax: "icms" })).toBe(false); }); + it("should return true for the monofasia de combustíveis codes Ajuste SINIEF 39/23 added", () => { + expect(isValidCst("002", { tax: "icms" })).toBe(true); + expect(isValidCst("015", { tax: "icms" })).toBe(true); + expect(isValidCst("053", { tax: "icms" })).toBe(true); + expect(isValidCst("061", { tax: "icms" })).toBe(true); + }); + + it("should return false for the codes Ajuste SINIEF 20/24 revoked (12, 13, 52, 72 and 74)", () => { + expect(isValidCst("012", { tax: "icms" })).toBe(false); + expect(isValidCst("013", { tax: "icms" })).toBe(false); + expect(isValidCst("052", { tax: "icms" })).toBe(false); + expect(isValidCst("072", { tax: "icms" })).toBe(false); + expect(isValidCst("074", { 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); @@ -129,8 +144,27 @@ describe("isValidCst", () => { expect(isValidCst(undefined, { tax: "icms" })).toBe(false); }); - it("should sanitize whitespace and mask characters", () => { + it("should accept a single separator between the digits and surrounding whitespace", () => { expect(isValidCst(" 1-10 ", { tax: "icms" })).toBe(true); + expect(isValidCst("0 10", { tax: "icms" })).toBe(true); + }); + + it("should return false when more than one separator sits between two digits", () => { + expect(isValidCst("1--10", { tax: "icms" })).toBe(false); + }); + + it("should return false for a string that is not a documented form", () => { + expect(isValidCst("abc110", { tax: "icms" })).toBe(false); + }); + + it("should return false for a number that is not a non-negative safe integer", () => { + expect(isValidCst(-110, { tax: "icms" })).toBe(false); + expect(isValidCst(1.1, { tax: "icms" })).toBe(false); + expect(isValidCst(2 ** 53, { tax: "icms" })).toBe(false); + }); + + it("should return false for a null-prototype object", () => { + expect(isValidCst(Object.create(null), { tax: "icms" })).toBe(false); }); describe("properties", () => { diff --git a/src/is-valid-cst/is-valid-cst.ts b/src/is-valid-cst/is-valid-cst.ts index 91194432..411c1c22 100644 --- a/src/is-valid-cst/is-valid-cst.ts +++ b/src/is-valid-cst/is-valid-cst.ts @@ -1,6 +1,6 @@ -import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -import { ICMS_CST_CODES, IPI_CST_CODES, PIS_COFINS_CST_CODES } from "./constants"; +import { CST_FORMAT_REGEX, ICMS_CST_CODES, IPI_CST_CODES, PIS_COFINS_CST_CODES } from "./constants"; /** * Options for `isValidCst`. @@ -32,7 +32,9 @@ const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): * 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`). + * followed by 1 of the 15 codes `00, 02, 10, 15, 20, 30, 40, 41, 50, 51, 53, 60, 61, 70, 90` + * of the Tabela B in force, the one Ajuste SINIEF 39/23 gave and Ajuste SINIEF 20/24 amended; + * `02`, `15`, `53` and `61` are the monofasia de combustíveis codes it added). * * `ipi` accepts 1 of the 14 codes `00, 01, 02, 03, 04, 05, 49, 50, 51, 52, 53, 54, 55, 99`. * @@ -42,26 +44,40 @@ const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): * `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. + * A string is only read as a code when it is written in one of the documented forms: the 2 or + * 3 digits, with a single separator between them and optional surrounding whitespace. + * Anything else (`"abc110"`) is rejected instead of having its digits picked out. A number is + * only read as a code when it is a non-negative safe integer, since a sign, a decimal point or + * a rounded magnitude would otherwise be read as a code the caller never wrote. + * + * @param {string|number} value - The CST code to be validated, e.g. `"110"`, `"0 10"` or `110`. * @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/1994/aj_003_94 Ajuste - * SINIEF 03/1994, which instituted the ICMS CST as the two digit code AB. - * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2000/aj_006_00 Ajuste - * SINIEF 06/2000, which gives the current Tabela B (the tributação pelo ICMS digits). - * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2012/aj_020_12 Ajuste - * SINIEF 20/2012, which gives Tabela A (origem da mercadoria, 0 to 7). - * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2013/aj_015_13 Ajuste - * SINIEF 15/2013, which added origem 8 to Tabela A. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 + * Convênio SINIEF s/nº 1970, whose Anexo I carries the CST tables in force. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23 + * Ajuste SINIEF 39/23, which gave Tabela B its current wording with effect from 01.12.23. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24 + * Ajuste SINIEF 20/24, which revoked items 12, 13, 52, 72 and 74 of Tabela B with effect from + * 09.07.24. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/1994/aj_003_94 + * Ajuste SINIEF 03/1994, which instituted the ICMS CST as the two digit code AB. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2000/AJ_006_00 + * Ajuste SINIEF 06/2000, the historical Tabela B superseded by Ajuste SINIEF 39/23. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2012/aj_020_12 + * Ajuste SINIEF 20/2012, which gives Tabela A (origem da mercadoria, 0 to 7). + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2013/aj_015_13 + * Ajuste SINIEF 15/2013, which added origem 8 to Tabela A. * @see Official: https://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=15974 * Instrução Normativa RFB nº 1.009/2010, Tabelas I to III (CST-IPI, CST-PIS and CST-COFINS). * * @example * ```typescript * isValidCst("110", { tax: "icms" }); // true + * isValidCst("002", { tax: "icms" }); // true (monofasia de combustíveis) * isValidCst("00", { tax: "ipi" }); // true * isValidCst("49", { tax: "pis" }); // true * isValidCst("07", { tax: "cofins" }); // true @@ -69,13 +85,19 @@ const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): * isValidCst("110"); // true (found in the icms table) * isValidCst("49"); // true (found in the ipi table) * isValidCst("999"); // false (not in any table) + * isValidCst("abc110"); // false (not a documented form) + * isValidCst(-110); // false (not a non-negative safe integer) * ``` */ export const isValidCst = (value: string | number, options?: IsValidCstOptions): boolean => { - if (isNullish(value)) return false; + if (!isLookupCode(value)) return false; if (options !== undefined && (options === null || typeof options !== "object")) return false; - const digits = sanitizeToDigits(value); + const code = typeof value === "number" ? String(value) : value.trim(); + + if (!CST_FORMAT_REGEX.test(code)) return false; + + const digits = sanitizeToDigits(code); const tax = options?.tax; if (tax !== undefined) return isValidForTax(digits, tax); diff --git a/src/is-valid-ncm/is-valid-ncm.test.ts b/src/is-valid-ncm/is-valid-ncm.test.ts index 89960d4f..e443f845 100644 --- a/src/is-valid-ncm/is-valid-ncm.test.ts +++ b/src/is-valid-ncm/is-valid-ncm.test.ts @@ -60,6 +60,21 @@ describe("isValidNcm", () => { expect(isValidNcm("abcdefgh")).toBe(false); }); + it("should return false for a string that is not a documented form", () => { + expect(isValidNcm("abc01012100")).toBe(false); + expect(isValidNcm("2203..00.00")).toBe(false); + }); + + it("should return false for a number that is not a non-negative safe integer", () => { + expect(isValidNcm(-22_030_000)).toBe(false); + expect(isValidNcm(2_203_000.01)).toBe(false); + expect(isValidNcm(2 ** 53)).toBe(false); + }); + + it("should return false for a null-prototype object", () => { + expect(isValidNcm(Object.create(null))).toBe(false); + }); + describe("properties", () => { const codeArbitrary = fc.constantFrom(...NCM_CODES); const nonZeroLeadingCodeArbitrary = fc.constantFrom( diff --git a/src/is-valid-ncm/is-valid-ncm.ts b/src/is-valid-ncm/is-valid-ncm.ts index c82b4ea7..9a8859f7 100644 --- a/src/is-valid-ncm/is-valid-ncm.ts +++ b/src/is-valid-ncm/is-valid-ncm.ts @@ -1,6 +1,6 @@ -import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -import { NCM_CODES } from "./constants"; +import { NCM_CODES, NCM_FORMAT_REGEX } from "./constants"; let cache: Set | undefined; @@ -12,6 +12,13 @@ const getCache = (): Set => { /** * Validates if a NCM (Nomenclatura Comum do Mercosul) code exists in the official table. * + * A string is only read as a code when it is written in one of the documented forms: the 8 + * digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional + * surrounding whitespace. Anything else (`"abc01012100"`) is rejected instead of having its + * digits picked out. A number is only read as a code when it is a non-negative safe integer, + * since a sign, a decimal point or a rounded magnitude would otherwise be read as a code the + * caller never wrote. + * * 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. @@ -25,14 +32,18 @@ const getCache = (): Set => { * isValidNcm("0101.21.00"); // true * isValidNcm("01012100"); // true * isValidNcm("00000000"); // false + * isValidNcm("abc01012100"); // false (not a documented form) + * isValidNcm(-84713012); // false (not a non-negative safe integer) * ``` * * @see Official: https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json */ export const isValidNcm = (value: string | number): boolean => { - if (isNullish(value)) return false; + if (!isLookupCode(value)) return false; + + const code = typeof value === "number" ? String(value) : value.trim(); - const digits = sanitizeToDigits(value); + if (!NCM_FORMAT_REGEX.test(code)) return false; - return getCache().has(digits); + return getCache().has(sanitizeToDigits(code)); };