-
Notifications
You must be signed in to change notification settings - Fork 129
[2.4.0 stack 6/18] New utils (2/2): CEI/CNO/CAEPF, registro profissional, credit card, IBAN, VIN, CBO/CNAE/NCM/CFOP/CST tables, business days, legal nature #511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fda26b0
263c6ff
5c74b88
31d3cb4
9d6c762
a59ffbd
2defadd
80f50d1
853202b
536eec5
2b78c29
1abbd8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { writeFile } from "node:fs/promises"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; | ||
|
|
||
| const scriptsDir = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| type CboEntry = { | ||
| cbo: string; | ||
| descricao: string; | ||
| }; | ||
|
|
||
| const main = async () => { | ||
| const response = await fetchWithRetry( | ||
| "https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json", | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`CBO mirror request failed with status ${response.status}`); | ||
| } | ||
|
|
||
| const json: CboEntry[] = await response.json(); | ||
|
|
||
| const data: Record<string, string> = {}; | ||
|
|
||
| for (const entry of json) { | ||
| const code = /^\d{5}$/.test(entry.cbo) ? `0${entry.cbo}` : entry.cbo; | ||
|
|
||
| if (!/^\d{6}$/.test(code)) continue; | ||
|
|
||
| data[code] = entry.descricao; | ||
| } | ||
|
|
||
| const sorted: Record<string, string> = {}; | ||
| for (const code of Object.keys(data).sort()) { | ||
| sorted[code] = data[code]; | ||
| } | ||
|
|
||
| await writeFile( | ||
| resolve(scriptsDir, "..", "./src/_internals/constants/cbo.ts"), | ||
| `/** | ||
| * CBO 2002 (Classificação Brasileira de Ocupações) titles, indexed by the raw 6 digit code. | ||
| * | ||
| * The MTE download at mtecbo.gov.br requires a browser session and cannot be fetched | ||
| * programmatically, so this table is generated from a public community mirror of the | ||
| * official table. Codes that are not purely numeric with 6 digits in the source (a small | ||
| * number of law enforcement and military ranks and a few sub-occupation codes suffixed | ||
| * with a letter) are normalized by left padding a 5 digit numeric code with a zero, or | ||
| * dropped when a letter is present, since \`Cbo.code\` only accepts 6 digits. | ||
| * | ||
| * Generated by \`node ./scripts/cbo.ts\`. Do not edit by hand. | ||
| * | ||
| * @see https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json | ||
| * @see http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf | ||
| */ | ||
| export const CBO_TITLES: Record<string, string> = ${JSON.stringify(sorted)}; | ||
| `, | ||
| ); | ||
| }; | ||
|
|
||
| await main().catch((error) => { | ||
| console.error(error instanceof Error ? error.message : error); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||
| #!/usr/bin/env node | ||||||
|
|
||||||
| import { writeFile } from "node:fs/promises"; | ||||||
| import { dirname, resolve } from "node:path"; | ||||||
| import { fileURLToPath } from "node:url"; | ||||||
|
|
||||||
| import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; | ||||||
|
|
||||||
| const scriptsDir = dirname(fileURLToPath(import.meta.url)); | ||||||
|
|
||||||
| const EMBEDDED_ENTRY_REGEX = /\s+(\d)\.(\d{3})\s+-\s+/g; | ||||||
|
|
||||||
| /** | ||||||
| * 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. | ||||||
| */ | ||||||
| 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).replace(/\s+/g, " ").trim(), | ||||||
| ]); | ||||||
| currentCode = `${match[1]}${match[2]}`; | ||||||
| lastIndex = match.index + match[0].length; | ||||||
| } | ||||||
|
|
||||||
| entries.push([currentCode, description.slice(lastIndex).replace(/\s+/g, " ").trim()]); | ||||||
|
|
||||||
| return entries; | ||||||
| }; | ||||||
|
|
||||||
| const main = async () => { | ||||||
| const response = await fetchWithRetry( | ||||||
| "https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv", | ||||||
| ); | ||||||
|
|
||||||
| if (!response.ok) { | ||||||
| throw new Error(`CFOP mirror request failed with status ${response.status}`); | ||||||
| } | ||||||
|
|
||||||
| const csv = await response.text(); | ||||||
|
|
||||||
| const data: Record<string, string> = {}; | ||||||
|
|
||||||
| for (const line of csv.split("\n")) { | ||||||
| const match = line.match(/^(\d{4});"(.*)"\s*$/); | ||||||
|
|
||||||
| if (!match) continue; | ||||||
|
|
||||||
| const [, code, description] = match; | ||||||
|
|
||||||
| for (const [entryCode, entryDescription] of splitEmbeddedEntries(code, description)) { | ||||||
| if (entryCode.endsWith("00")) continue; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Exclude CFOP category headers ending in The generator retains all 23 current codes ending in Exclude codes ending in Proposed fix- if (entryCode.endsWith("00")) continue;
+ if (entryCode.endsWith("00") || entryCode.endsWith("50")) continue;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed at the tip (#520, 27d6664): the official CONFAZ text prints every x.x50 entry as a heading (upper case, no 'Classificam-se neste código' body), exactly like the x.x00 headings the generator already skipped, so the 23 headings were excluded and the table regenerated (564 to 541 entries). Tests for 1150/5350 (invalid) and 1151/5351 (valid). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline scripts/cfop.ts --items all
printf '\n--- generator filter and generation output ---\n'
sed -n '45,90p' scripts/cfop.ts
printf '\n--- generated heading-code presence ---\n'
rg -n -C2 '"(1150|5350|1151|5351)"\s*:' src/_internals/constants/cfop.ts || true
printf '\n--- regression tests ---\n'
rg -n -C3 '(1150|5350|1151|5351)' src/get-cfop src/is-valid-cfop
printf '\n--- current commit ---\n'
git rev-parse HEAD
git status --shortLength of output: 3624 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- repository state ---'
git rev-parse HEAD
git status --short
git branch --show-current
printf '%s\n' '--- claimed commit availability ---'
git cat-file -t c112ca3 2>&1 || true
git show --no-patch --format='%H%n%s%n%D' c112ca3 2>&1 || true
printf '%s\n' '--- PR `#520` reference ---'
gh pr view 520 --repo brazilian-utils/javascript --json number,state,headRefName,headRefOid,url 2>&1 || true
printf '%s\n' '--- current filter and relevant tests ---'
rg -n -C2 'endsWith\("00"\)|endsWith\("50"\)' scripts/cfop.ts || true
rg -n -C3 '(1150|5350|1151|5351)' src/get-cfop src/is-valid-cfop 2>&1 || trueLength of output: 1542
PR You are interacting with an AI system. |
||||||
|
|
||||||
| data[entryCode] = entryDescription; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| const sorted: Record<string, string> = {}; | ||||||
| for (const code of Object.keys(data).sort()) { | ||||||
| sorted[code] = data[code]; | ||||||
| } | ||||||
|
|
||||||
| await writeFile( | ||||||
| resolve(scriptsDir, "..", "./src/_internals/constants/cfop.ts"), | ||||||
| `/** | ||||||
| * CFOP (Código Fiscal de Operações e Prestações) table, indexed by the 4 digit code. | ||||||
| * | ||||||
| * Group and subgroup headers (codes ending in "00", e.g. "1000", "1100") are section | ||||||
| * titles from the official nomenclature rather than operable codes, so they are excluded. | ||||||
| * | ||||||
| * Generated by \`node ./scripts/cfop.ts\`. Do not edit by hand. | ||||||
| * | ||||||
| * @see https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv | ||||||
| * @see https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 | ||||||
| */ | ||||||
| export const CFOP_TABLE: Record<string, string> = ${JSON.stringify(sorted)}; | ||||||
| `, | ||||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| await main().catch((error) => { | ||||||
| console.error(error instanceof Error ? error.message : error); | ||||||
| process.exit(1); | ||||||
| }); | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { writeFile } from "node:fs/promises"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; | ||
|
|
||
| const scriptsDir = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| type CnaeSubclass = { | ||
| id: string; | ||
| descricao: string; | ||
| }; | ||
|
|
||
| const main = async () => { | ||
| const response = await fetchWithRetry("https://servicodados.ibge.gov.br/api/v2/cnae/subclasses"); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`IBGE CNAE request failed with status ${response.status}`); | ||
| } | ||
|
|
||
| const json: CnaeSubclass[] = await response.json(); | ||
|
|
||
| const entries = json | ||
| .filter((subclass) => /^\d{7}$/.test(subclass.id)) | ||
| .sort((subclassA, subclassB) => (subclassA.id > subclassB.id ? 1 : -1)) | ||
| .map((subclass) => [subclass.id, subclass.descricao] as const); | ||
|
|
||
| const data: Record<string, string> = {}; | ||
| for (const [id, descricao] of entries) { | ||
| data[id] = descricao; | ||
| } | ||
|
|
||
| await writeFile( | ||
| resolve(scriptsDir, "..", "./src/_internals/constants/cnae.ts"), | ||
| `/** | ||
| * CNAE 2.3 (Classificação Nacional de Atividades Econômicas) subclasses, indexed by the | ||
| * raw 7 digit code, mapping to the official subclass description. | ||
| * | ||
| * Generated by \`node ./scripts/cnae.ts\`. Do not edit by hand. | ||
| * | ||
| * @see https://servicodados.ibge.gov.br/api/v2/cnae/subclasses | ||
| * @see https://concla.ibge.gov.br/classificacoes/por-tema/atividades-economicas/classificacao-nacional-de-atividades-economicas | ||
| */ | ||
| export const CNAE_SUBCLASSES: Record<string, string> = ${JSON.stringify(data)}; | ||
| `, | ||
| ); | ||
| }; | ||
|
|
||
| await main().catch((error) => { | ||
| console.error(error instanceof Error ? error.message : error); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { writeFile } from "node:fs/promises"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; | ||
|
|
||
| const scriptsDir = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| type NcmEntry = { | ||
| Codigo: string; | ||
| Data_Fim: string; | ||
| }; | ||
|
|
||
| type NcmResponse = { | ||
| Nomenclaturas: NcmEntry[]; | ||
| }; | ||
|
|
||
| const main = async () => { | ||
| const response = await fetchWithRetry( | ||
| "https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json?perfil=PUBLICO", | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Siscomex NCM request failed with status ${response.status}`); | ||
| } | ||
|
|
||
| const json: NcmResponse = await response.json(); | ||
|
|
||
| const codes = json.Nomenclaturas.filter( | ||
| (entry) => entry.Data_Fim === "31/12/9999" && /^[\d.]{10}$/.test(entry.Codigo), | ||
| ) | ||
| .map((entry) => entry.Codigo.replace(/\D/g, "")) | ||
| .filter((code) => code.length === 8); | ||
|
|
||
| const uniqueSortedCodes = Array.from(new Set(codes)).sort(); | ||
|
|
||
| await writeFile( | ||
| resolve(scriptsDir, "..", "./src/is-valid-ncm/constants.ts"), | ||
| `/** | ||
| * Currently valid NCM (Nomenclatura Comum do Mercosul) 8 digit codes, sorted ascending. | ||
| * | ||
| * Generated by \`node ./scripts/ncm.ts\`. Do not edit by hand. | ||
| * | ||
| * @see https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json | ||
| */ | ||
| export const NCM_CODES: readonly string[] = ${JSON.stringify(uniqueSortedCodes)}; | ||
| `, | ||
| ); | ||
| }; | ||
|
|
||
| await main().catch((error) => { | ||
| console.error(error instanceof Error ? error.message : error); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { describe, expect, test } from "../test/runtime"; | ||
| import { calculateCeiCheckDigit } from "./calculate-cei-check-digit"; | ||
|
|
||
| describe("calculateCeiCheckDigit", () => { | ||
| test("should return 5 for the base of 11.583.00249/85 (yiibr/yii2-br-validator CeiValidatorTest)", () => { | ||
| expect(calculateCeiCheckDigit("11583002498")).toBe(5); | ||
| }); | ||
|
|
||
| test("should return 7 for the base of 27.729.71181/87 (yiibr/yii2-br-validator CeiValidatorTest)", () => { | ||
| expect(calculateCeiCheckDigit("27729711818")).toBe(7); | ||
| }); | ||
|
|
||
| test("should return 6 for the base of 24.985.96743/86 (marcos-cruz/Documento CeiTest)", () => { | ||
| expect(calculateCeiCheckDigit("24985967438")).toBe(6); | ||
| }); | ||
|
|
||
| test("should return 0 when the folded sum ends in 0 (CNO 401800097960 of the Receita Federal CNO dataset)", () => { | ||
| expect(calculateCeiCheckDigit("40180009796")).toBe(0); | ||
| }); | ||
|
|
||
| test("should return 0 for a base of only zeros", () => { | ||
| expect(calculateCeiCheckDigit("00000000000")).toBe(0); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { CEI_WEIGHTS } from "../constants/cei"; | ||
| import { generateChecksum } from "../generate-checksum/generate-checksum"; | ||
|
|
||
| /** | ||
| * Calculates the check digit of a CEI (Cadastro Específico do INSS) base, the same digit the | ||
| * CNO (Cadastro Nacional de Obras) kept when it replaced the CEI numbering. | ||
| * | ||
| * The 11 base digits are weighted by 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4 from left to right. | ||
| * The tens part and the units part of that sum are added together and the check digit is the | ||
| * complement of the units digit of the result to 10, with 10 mapped back to 0. | ||
| * | ||
| * @param {string} base - The 11 digits that precede the check digit. | ||
| * @returns {number} The check digit, 0 to 9. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * calculateCeiCheckDigit("11583002498"); // 5 | ||
| * calculateCeiCheckDigit("40180009796"); // 0 | ||
| * ``` | ||
| * | ||
| * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno | ||
| * @see Official: Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: the | ||
| * 38432 works registered in Minas Gerais confirm the rule, and their check digits of 0 are | ||
| * what shows that a computed 10 maps back to 0, which neither reference implementation does. | ||
| * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php | ||
| * PHP reference implementation of the CEI check digit. | ||
| * @see Based on: https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs | ||
| * Second, independent reference implementation agreeing with the first. | ||
| */ | ||
| export const calculateCeiCheckDigit = (base: string): number => { | ||
| const sum = generateChecksum({ base, weight: CEI_WEIGHTS }); | ||
| const folded = Math.floor(sum / 10) + (sum % 10); | ||
|
|
||
| return (10 - (folded % 10)) % 10; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.