From 8a81b95da92bb38a4a87663caebc1f0c4a8a36dd Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:02:30 -0300 Subject: [PATCH 1/6] feat(ibs-cbs): add the CST-IBS/CBS and cClassTrib validators and lookups The electronic fiscal documents of the tax reform (Lei Complementar 214/2025) carry two new code sets in the IBSCBS group, the CST-IBS/CBS and the cClassTrib, and a system that emits or reads them from 2026 on has to validate both and show what they mean, as isValidCfop/getCfop do for the CFOP. isValidCstIbsCbs and getCstIbsCbs cover the 18 codes of the CST table; isValidClassTrib and getClassTrib the 161 classifications in force, each with its CST, its short name and its description. They are functions of their own rather than a tax of isValidCst: IBS and CBS share one table, the 3 digit codes collide with the ICMS form, and isValidCst without a tax accepts a code of any table, so a new table there would change what that default accepts. Every classification belongs to the CST its first 3 digits spell (Informe Tecnico 2025.002), and the NF-e rejects a mismatched pair (rejection 1024), so isValidClassTrib takes the CST of the document as options.cst. The validator bundles the code list only (2.6 KB); the names and descriptions come with getClassTrib (50.8 KB, 9.6 KB gzipped). scripts/ibs-cbs.ts builds both tables from the workbook the Portal Nacional da NF-e publishes, always the newest one listed, and records the table and the Informe Tecnico version in the generated header; it reads the xlsx with node:zlib, so no dependency is added. Rows whose validity ended are left out, which is how v.1.60 excluded 220001, 220002 and 220003. Part of #541 --- CONTRIBUTING.md | 6 +- context7.json | 2 +- docs/getting-started.md | 1 + docs/pt-br/getting-started.md | 1 + docs/pt-br/utilities.md | 85 ++ docs/utilities.md | 85 ++ jsr.json | 4 + reports/api/brazilian-utils.api.md | 31 + scripts/data.ts | 2 + scripts/ibs-cbs.ts | 403 ++++++++ src/_internals/constants/ibs-cbs.ts | 876 ++++++++++++++++++ src/get-class-trib/get-class-trib.test.ts | 148 +++ src/get-class-trib/get-class-trib.ts | 89 ++ src/get-cst-ibs-cbs/get-cst-ibs-cbs.test.ts | 107 +++ src/get-cst-ibs-cbs/get-cst-ibs-cbs.ts | 72 ++ src/index.test.ts | 10 + src/index.ts | 7 + .../is-valid-class-trib.test.ts | 179 ++++ .../is-valid-class-trib.ts | 91 ++ .../is-valid-cst-ibs-cbs.test.ts | 130 +++ .../is-valid-cst-ibs-cbs.ts | 59 ++ 21 files changed, 2384 insertions(+), 4 deletions(-) create mode 100644 scripts/ibs-cbs.ts create mode 100644 src/_internals/constants/ibs-cbs.ts create mode 100644 src/get-class-trib/get-class-trib.test.ts create mode 100644 src/get-class-trib/get-class-trib.ts create mode 100644 src/get-cst-ibs-cbs/get-cst-ibs-cbs.test.ts create mode 100644 src/get-cst-ibs-cbs/get-cst-ibs-cbs.ts create mode 100644 src/is-valid-class-trib/is-valid-class-trib.test.ts create mode 100644 src/is-valid-class-trib/is-valid-class-trib.ts create mode 100644 src/is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs.test.ts create mode 100644 src/is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b684d44b..e4c4ab5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,9 +73,9 @@ but the few rules below hold everywhere: they call. - `src/_internals/` holds the helpers shared between utilities (check-digit arithmetic, `mod10`/ `mod11`, TLV parsing for Pix, sanitizers). `src/_internals/constants/` holds the datasets - (banks, municipalities, CNAE, CBO, CFOP, NCM, NBS, the LC 116/2003 service list, legal natures), - generated by the `scripts/` from their official sources and refreshed by the `Update datasets` - workflow through a pull request, never edited by hand. + (banks, municipalities, CNAE, CBO, CFOP, NCM, NBS, the LC 116/2003 service list, legal natures, + the IBS/CBS tables), generated by the `scripts/` from their official sources and refreshed by the + `Update datasets` workflow through a pull request, never edited by hand. - Utilities are synchronous, stateless and side-effect free: input in, value out, no globals, no environment access, no dynamic code. The two exceptions are `getAddressInfoByCep` and `getCepInfoByAddress`, the only utilities that do I/O: they query public CEP APIs (BrasilAPI, diff --git a/context7.json b/context7.json index e92877c2..4ad4d0bc 100644 --- a/context7.json +++ b/context7.json @@ -30,7 +30,7 @@ "rules": [ "The package has zero runtime dependencies and ships as ESM plus a UMD build; nothing else needs to be installed to use it.", "Import from the root: import { isValidCpf } from '@brazilian-utils/brazilian-utils'. Every util is also a kebab-case subpath, e.g. '@brazilian-utils/brazilian-utils/is-valid-cpf'.", - "Use the subpaths to lazy-load the dataset-backed utils (getMunicipalities, getMunicipalityByCode, getCnae, getCbo, getCfop, isValidNcm, getBanks, getNbs, isValidNbs, getServiceItem, isValidServiceItem): each one embeds a large official table.", + "Use the subpaths to lazy-load the dataset-backed utils (getMunicipalities, getMunicipalityByCode, getCnae, getCbo, getCfop, getClassTrib, isValidNcm, getBanks, getNbs, isValidNbs, getServiceItem, isValidServiceItem): each one embeds a large official table.", "Never import the same util from both the root and its subpath in one app: a bundler treats them as two unrelated modules and bundles the dataset twice.", "Public functions never throw on bad input (null, undefined, wrong type): isValid* return false, format* and parse* return '', single-item getters return null, list getters return [].", "The only utils that reject are the async getAddressInfoByCep (GetAddressInfoByCepError: NotFound, Validation, Service) and getCepInfoByAddress (GetCepInfoByAddressError: NotFound, Validation).", diff --git a/docs/getting-started.md b/docs/getting-started.md index 853c578b..f524c313 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -75,6 +75,7 @@ A few utils embed an official dataset and weigh far more than everything else co | `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 93.9 KB | 21.2 KB | | `isValidNbs` · `getNbs` | NBS 2.0 (Nomenclatura Brasileira de Serviços) descriptions | 81.8 KB | 13.8 KB | | `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.9 KB | 6.9 KB | +| `getClassTrib` | cClassTrib (IBS/CBS) names and descriptions | 50.8 KB | 9.6 KB | | `getBanks` · `getBankByCode` · `getBankByIspb` | Banco Central STR participants (COMPE + ISPB) | 38.3 - 38.6 KB | 9.5 - 9.7 KB | | `isValidServiceItem` · `getServiceItem` | Service list of the Lei Complementar 116/2003 | 27.2 KB | 8.9 KB | diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md index a3543b4a..1f9ea0a9 100644 --- a/docs/pt-br/getting-started.md +++ b/docs/pt-br/getting-started.md @@ -75,6 +75,7 @@ Alguns utilitários embutem uma base de dados oficial e pesam muito mais que tod | `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 93,9 KB | 21,2 KB | | `isValidNbs` · `getNbs` | descrições da NBS 2.0 (Nomenclatura Brasileira de Serviços) | 81,8 KB | 13,8 KB | | `isValidCfop` · `getCfop` | descrições das operações do CFOP | 68,9 KB | 6,9 KB | +| `getClassTrib` | nomes e descrições do cClassTrib (IBS/CBS) | 50,8 KB | 9,6 KB | | `getBanks` · `getBankByCode` · `getBankByIspb` | participantes do STR do Banco Central (COMPE + ISPB) | 38,3 - 38,6 KB | 9,5 - 9,7 KB | | `isValidServiceItem` · `getServiceItem` | lista de serviços da Lei Complementar 116/2003 | 27,2 KB | 8,9 KB | diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 79fa1af5..6dbf830e 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -3053,6 +3053,91 @@ isValidCsosn(-101); // false (não é um inteiro seguro não negativo) Fonte: [Anexo III-A consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70) e [Ajuste SINIEF 03/2010](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10). +### isValidCstIbsCbs + +Valida um CST-IBS/CBS (Código de Situação Tributária do IBS e da CBS) contra a tabela oficial, o código que o campo `CST` do grupo `IBSCBS` leva nos documentos fiscais eletrônicos da reforma tributária (Lei Complementar nº 214/2025): NF-e, NFC-e, CT-e, NFS-e e os demais. + +- Os códigos vigentes são `000`, `010`, `011`, `200`, `220`, `221`, `222`, `400`, `410`, `510`, `515`, `550`, `620`, `800`, `810`, `811`, `820` e `830`. +- É uma função própria, não um `tax` de `isValidCst`: IBS e CBS compartilham uma única tabela, seus códigos de 3 dígitos colidem com a forma origem + Tabela B do ICMS (`000`, `200`), e `isValidCst` sem `tax` aceita um código de qualquer tabela, então incluir esta mudaria o que esse padrão aceita. +- Aceita uma string de dígitos puros, com espaços opcionais nas extremidades, ou um inteiro seguro não negativo. O campo é numérico com 3 dígitos e não tem máscara, então qualquer outra string é rejeitada em vez de ter os dígitos pinçados. +- Um valor com menos de 3 dígitos é completado com zeros à esquerda, como string ou como número, já que os códigos começam com zeros que um campo numérico descarta: `0`, `'0'` e `'000'` são todos o código `000`. + +```javascript +import { isValidCstIbsCbs } from '@brazilian-utils/brazilian-utils'; + +isValidCstIbsCbs('000'); // true +isValidCstIbsCbs(410); // true +isValidCstIbsCbs(10); // true (completado para '010') +isValidCstIbsCbs('100'); // false +isValidCstIbsCbs('cst200'); // false (não é uma forma documentada) +isValidCstIbsCbs(-200); // false (não é um inteiro seguro não negativo) +``` + +### getCstIbsCbs + +Busca um CST-IBS/CBS e retorna a descrição que a tabela oficial de CST dá a ele. O resultado é um registro `CstIbsCbs`: `{ code, description }`. + +- Valem a mesma tabela e as mesmas regras de entrada de `isValidCstIbsCbs`. Retorna `null` quando o código é desconhecido ou o valor não está em uma forma documentada. + +```javascript +import { getCstIbsCbs } from '@brazilian-utils/brazilian-utils'; + +getCstIbsCbs('000'); // { code: '000', description: 'Tributação integral' } +getCstIbsCbs(410); // { code: '410', description: 'Imunidade e não incidência' } +getCstIbsCbs(10); // { code: '010', description: 'Tributação com alíquotas uniformes' } +getCstIbsCbs('100'); // null +getCstIbsCbs('cst200'); // null (não é uma forma documentada) +``` + +### isValidClassTrib + +Valida um cClassTrib (Código de Classificação Tributária do IBS e da CBS) contra a tabela oficial, o código que o campo `cClassTrib` leva ao lado do CST-IBS/CBS. + +- **Opções** (`IsValidClassTribOptions`): `cst` é o CST-IBS/CBS que o documento leva, validado também contra a classificação. Omita-o para validar só o cClassTrib. +- Toda classificação pertence a exatamente um CST-IBS/CBS, os 3 primeiros dígitos do seu código, e um documento que leva um cClassTrib com outro CST é rejeitado (rejeição 1024, "Classificação Tributária do IBS e da CBS incompatível com o CST informado"). Um `cst` informado que não seja o CST da classificação, seja ele qual for, torna o resultado `false`. +- Só contam as classificações vigentes: o Informe Técnico 2025.002 exclui uma classificação encerrando sua vigência (`dFimVig`), como a v.1.60 fez com `220001`, `220002` e `220003`, e essas são rejeitadas. São 161 vigentes na versão publicada em 23/06/2026. +- Aceita uma string de dígitos puros, com espaços opcionais nas extremidades, ou um inteiro seguro não negativo. O campo é numérico com 6 dígitos e não tem máscara, então qualquer outra string é rejeitada. +- Um valor com menos de 6 dígitos é completado com zeros à esquerda: `1`, `'1'` e `'000001'` são todos o código `000001`. `cst` é lido da mesma forma, completado para 3 dígitos. +- Só a lista de códigos entra no bundle com esta função, não as descrições que `getClassTrib` retorna. + +```javascript +import { isValidClassTrib } from '@brazilian-utils/brazilian-utils'; + +isValidClassTrib('200001'); // true +isValidClassTrib(1); // true (completado para '000001') +isValidClassTrib('200001', { cst: '200' }); // true +isValidClassTrib('200001', { cst: '000' }); // false (a classificação pertence ao CST 200) +isValidClassTrib('999999'); // false +isValidClassTrib('220001'); // false (excluído pelo Informe Técnico 2025.002 v.1.60) +isValidClassTrib('c200001'); // false (não é uma forma documentada) +``` + +### getClassTrib + +Busca um cClassTrib e retorna a sua classificação oficial. O resultado é um registro `ClassTrib`: `{ code, cst, name, description }`. + +- Valem a mesma tabela e as mesmas regras de entrada de `isValidClassTrib`. Retorna `null` quando o código é desconhecido ou o valor não está em uma forma documentada. +- `cst` é o CST-IBS/CBS a que a classificação pertence, os 3 primeiros dígitos do seu código; `name` é o nome reduzido que a tabela oficial dá para apresentação (a coluna "Nome cClassTrib") e `description` a situação a que se refere (a coluna "Descrição cClassTrib"). +- A redação legal que a planilha também traz em cada linha (o artigo da Lei Complementar nº 214/2025 e dos dois regulamentos) não é distribuída. + +```javascript +import { getClassTrib } from '@brazilian-utils/brazilian-utils'; + +getClassTrib('000002'); +// { +// code: '000002', +// cst: '000', +// name: 'Exploração de via', +// description: 'Exploração de via, observado o art. 11 da Lei Complementar nº 214, de 2025.', +// } +getClassTrib(2)?.code; // '000002' +getClassTrib('999999'); // null +getClassTrib('220001'); // null (excluído pelo Informe Técnico 2025.002 v.1.60) +getClassTrib('c200001'); // null (não é uma forma documentada) +``` + +Fonte: as abas CST e cClassTrib da planilha "Tabela de Classificação Tributária do IBS e CBS" que o [Portal Nacional da NF-e publica em "Documentos" > "Diversos"](https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=/NJarYc9nus=) (a versão publicada em 23/06/2026), divulgada pelo [Informe Técnico 2025.002](https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=hXzemuyNHW4=) (v.1.60), e a [Nota Técnica 2025.002-RTC](https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=04BIflQt1aY=), campos UB13 e UB14. + ## GTIN (código de barras de produto) ### isValidGtin diff --git a/docs/utilities.md b/docs/utilities.md index cdfc4f40..d386120d 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -3053,6 +3053,91 @@ isValidCsosn(-101); // false (not a non-negative safe integer) Source: [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70) and [Ajuste SINIEF 03/2010](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10). +### isValidCstIbsCbs + +Check if a CST-IBS/CBS (Código de Situação Tributária do IBS e da CBS) exists in the official table, the code the field `CST` of the group `IBSCBS` carries in the electronic fiscal documents of the tax reform (Lei Complementar nº 214/2025): NF-e, NFC-e, CT-e, NFS-e and the others. + +- The codes in force are `000`, `010`, `011`, `200`, `220`, `221`, `222`, `400`, `410`, `510`, `515`, `550`, `620`, `800`, `810`, `811`, `820` and `830`. +- It is a function of its own, not a `tax` of `isValidCst`: IBS and CBS share one table, its 3 digit codes collide with the ICMS origin plus Tabela B form (`000`, `200`), and `isValidCst` without a `tax` accepts a code of any table, so adding this one would change what that default accepts. +- Accepts a string of bare digits with optional surrounding whitespace, or a non-negative safe integer. The field is numeric with 3 digits and has no mask, so any other string is rejected instead of having its digits picked out. +- A value narrower than 3 digits is left padded with zeros, as a string or as a number, since the codes start with zeros a numeric field drops: `0`, `'0'` and `'000'` are all the code `000`. + +```javascript +import { isValidCstIbsCbs } from '@brazilian-utils/brazilian-utils'; + +isValidCstIbsCbs('000'); // true +isValidCstIbsCbs(410); // true +isValidCstIbsCbs(10); // true (padded to '010') +isValidCstIbsCbs('100'); // false +isValidCstIbsCbs('cst200'); // false (not a documented form) +isValidCstIbsCbs(-200); // false (not a non-negative safe integer) +``` + +### getCstIbsCbs + +Look a CST-IBS/CBS up and get the description the official CST table gives it. The result is a `CstIbsCbs` record: `{ code, description }`. + +- Same table and input rules as `isValidCstIbsCbs`. Returns `null` when the code is unknown or the value is not in a documented form. + +```javascript +import { getCstIbsCbs } from '@brazilian-utils/brazilian-utils'; + +getCstIbsCbs('000'); // { code: '000', description: 'Tributação integral' } +getCstIbsCbs(410); // { code: '410', description: 'Imunidade e não incidência' } +getCstIbsCbs(10); // { code: '010', description: 'Tributação com alíquotas uniformes' } +getCstIbsCbs('100'); // null +getCstIbsCbs('cst200'); // null (not a documented form) +``` + +### isValidClassTrib + +Check if a cClassTrib (Código de Classificação Tributária do IBS e da CBS) exists in the official table, the code the field `cClassTrib` carries next to the CST-IBS/CBS. + +- **Options** (`IsValidClassTribOptions`): `cst` is the CST-IBS/CBS the document carries, checked against the classification as well. Omit it to check the cClassTrib alone. +- Every classification belongs to exactly one CST-IBS/CBS, the first 3 digits of its code, and a document that carries a cClassTrib with another CST is rejected (rejection 1024, "Classificação Tributária do IBS e da CBS incompatível com o CST informado"). A `cst` that is given and is not the CST of the classification, whatever it is, makes the result `false`. +- Only the classifications in force count: the Informe Técnico 2025.002 excludes a classification by closing its validity (`dFimVig`), as v.1.60 did with `220001`, `220002` and `220003`, and those are rejected. 161 are in force in the version published on 23/06/2026. +- Accepts a string of bare digits with optional surrounding whitespace, or a non-negative safe integer. The field is numeric with 6 digits and has no mask, so any other string is rejected. +- A value narrower than 6 digits is left padded with zeros: `1`, `'1'` and `'000001'` are all the code `000001`. `cst` is read the same way, padded to 3 digits. +- Only the code list is bundled with this function, not the descriptions `getClassTrib` returns. + +```javascript +import { isValidClassTrib } from '@brazilian-utils/brazilian-utils'; + +isValidClassTrib('200001'); // true +isValidClassTrib(1); // true (padded to '000001') +isValidClassTrib('200001', { cst: '200' }); // true +isValidClassTrib('200001', { cst: '000' }); // false (the classification belongs to CST 200) +isValidClassTrib('999999'); // false +isValidClassTrib('220001'); // false (excluded by Informe Técnico 2025.002 v.1.60) +isValidClassTrib('c200001'); // false (not a documented form) +``` + +### getClassTrib + +Look a cClassTrib up and get its official classification. The result is a `ClassTrib` record: `{ code, cst, name, description }`. + +- Same table and input rules as `isValidClassTrib`. Returns `null` when the code is unknown or the value is not in a documented form. +- `cst` is the CST-IBS/CBS the classification belongs to, the first 3 digits of its code; `name` is the short name the official table gives for display (the column "Nome cClassTrib") and `description` the situation it refers to (the column "Descrição cClassTrib"). +- The legal wording the workbook also prints for each row (the article of Lei Complementar nº 214/2025 and of both regulations) is not shipped. + +```javascript +import { getClassTrib } from '@brazilian-utils/brazilian-utils'; + +getClassTrib('000002'); +// { +// code: '000002', +// cst: '000', +// name: 'Exploração de via', +// description: 'Exploração de via, observado o art. 11 da Lei Complementar nº 214, de 2025.', +// } +getClassTrib(2)?.code; // '000002' +getClassTrib('999999'); // null +getClassTrib('220001'); // null (excluded by Informe Técnico 2025.002 v.1.60) +getClassTrib('c200001'); // null (not a documented form) +``` + +Source: the CST and cClassTrib sheets of the "Tabela de Classificação Tributária do IBS e CBS" workbook the [Portal Nacional da NF-e publishes under "Documentos" > "Diversos"](https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=/NJarYc9nus=) (the version published on 23/06/2026), divulged by the [Informe Técnico 2025.002](https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=hXzemuyNHW4=) (v.1.60), and the [Nota Técnica 2025.002-RTC](https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=04BIflQt1aY=), fields UB13 and UB14. + ## GTIN (product barcode) ### isValidGtin diff --git a/jsr.json b/jsr.json index 8f3ca508..241ebfe0 100644 --- a/jsr.json +++ b/jsr.json @@ -62,9 +62,11 @@ "./get-certidao-info": "./src/get-certidao-info/get-certidao-info.ts", "./get-cfop": "./src/get-cfop/get-cfop.ts", "./get-cities": "./src/get-cities/get-cities.ts", + "./get-class-trib": "./src/get-class-trib/get-class-trib.ts", "./get-cnae": "./src/get-cnae/get-cnae.ts", "./get-cnpj-info": "./src/get-cnpj-info/get-cnpj-info.ts", "./get-cpf-info": "./src/get-cpf-info/get-cpf-info.ts", + "./get-cst-ibs-cbs": "./src/get-cst-ibs-cbs/get-cst-ibs-cbs.ts", "./get-format-license-plate": "./src/get-format-license-plate/get-format-license-plate.ts", "./get-gtin-info": "./src/get-gtin-info/get-gtin-info.ts", "./get-holidays": "./src/get-holidays/get-holidays.ts", @@ -100,6 +102,7 @@ "./is-valid-cep": "./src/is-valid-cep/is-valid-cep.ts", "./is-valid-certidao": "./src/is-valid-certidao/is-valid-certidao.ts", "./is-valid-cfop": "./src/is-valid-cfop/is-valid-cfop.ts", + "./is-valid-class-trib": "./src/is-valid-class-trib/is-valid-class-trib.ts", "./is-valid-cnae": "./src/is-valid-cnae/is-valid-cnae.ts", "./is-valid-cnh": "./src/is-valid-cnh/is-valid-cnh.ts", "./is-valid-cno": "./src/is-valid-cno/is-valid-cno.ts", @@ -109,6 +112,7 @@ "./is-valid-credit-card": "./src/is-valid-credit-card/is-valid-credit-card.ts", "./is-valid-csosn": "./src/is-valid-csosn/is-valid-csosn.ts", "./is-valid-cst": "./src/is-valid-cst/is-valid-cst.ts", + "./is-valid-cst-ibs-cbs": "./src/is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs.ts", "./is-valid-email": "./src/is-valid-email/is-valid-email.ts", "./is-valid-gtin": "./src/is-valid-gtin/is-valid-gtin.ts", "./is-valid-iban": "./src/is-valid-iban/is-valid-iban.ts", diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md index bc7d31eb..6f4c8a59 100644 --- a/reports/api/brazilian-utils.api.md +++ b/reports/api/brazilian-utils.api.md @@ -109,6 +109,14 @@ export type Cfop = { description: string; }; +// @public +export type ClassTrib = { + code: string; + cst: string; + name: string; + description: string; +}; + // @public export type Cnae = { code: string; @@ -154,6 +162,12 @@ export type CpfInfo = { checkDigits: string; }; +// @public +export type CstIbsCbs = { + code: string; + description: string; +}; + // @public export const differenceInBusinessDays: (laterDate: Date, earlierDate: Date, options?: BusinessDayOptions) => number | null; @@ -524,6 +538,9 @@ export const getCfop: (value: string | number) => Cfop | null; // @public @deprecated export const getCities: (state?: StateCode) => string[]; +// @public +export const getClassTrib: (value: string | number) => ClassTrib | null; + // @public export const getCnae: (value: string | number) => Cnae | null; @@ -536,6 +553,9 @@ export type GetCnpjInfoOptions = Pick; // @public export const getCpfInfo: (value: string) => CpfInfo | null; +// @public +export const getCstIbsCbs: (value: string | number) => CstIbsCbs | null; + // @public export const getFormatLicensePlate: (value: string) => LicensePlateFormat | null; @@ -757,6 +777,14 @@ export type IsValidCertidaoOptions = { // @public export const isValidCfop: (value: string | number) => boolean; +// @public +export const isValidClassTrib: (value: string | number, options?: IsValidClassTribOptions) => boolean; + +// @public +export type IsValidClassTribOptions = { + cst?: string | number; +}; + // @public export const isValidCnae: (value: string | number) => boolean; @@ -795,6 +823,9 @@ export const isValidCsosn: (value: string | number) => boolean; // @public export const isValidCst: (value: string | number, options?: IsValidCstOptions) => boolean; +// @public +export const isValidCstIbsCbs: (value: string | number) => boolean; + // @public export type IsValidCstOptions = { tax?: "icms" | "ipi" | "pis" | "cofins"; diff --git a/scripts/data.ts b/scripts/data.ts index 19fe2ef9..dc1bca83 100644 --- a/scripts/data.ts +++ b/scripts/data.ts @@ -25,6 +25,7 @@ const generators = [ "cfop.ts", "cities.ts", "cnae.ts", + "ibs-cbs.ts", "legal-natures.ts", "nbs.ts", "ncm.ts", @@ -38,6 +39,7 @@ const generatedFiles = [ "./src/_internals/constants/cfop.ts", "./src/_internals/constants/cities.ts", "./src/_internals/constants/cnae.ts", + "./src/_internals/constants/ibs-cbs.ts", "./src/_internals/constants/nbs.ts", "./src/_internals/constants/service-items.ts", "./src/_internals/constants/states.ts", diff --git a/scripts/ibs-cbs.ts b/scripts/ibs-cbs.ts new file mode 100644 index 00000000..409c76f2 --- /dev/null +++ b/scripts/ibs-cbs.ts @@ -0,0 +1,403 @@ +#!/usr/bin/env node + +import { writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { inflateRawSync } from "node:zlib"; + +import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; + +const scriptsDir = import.meta.dirname; + +const PORTAL = "https://www.nfe.fazenda.gov.br/portal/"; + +/** "Documentos" > "Diversos" of the Portal Nacional da NF-e, where every table version is listed. */ +const TABLES_LISTING = `${PORTAL}listaConteudo.aspx?tipoConteudo=/NJarYc9nus=`; + +/** "Documentos" > "Informes Técnicos", where every version of the Informe Técnico 2025.002 is listed. */ +const INFORMES_LISTING = `${PORTAL}listaConteudo.aspx?tipoConteudo=hXzemuyNHW4=`; + +/** + * The portal answers a request without this cookie with a redirect that only sets it, and a + * request that then arrives without an ASP.NET session with a redirect to the home page. + * Sending the cookie up front gets the page in one request. + */ +const PORTAL_HEADERS = { Cookie: "AspxAutoDetectCookieSupport=1" }; + +const ANCHOR_REGEX = /]*href="(exibirArquivo\.aspx\?conteudo=[^"]*)"[^>]*>([\s\S]*?)<\/a>/g; + +/** + * The listing title of a table version. The wording changed between versions ("Tabela de Código + * de Classificação Tributária do IBS/CBS", "Tabela de Classificação Tributária do IBS e CBS"), + * so only the stable part is matched. + */ +const TABLE_TITLE_REGEX = + /Tabela de (?:Código de )?Classificação Tributária do IBS.*Publicada em (\d{2})\/(\d{2})\/(\d{4})/; + +const INFORME_TITLE_REGEX = + /Informe Técnico 2025\.002\s*-?\s*v\.(\d+\.\d+).*Publicado em (\d{2})\/(\d{2})\/(\d{4})/; + +/** + * Smallest number of rows a complete table yields. The table published on 23/06/2026 carries 18 + * CST codes and 164 classifications, and new versions only add rows or close them with a + * `dFimVig`, so a result far below it means the workbook layout changed, not that codes were + * revoked. + */ +const MINIMUM_CST_CODES = 15; +const MINIMUM_CLASSIFICATIONS = 150; + +const CST_HEADER = "CST-IBS/CBS"; +const CST_DESCRIPTION_HEADER = "Descrição CST-IBS/CBS"; +const CLASS_TRIB_HEADER = "cClassTrib"; +const CLASS_TRIB_NAME_HEADER = "Nome cClassTrib"; +const CLASS_TRIB_DESCRIPTION_HEADER = "Descrição cClassTrib"; +const END_OF_VALIDITY_HEADER = "dFimVig"; + +const XML_ENTITIES: Record = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }; + +const decodeXml = (text: string): string => + text.replaceAll( + /&(?:#(\d+)|#x([\da-f]+)|(\w+));/gi, + (entity: string, ...groups: (string | undefined)[]) => { + const [decimal, hex, name = ""] = groups; + + if (decimal !== undefined) return String.fromCodePoint(Number(decimal)); + if (hex !== undefined) return String.fromCodePoint(Number.parseInt(hex, 16)); + + return XML_ENTITIES[name] ?? entity; + }, + ); + +const normalizeText = (text: string): string => text.replaceAll(/\s+/g, " ").trim(); + +type Listing = { + /** Absolute download URL. */ + url: string; + /** The title the portal lists the file under. */ + title: string; + /** Publication date as `yyyy-mm-dd`, which sorts chronologically. */ + publishedAt: string; + /** The groups the title regex captured. */ + groups: string[]; +}; + +/** + * Reads a listing page of the portal and returns the newest entry whose title matches. + * @param {string} listingUrl - The listing page. + * @param {RegExp} titleRegex - Matches the title; its last three groups are day, month and year. + * @returns {Promise} The entry with the latest publication date. + */ +const fetchLatestListing = async (listingUrl: string, titleRegex: RegExp): Promise => { + const response = await fetchWithRetry(listingUrl, { headers: PORTAL_HEADERS }); + + if (!response.ok) { + throw new Error(`Portal da NF-e listing request failed with status ${response.status}`); + } + + const html = await response.text(); + const listings: Listing[] = []; + + for (const [, href = "", body = ""] of html.matchAll(ANCHOR_REGEX)) { + const title = normalizeText(decodeXml(body.replaceAll(/<[^>]+>/g, " "))); + const match = titleRegex.exec(title); + + if (match === null) continue; + + const groups = match.slice(1); + const [day, month, year] = groups.slice(-3); + + listings.push({ + url: new URL(decodeXml(href).replaceAll(" ", "+"), PORTAL).href, + title, + publishedAt: `${year}-${month}-${day}`, + groups, + }); + } + + const [latest] = listings.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)); + + if (latest === undefined) { + throw new Error(`No entry of ${listingUrl} matches ${titleRegex.source}; the listing changed`); + } + + return latest; +}; + +const END_OF_CENTRAL_DIRECTORY = 0x06_05_4b_50; +const CENTRAL_DIRECTORY_ENTRY = 0x02_01_4b_50; +const DEFLATE = 8; + +/** + * Reads the text files of a zip archive (an xlsx workbook is one) through its central directory. + * @param {Buffer} zip - The archive. + * @returns {Map} The UTF-8 content of every entry, by path. + */ +const unzip = (zip: Buffer): Map => { + let end = zip.length - 22; + + while (end >= 0 && zip.readUInt32LE(end) !== END_OF_CENTRAL_DIRECTORY) end -= 1; + + if (end < 0) throw new Error("The downloaded table is not a zip archive (xlsx)"); + + const files = new Map(); + let offset = zip.readUInt32LE(end + 16); + + for (let index = 0; index < zip.readUInt16LE(end + 10); index += 1) { + if (zip.readUInt32LE(offset) !== CENTRAL_DIRECTORY_ENTRY) { + throw new Error("The downloaded table has a broken zip central directory"); + } + + const method = zip.readUInt16LE(offset + 10); + const compressedSize = zip.readUInt32LE(offset + 20); + const nameLength = zip.readUInt16LE(offset + 28); + const local = zip.readUInt32LE(offset + 42); + const name = zip.toString("utf8", offset + 46, offset + 46 + nameLength); + const start = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28); + const data = zip.subarray(start, start + compressedSize); + + files.set(name, (method === DEFLATE ? inflateRawSync(data) : data).toString("utf8")); + offset += 46 + nameLength + zip.readUInt16LE(offset + 30) + zip.readUInt16LE(offset + 32); + } + + return files; +}; + +type Row = Record; + +const SHARED_STRING_REGEX = /([\s\S]*?)<\/si>/g; +const TEXT_RUN_REGEX = /]*>([\s\S]*?)<\/t>/g; +const ROW_REGEX = /]*>([\s\S]*?)<\/row>/g; +const CELL_REGEX = /]*?)(?:\/>|>([\s\S]*?)<\/c>)/g; +const CELL_VALUE_REGEX = /([\s\S]*?)<\/v>/; + +/** + * Reads a worksheet into rows keyed by the text of the header row (the first one). + * @param {string} sheet - The worksheet XML. + * @param {string[]} sharedStrings - The workbook's shared strings. + * @returns {Row[]} One record per data row, empty cells left out. + */ +const readSheet = (sheet: string, sharedStrings: string[]): Row[] => { + const rows = [...sheet.matchAll(ROW_REGEX)].map(([, row = ""]) => { + const cells: Row = {}; + + for (const [, column = "", attributes = "", body = ""] of row.matchAll(CELL_REGEX)) { + const raw = CELL_VALUE_REGEX.exec(body)?.[1]; + + if (raw === undefined) continue; + + const value = attributes.includes('t="s"') ? sharedStrings[Number(raw)] : decodeXml(raw); + + cells[column] = normalizeText(value ?? ""); + } + + return cells; + }); + + const [header = {}, ...body] = rows; + + return body.map((cells) => { + const row: Row = {}; + + for (const [column, value] of Object.entries(cells)) { + const key = header[column]; + + if (key !== undefined && value !== "") row[key] = value; + } + + return row; + }); +}; + +/** + * Reads every worksheet of the workbook. + * @param {Buffer} xlsx - The workbook. + * @returns {Row[][]} The rows of each worksheet. + */ +const readWorkbook = (xlsx: Buffer): Row[][] => { + const files = unzip(xlsx); + const sharedStrings = [ + ...(files.get("xl/sharedStrings.xml") ?? "").matchAll(SHARED_STRING_REGEX), + ].map(([, item = ""]) => + decodeXml([...item.matchAll(TEXT_RUN_REGEX)].map(([, text = ""]) => text).join("")), + ); + + return [...files] + .filter(([name]) => name.startsWith("xl/worksheets/") && name.endsWith(".xml")) + .map(([, sheet]) => readSheet(sheet, sharedStrings)); +}; + +const MS_PER_DAY = 86_400_000; + +/** + * Whether a row is in force on `today`. `dFimVig` is an Excel serial date (days since + * 30/12/1899) and is inclusive, the way `scripts/ncm.ts` reads the Siscomex window. A + * classification the Informe Técnico excludes is not deleted from the table, it gets a `dFimVig` + * (220001, 220002 and 220003 in v.1.60), so this is what keeps it out. + * @param {Row} row - A classification row. + * @param {number} today - The reference date, in milliseconds at UTC midnight. + * @returns {boolean} True when the row has no end of validity, or one that has not passed. + */ +const isInForce = (row: Row, today: number): boolean => { + const serial = row[END_OF_VALIDITY_HEADER]; + + if (serial === undefined) return true; + + return today <= Date.UTC(1899, 11, 30) + Number(serial) * MS_PER_DAY; +}; + +const serializeSorted = (data: Record): string => + `{${Object.keys(data) + .sort() + .map((key) => `${JSON.stringify(key)}:${JSON.stringify(data[key])}`) + .join(",")}}`; + +type Tables = { + /** CST description by 3 digit code. */ + csts: Record; + /** `[name, description]` by 6 digit cClassTrib. */ + classifications: Record; +}; + +/** + * Reads the CST worksheet, the one without a cClassTrib column. The cClassTrib worksheet repeats + * the CST description on every row but refines it per classification ("Alíquota reduzida em + * 60%", "Alíquota zero"), so it is not where the description of a CST comes from. + * @param {Row[]} rows - The rows of every worksheet. + * @returns {Record} The description of each CST. + */ +const buildCsts = (rows: Row[]): Record => { + const csts: Record = {}; + + for (const row of rows) { + const cst = row[CST_HEADER]; + const description = row[CST_DESCRIPTION_HEADER]; + + if (cst === undefined || description === undefined || CLASS_TRIB_HEADER in row) continue; + + if (!/^\d{3}$/.test(cst)) throw new Error(`CST "${cst}" is not a 3 digit code`); + + csts[cst] = description; + } + + return csts; +}; + +/** + * Builds both tables out of the workbook. + * @param {Row[]} rows - The rows of every worksheet. + * @param {number} today - The reference date, in milliseconds at UTC midnight. + * @returns {Tables} The CST table and the classifications in force. + */ +const buildTables = (rows: Row[], today: number): Tables => { + const csts = buildCsts(rows); + const classifications: Record = {}; + + for (const row of rows) { + const code = row[CLASS_TRIB_HEADER]; + + if (code === undefined || !isInForce(row, today)) continue; + + if (!/^\d{6}$/.test(code)) throw new Error(`cClassTrib "${code}" is not a 6 digit code`); + + const cst = row[CST_HEADER]; + + if (cst === undefined || !code.startsWith(cst) || !(cst in csts)) { + throw new Error(`cClassTrib "${code}" does not start with a CST of the CST table`); + } + + const name = row[CLASS_TRIB_NAME_HEADER]; + const description = row[CLASS_TRIB_DESCRIPTION_HEADER]; + + if (name === undefined || description === undefined) { + throw new Error(`cClassTrib "${code}" has no name or no description`); + } + + classifications[code] = [name, description]; + } + + return { csts, classifications }; +}; + +const main = async (): Promise => { + const table = await fetchLatestListing(TABLES_LISTING, TABLE_TITLE_REGEX); + const informe = await fetchLatestListing(INFORMES_LISTING, INFORME_TITLE_REGEX); + const response = await fetchWithRetry(table.url, { headers: PORTAL_HEADERS }); + + if (!response.ok) { + throw new Error(`cClassTrib table request failed with status ${response.status}`); + } + + const now = new Date(); + const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); + const workbook = Buffer.from(await response.arrayBuffer()); + const { csts, classifications } = buildTables(readWorkbook(workbook).flat(), today); + const codes = Object.keys(classifications).sort(); + + if (Object.keys(csts).length < MINIMUM_CST_CODES || codes.length < MINIMUM_CLASSIFICATIONS) { + throw new Error( + `The table yielded ${Object.keys(csts).length} CST codes and ${codes.length} classifications, below the ${MINIMUM_CST_CODES} and ${MINIMUM_CLASSIFICATIONS} a complete table holds; the workbook layout probably changed`, + ); + } + + await writeFile( + resolve(scriptsDir, "..", "./src/_internals/constants/ibs-cbs.ts"), + `/** + * CST-IBS/CBS (Código de Situação Tributária do IBS e da CBS) table, indexed by the 3 digit + * code, with the description the official table gives each one. + * + * Table version: + * "${table.title}", + * the workbook of the Portal Nacional da NF-e ("Documentos" > "Diversos"), divulged by Informe + * Técnico 2025.002 v.${informe.groups[0]} (published on ${informe.publishedAt}). The generator always reads the + * newest workbook listed and records it here, so a refresh that picks a new version up shows in + * this header. + * + * Generated by \`node ./scripts/ibs-cbs.ts\`. Do not edit by hand. + * + * @see Official: ${table.url} + * ${table.title}. + * @see Official: ${informe.url} + * Informe Técnico 2025.002 v.${informe.groups[0]}, which defines the columns of both tables. + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=/NJarYc9nus= + * "Documentos" > "Diversos" of the Portal Nacional da NF-e, where every table version is listed. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm + * Lei Complementar nº 214/2025, which institutes the IBS and the CBS. + */ +export const CST_IBS_CBS_TABLE: Record = ${serializeSorted(csts)}; + +/** + * cClassTrib (Código de Classificação Tributária do IBS e da CBS) codes in force on the + * generation date, sorted ascending. A row of the workbook whose \`dFimVig\` (end of validity) + * has passed is left out: that is how the Informe Técnico excludes a classification (220001, + * 220002 and 220003 in v.1.60). The first 3 digits of a code are its CST-IBS/CBS. + * + * Kept apart from the descriptions so that validating a code does not bundle them. + */ +export const CLASS_TRIB_CODES: readonly string[] = ${JSON.stringify(codes)}; + +/** + * The \`[name, description]\` pair of every code of \`CLASS_TRIB_CODES\`: the columns "Nome + * cClassTrib" (the short name the table gives for display) and "Descrição cClassTrib" (the + * situation the classification refers to). The legal wording columns ("LC Redação", + * "Regulamento CBS", "Regulamento IBS") are not shipped. + */ +export const CLASS_TRIB_TABLE: Record = ${serializeSorted(classifications)}; + +/** Shape a CST-IBS/CBS has to be written in: the 3 digits of the field \`CST\` (UB13, N 3). */ +export const CST_IBS_CBS_FORMAT_REGEX = /^\\d{3}$/; + +/** Shape a cClassTrib has to be written in: the 6 digits of the field \`cClassTrib\` (UB14, N 6). */ +export const CLASS_TRIB_FORMAT_REGEX = /^\\d{6}$/; + +/** Width of a CST-IBS/CBS, which is also the prefix a cClassTrib shares with its CST. */ +export const CST_IBS_CBS_LENGTH = 3; + +/** Width of a cClassTrib. */ +export const CLASS_TRIB_LENGTH = 6; +`, + ); +}; + +await main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +}); diff --git a/src/_internals/constants/ibs-cbs.ts b/src/_internals/constants/ibs-cbs.ts new file mode 100644 index 00000000..df9b0a6f --- /dev/null +++ b/src/_internals/constants/ibs-cbs.ts @@ -0,0 +1,876 @@ +/** + * CST-IBS/CBS (Código de Situação Tributária do IBS e da CBS) table, indexed by the 3 digit + * code, with the description the official table gives each one. + * + * Table version: + * "Tabela de Classificação Tributária do IBS e CBS - Publicada em 23/06/2026", + * the workbook of the Portal Nacional da NF-e ("Documentos" > "Diversos"), divulged by Informe + * Técnico 2025.002 v.1.60 (published on 2026-06-23). The generator always reads the + * newest workbook listed and records it here, so a refresh that picks a new version up shows in + * this header. + * + * Generated by `node ./scripts/ibs-cbs.ts`. Do not edit by hand. + * + * @see Official: https://www.nfe.fazenda.gov.br/portal/exibirArquivo.aspx?conteudo=D5b4Ov84WDg= + * Tabela de Classificação Tributária do IBS e CBS - Publicada em 23/06/2026. + * @see Official: https://www.nfe.fazenda.gov.br/portal/exibirArquivo.aspx?conteudo=jxTMMQeEVM8= + * Informe Técnico 2025.002 v.1.60, which defines the columns of both tables. + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=/NJarYc9nus= + * "Documentos" > "Diversos" of the Portal Nacional da NF-e, where every table version is listed. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm + * Lei Complementar nº 214/2025, which institutes the IBS and the CBS. + */ +export const CST_IBS_CBS_TABLE: Record = { + "000": "Tributação integral", + "010": "Tributação com alíquotas uniformes", + "011": "Tributação com alíquotas uniformes reduzidas", + "200": "Alíquota reduzida", + "220": "Alíquota fixa", + "221": "Alíquota fixa proporcional", + "222": "Redução de base de cálculo", + "400": "Isenção", + "410": "Imunidade e não incidência", + "510": "Diferimento", + "515": "Diferimento com redução de alíquota", + "550": "Suspensão", + "620": "Tributação monofásica", + "800": "Transferência de crédito", + "810": "Ajuste de IBS na ZFM", + "811": "Ajustes", + "820": "Tributação em documento específico", + "830": "Exclusão de base de cálculo", +}; + +/** + * cClassTrib (Código de Classificação Tributária do IBS e da CBS) codes in force on the + * generation date, sorted ascending. A row of the workbook whose `dFimVig` (end of validity) + * has passed is left out: that is how the Informe Técnico excludes a classification (220001, + * 220002 and 220003 in v.1.60). The first 3 digits of a code are its CST-IBS/CBS. + * + * Kept apart from the descriptions so that validating a code does not bundle them. + */ +export const CLASS_TRIB_CODES: readonly string[] = [ + "000001", + "000002", + "000003", + "000004", + "000005", + "010001", + "010002", + "011001", + "011002", + "011003", + "011004", + "011005", + "200001", + "200002", + "200003", + "200004", + "200005", + "200006", + "200007", + "200008", + "200009", + "200010", + "200011", + "200012", + "200013", + "200014", + "200015", + "200016", + "200017", + "200018", + "200019", + "200020", + "200021", + "200022", + "200023", + "200024", + "200025", + "200026", + "200027", + "200028", + "200029", + "200030", + "200031", + "200032", + "200033", + "200034", + "200035", + "200036", + "200037", + "200038", + "200039", + "200040", + "200041", + "200042", + "200043", + "200044", + "200045", + "200046", + "200047", + "200048", + "200049", + "200050", + "200051", + "200052", + "200053", + "200054", + "221001", + "221002", + "221003", + "221004", + "222001", + "400001", + "400002", + "410001", + "410002", + "410003", + "410004", + "410005", + "410006", + "410007", + "410008", + "410009", + "410010", + "410011", + "410012", + "410013", + "410014", + "410015", + "410016", + "410017", + "410018", + "410019", + "410020", + "410021", + "410022", + "410023", + "410024", + "410025", + "410026", + "410027", + "410028", + "410029", + "410030", + "410031", + "410032", + "410033", + "410034", + "410035", + "410036", + "410037", + "410999", + "510001", + "515001", + "550001", + "550002", + "550003", + "550004", + "550005", + "550006", + "550007", + "550008", + "550009", + "550010", + "550011", + "550012", + "550013", + "550014", + "550015", + "550016", + "550017", + "550018", + "550019", + "550020", + "550021", + "550022", + "550023", + "550024", + "550025", + "620001", + "620002", + "620003", + "620004", + "620005", + "620006", + "620007", + "800001", + "800002", + "810001", + "811001", + "811002", + "811003", + "820001", + "820002", + "820003", + "820004", + "820005", + "820006", + "820007", + "820008", + "820009", + "830001", +]; + +/** + * The `[name, description]` pair of every code of `CLASS_TRIB_CODES`: the columns "Nome + * cClassTrib" (the short name the table gives for display) and "Descrição cClassTrib" (the + * situation the classification refers to). The legal wording columns ("LC Redação", + * "Regulamento CBS", "Regulamento IBS") are not shipped. + */ +export const CLASS_TRIB_TABLE: Record = { + "000001": [ + "Situações tributadas integralmente pelo IBS e CBS.", + "Situações tributadas integralmente pelo IBS e CBS.", + ], + "000002": [ + "Exploração de via", + "Exploração de via, observado o art. 11 da Lei Complementar nº 214, de 2025.", + ], + "000003": [ + "Regime automotivo - projetos incentivados (art. 311)", + "Regime automotivo - projetos incentivados, observado o art. 311 da Lei Complementar nº 214, de 2025.", + ], + "000004": [ + "Regime automotivo - projetos incentivados (art. 312)", + "Regime automotivo - projetos incentivados, observado o art. 312 da Lei Complementar nº 214, de 2025.", + ], + "000005": [ + "Operação com EAC destinado à mistura com gasolina A, mas com saída do biocombustível com destinação diversa", + "Operação com EAC destinado à mistura com gasolina A, mas com saída do biocombustível com destinação diversa, observado o art. 179 da Lei Complementar nº 214, de 2025.", + ], + "010001": [ + "Operações do FGTS não realizadas pela Caixa Econômica Federal", + "Operações do FGTS não realizadas pela Caixa Econômica Federal, observado o art. 212 da Lei Complementar nº 214, de 2025.", + ], + "010002": ["Operações do serviço financeiro", "Operações do serviço financeiro"], + "011001": [ + "Planos de assistência funerária.", + "Planos de assistência funerária, observado o art. 236 da Lei Complementar nº 214, de 2025.", + ], + "011002": [ + "Planos de assistência à saúde", + "Planos de assistência à saúde, observado o art. 237 da Lei Complementar nº 214, de 2025.", + ], + "011003": [ + "Intermediação de planos de assistência à saúde", + "Intermediação de planos de assistência à saúde, observado o art. 240 da Lei Complementar nº 214, de 2025.", + ], + "011004": [ + "Concursos e prognósticos", + "Concursos e prognósticos, observado o art. 246 da Lei Complementar nº 214, de 2025.", + ], + "011005": [ + "Planos de assistência à saúde de animais domésticos", + "Planos de assistência à saúde de animais domésticos, observado o art. 243 da Lei Complementar nº 214, de 2025.", + ], + "200001": [ + "Serviços de transporte de bens até as zonas de processamento de exportação e bens exportados a partir das zonas de processamento de exportação", + "Serviços de transporte de bens até as zonas de processamento de exportação e bens exportados a partir das zonas de processamento de exportação, observado o art. 103 da Lei Complementar nº 214, de 2025.", + ], + "200002": [ + "Fornecimento ou importação para produtor rural não contribuinte ou TAC", + "Fornecimento ou importação de tratores, máquinas e implementos agrícolas, destinados a produtor rural não contribuinte, e de veículos de transporte de carga destinados a transportador autônomo de carga pessoa física não contribuinte, observado o art. 110 da Lei Complementar nº 214, de 2025.", + ], + "200003": [ + "Vendas de produtos destinados à alimentação humana (Anexo I)", + "Vendas de produtos destinados à alimentação humana relacionados no Anexo I da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH, que compõem a Cesta Básica Nacional de Alimentos, criada nos termos do art. 8º da Emenda Constitucional nº 132, de 20 de dezembro de 2023, observado o art. 125 da Lei Complementar nº 214, de 2025.", + ], + "200004": [ + "Fornecimento de dispositivos médicos (Anexo XII)", + "Fornecimento de dispositivos médicos com a especificação das respectivas classificações da NCM/SH previstas no Anexo XII da Lei Complementar nº 214, de 2025, observado o art. 144 da Lei Complementar nº 214, de 2025.", + ], + "200005": [ + "Fornecimento de dispositivos médicos para órgãos da administração pública e entidades de saúde imunes (Anexo IV)", + "Fornecimento de dispositivos médicos com a especificação das respectivas classificações da NCM/SH previstas no Anexo IV da Lei Complementar nº 214, de 2025, quando adquiridos por órgãos da administração pública direta, autarquias, fundações públicas e entidades de saúde imunes, observado o art. 144 da Lei Complementar nº 214, de 2025.", + ], + "200006": [ + "Situação de emergência de saúde pública reconhecida pelo Poder público", + "Situação de emergência de saúde pública reconhecida pelo Poder Legislativo federal, estadual, distrital ou municipal competente, ato conjunto do Ministro da Fazenda e do Comitê Gestor do IBS poderá ser editado, a qualquer momento, para incluir dispositivos não listados no Anexo XII da Lei Complementar nº 214, de 2025, limitada a vigência do benefício ao período e à localidade da emergência de saúde pública, observado o art. 144 da Lei Complementar nº 214, de 2025.", + ], + "200007": [ + "Fornecimento dos dispositivos de acessibilidade próprios para pessoas com deficiência (Anexo XIII)", + "Fornecimento dos dispositivos de acessibilidade próprios para pessoas com deficiência relacionados no Anexo XIII da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH, observado o art. 145 da Lei Complementar nº 214, de 2025.", + ], + "200008": [ + "Fornecimento dos dispositivos de acessibilidade próprios para pessoas com deficiência adquiridos por órgãos da administração pública (Anexo V)", + "Fornecimento dos dispositivos de acessibilidade próprios para pessoas com deficiência relacionados no Anexo V da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH, quando adquiridos por órgãos da administração pública direta, autarquias, fundações públicas e entidades imunes, observado o art. 145 da Lei Complementar nº 214, de 2025.", + ], + "200009": [ + "Fornecimento dos medicamentos registrados na Anvisa", + "Fornecimento dos medicamentos registrados na Anvisa, observado o art. 146 da Lei Complementar nº 214, de 2025.", + ], + "200010": [ + "Fornecimento dos medicamentos registrados na Anvisa, adquiridos por órgãos da administração pública", + "Fornecimento dos medicamentos registrados na Anvisa, quando adquiridos por órgãos da administração pública direta, autarquias, fundações públicas e entidades imunes, observado o art. 146 da Lei Complementar nº 214, de 2025.", + ], + "200011": [ + "Fornecimento das composições para nutrição enteral e parenteral quando adquiridas por órgãos da administração pública (Anexo VI)", + "Fornecimento das composições para nutrição enteral e parenteral, composições especiais e fórmulas nutricionais destinadas às pessoas com erros inatos do metabolismo relacionadas no Anexo VI da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH, quando adquiridas por órgãos da administração pública direta, autarquias e fundações públicas, observado o art. 146 da Lei Complementar nº 214, de 2025.", + ], + "200012": [ + "Situação de emergência de saúde pública reconhecida pelo Poder público", + "Situação de emergência de saúde pública reconhecida pelo Poder Legislativo federal, estadual, distrital ou municipal competente, ato conjunto do Ministro da Fazenda e do Comitê Gestor do IBS poderá ser editado, a qualquer momento, limitada a vigência do benefício ao período e à localidade da emergência de saúde pública, observado o art. 146 da Lei Complementar nº 214, de 2025.", + ], + "200013": [ + "Fornecimento de tampões higiênicos, absorventes higiênicos internos ou externos", + "Fornecimento de tampões higiênicos, absorventes higiênicos internos ou externos, descartáveis ou reutilizáveis, calcinhas absorventes e coletores menstruais, observado o art. 147 da Lei Complementar nº 214, de 2025.", + ], + "200014": [ + "Fornecimento dos produtos hortícolas, frutas e ovos (Anexo XV)", + "Fornecimento dos produtos hortícolas, frutas e ovos, relacionados no Anexo XV da Lei Complementar nº 214 , de 2025, com a especificação das respectivas classificações da NCM/SH e desde que não cozidos, observado o art. 148 da Lei Complementar nº 214, de 2025.", + ], + "200015": [ + "Venda de automóveis de passageiros de fabricação nacional adquiridos por motoristas profissionais ou pessoas com deficiência", + "Venda de automóveis de passageiros de fabricação nacional de, no mínimo, 4 (quatro) portas, inclusive a de acesso ao bagageiro, quando adquiridos por motoristas profissionais que exerçam, comprovadamente, em automóvel de sua propriedade, atividade de condutor autônomo de passageiros, na condição de titular de autorização, permissão ou concessão do poder público, e que destinem o automóvel à utilização na categoria de aluguel (táxi), ou por pessoas com deficiência física, visual, auditiva, deficiência mental severa ou profunda, transtorno do espectro autista, com prejuízos na comunicação social e em padrões restritos ou repetitivos de comportamento de nível moderado ou grave, nos termos da legislação relativa à matéria, observado o disposto no art. 149 da Lei Complementar nº 214, de 2025.", + ], + "200016": [ + "Prestação de serviços de pesquisa e desenvolvimento por Instituição Científica, Tecnológica e de Inovação (ICT)", + "Prestação de serviços de pesquisa e desenvolvimento por Instituição Científica, Tecnológica e de Inovação (ICT) sem fins lucrativos para a administração pública direta, autarquias e fundações públicas ou para o contribuinte sujeito ao regime regular do IBS e da CBS, observado o disposto no art. 156 da Lei Complementar nº 214, de 2025.", + ], + "200017": [ + "Operações relacionadas ao FGTS", + "Operações relacionadas ao FGTS, considerando aquelas necessárias à aplicação da Lei nº 8.036, de 1990, realizadas pelo Conselho Curador ou Secretaria Executiva do FGTS, observado o art. 212 da Lei Complementar nº 214, de 2025.", + ], + "200018": [ + "Operações de resseguro e retrocessão", + "Operações de resseguro e retrocessão ficam sujeitas à incidência à alíquota zero, inclusive quando os prêmios de resseguro e retrocessão forem cedidos ao exterior, observado o art. 223 da Lei Complementar nº 214, de 2025.", + ], + "200019": [ + "Importador dos serviços financeiros contribuinte", + "Importador dos serviços financeiros que seja contribuinte e tenha direito de apropriação de créditos na aquisição do mesmo serviço financeiro no País, observado o art. 231 da Lei Complementar nº 214, de 2025.", + ], + "200020": [ + "Operação praticada por sociedades cooperativas optantes por regime específico do IBS e CBS", + "Operação praticada por sociedades cooperativas optantes por regime específico do IBS e CBS, quando o associado destinar bem ou serviço à cooperativa de que participa, e a cooperativa fornecer bem ou serviço ao associado sujeito ao regime regular do IBS e da CBS, observado o art. 271 da Lei Complementar nº 214, de 2025.", + ], + "200021": [ + "Serviços de transporte público coletivo de passageiros ferroviário e hidroviário", + "Serviços de transporte público coletivo de passageiros ferroviário e hidroviário urbanos, semiurbanos e metropolitanos, observado o art. 285 da Lei Complementar nº 214, de 2025.", + ], + "200022": [ + "Operação originada fora da ZFM que destine bem material industrializado a contribuinte estabelecido na ZFM", + "Operação originada fora da Zona Franca de Manaus que destine bem material industrializado de origem nacional a contribuinte estabelecido na Zona Franca de Manaus que seja habilitado nos termos do art. 442 da Lei Complementar nº 214, de 2025, e sujeito ao regime regular do IBS e da CBS ou optante pelo regime do Simples Nacional de que trata o art. 12 da Lei Complementar nº 123, de 2006, observado o art. 445 da Lei Complementar nº 214, de 2025.", + ], + "200023": [ + "Operação realizada por indústria incentivada que destine bem material intermediário para outra indústria incentivada na ZFM", + "Operação realizada por indústria incentivada que destine bem material intermediário para outra indústria incentivada na Zona Franca de Manaus, desde que a entrega ou disponibilização dos bens ocorra dentro da referida área, observado o art. 448 da Lei Complementar nº 214, de 2025.", + ], + "200024": [ + "Operação originada fora das Áreas de Livre Comércio destinadas a contribuinte estabelecido nas Áreas de Livre Comércio", + "Operação originada fora das Áreas de Livre Comércio que destine bem material industrializado de origem nacional a contribuinte estabelecido nas Áreas de Livre Comércio que seja habilitado nos termos do art. 456 da Lei Complementar nº 214, de 2025, e sujeito ao regime regular do IBS e da CBS ou optante pelo regime do Simples Nacional de que trata o art. 12 da Lei Complementar nº 123, de 2006, observado o art. 463 da Lei Complementar nº 214, de 2025.", + ], + "200025": [ + "Fornecimento dos serviços de educação relacionados ao Programa Universidade para Todos (Prouni)", + "Fornecimento dos serviços de educação relacionados ao Programa Universidade para Todos (Prouni), instituído pela Lei nº 11.096, de 13 de janeiro de 2005, observado o art. 308 da Lei Complementar nº 214, de 2025.", + ], + "200026": [ + "Locação de imóveis localizados nas zonas reabilitadas", + "Locação de imóveis localizados nas zonas reabilitadas, pelo prazo de 5 (cinco) anos, contado da data de expedição do habite-se, e relacionados a projetos de reabilitação urbana de zonas históricas e de áreas críticas de recuperação e reconversão urbanística dos Municípios ou do Distrito Federal, a serem delimitadas por lei municipal ou distrital, observado o art. 158 da Lei Complementar nº 214, de 2025.", + ], + "200027": [ + "Operações de locação, cessão onerosa e arrendamento de bens imóveis", + "Operações de locação, cessão onerosa e arrendamento de bens imóveis, observado o art. 261 da Lei Complementar nº 214, de 2025.", + ], + "200028": [ + "Fornecimento dos serviços de educação (Anexo II)", + "Fornecimento dos serviços de educação relacionados no Anexo II da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da Nomenclatura Brasileira de Serviços, Intangíveis e Outras Operações que Produzam Variações no Patrimônio (NBS), observado o art. 129 da Lei Complementar nº 214, de 2025.", + ], + "200029": [ + "Fornecimento dos serviços de saúde humana (Anexo III)", + "Fornecimento dos serviços de saúde humana relacionados no Anexo III da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NBS, observado o art. 130 da Lei Complementar nº 214, de 2025.", + ], + "200030": [ + "Venda dos dispositivos médicos (Anexo IV)", + "Venda dos dispositivos médicos relacionados no Anexo IV da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH, observado o art. 131 da Lei Complementar nº 214, de 2025.", + ], + "200031": [ + "Fornecimento dos dispositivos de acessibilidade próprios para pessoas com deficiência (Anexo V)", + "Fornecimento dos dispositivos de acessibilidade próprios para pessoas com deficiência relacionados no Anexo V da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH, observado o art. 132 da Lei Complementar nº 214, de 2025.", + ], + "200032": [ + "Fornecimento dos medicamentos registrados na Anvisa ou produzidos por farmácias de manipulação, ressalvados os medicamentos sujeitos à alíquota zero", + "Fornecimento dos medicamentos registrados na Anvisa ou produzidos por farmácias de manipulação, ressalvados os medicamentos sujeitos à alíquota zero de que trata o art. 146 da Lei Complementar nº 214, de 2025, observado o art. 133 da Lei Complementar nº 214, de 2025.", + ], + "200033": [ + "Fornecimento das composições para nutrição enteral e parenteral (Anexo VI)", + "Fornecimento das composições para nutrição enteral e parenteral, composições especiais e fórmulas nutricionais destinadas às pessoas com erros inatos do metabolismo relacionadas no Anexo VI da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH, observado o art. 133 da Lei Complementar nº 214, de 2025.", + ], + "200034": [ + "Fornecimento dos alimentos destinados ao consumo humano (Anexo VII)", + "Fornecimento dos alimentos destinados ao consumo humano relacionados no Anexo VII da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH, observado o art. 135 da Lei Complementar nº 214, de 2025.", + ], + "200035": [ + "Fornecimento dos produtos de higiene pessoal e limpeza (Anexo VIII)", + "Fornecimento dos produtos de higiene pessoal e limpeza relacionados no Anexo VIII da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH, observado o art. 136 da Lei Complementar nº 214, de 2025.", + ], + "200036": [ + "Fornecimento de produtos agropecuários, aquícolas, pesqueiros, florestais e extrativistas vegetais in natura", + "Fornecimento de produtos agropecuários, aquícolas, pesqueiros, florestais e extrativistas vegetais in natura, observado o art. 137 da Lei Complementar nº 214, de 2025.", + ], + "200037": [ + "Fornecimento de serviços ambientais de conservação ou recuperação da vegetação nativa", + "Fornecimento de serviços ambientais de conservação ou recuperação da vegetação nativa, mesmo que fornecidos sob a forma de manejo sustentável de sistemas agrícolas, agroflorestais e agrossilvopastoris, em conformidade com as definições e requisitos da legislação específica, observado o art. 137 da Lei Complementar nº 214, de 2025.", + ], + "200038": [ + "Fornecimento dos insumos agropecuários e aquícolas (Anexo IX)", + "Fornecimento dos insumos agropecuários e aquícolas relacionados no Anexo IX da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH e da NBS, observado o art. 138 da Lei Complementar nº 214, de 2025.", + ], + "200039": [ + "Fornecimento dos bens e serviços relacionados com produções nacionais artísticas, culturais, de eventos, jornalísticas e audiovisuais (Anexo X)", + "Fornecimento dos bens e serviços listados no Anexo X da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NCM/SH e NBS, nos casos relacionados com produções nacionais artísticas, culturais, de eventos, jornalísticas e audiovisuais, observado o art. 139 da Lei Complementar nº 214, de 2025.", + ], + "200040": [ + "Fornecimento de serviços de comunicação institucional à administração pública", + "Fornecimento dos seguintes serviços de comunicação institucional à administração pública direta, autarquias e fundações públicas: serviços direcionados ao planejamento, criação, programação e manutenção de páginas eletrônicas da administração pública, ao monitoramento e gestão de suas redes sociais e à otimização de páginas e canais digitais para mecanismos de buscas e produção de mensagens, infográficos, painéis interativos e conteúdo institucional, serviços de relações com a imprensa, que reúnem estratégias organizacionais para promover e reforçar a comunicação dos órgãos e das entidades contratantes com seus públicos de interesse, por meio da interação com profissionais da imprensa, e serviços de relações públicas, que compreendem o esforço de comunicação planejado, coeso e contínuo que tem por objetivo estabelecer adequada percepção da atuação e dos objetivos institucionais, a partir do estímulo à compreensão mútua e da manutenção de padrões de relacionamento e fluxos de informação entre os órgãos e as entidades contratantes e seus públicos de interesse, no País e no exterior, observado o art. 140 da Lei Complementar nº 214, de 2025.", + ], + "200041": [ + "Fornecimento de serviço de educação desportiva (art. 141. I)", + "Operações relacionadas às seguintes atividades desportivas: fornecimento de serviço de educação desportiva, classificado no código 1.2205.12.00 da NBS, observado o art. 141 da Lei Complementar nº 214, de 2025.", + ], + "200042": [ + "Fornecimento de serviço de gestão e exploração do desporto (art. 141. II)", + "Operações relacionadas às seguintes atividades desportivas: gestão e exploração do desporto por associações e clubes esportivos filiados ao órgão estadual ou federal responsável pela coordenação dos desportos, observado o art. 141 da Lei Complementar nº 214, de 2025.", + ], + "200043": [ + "Fornecimento à administração pública dos serviços e dos bens relativos à soberania (Anexo XI)", + "Fornecimento à administração pública direta, autarquias e fundações púbicas dos serviços e dos bens relativos à soberania e à segurança nacional, à segurança da informação e à segurança cibernética relacionados no Anexo XI da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NBS e da NCM/SH, observado o art. 142 da Lei Complementar nº 214, de 2025.", + ], + "200044": [ + "Operações e prestações de serviços de segurança da informação e segurança cibernética desenvolvidos por sociedade que tenha sócio brasileiro (Anexo XI)", + "Operações e prestações de serviços de segurança da informação e segurança cibernética desenvolvidos por sociedade que tenha sócio brasileiro com o mínimo de 20% (vinte por cento) do seu capital social, relacionados no Anexo XI da Lei Complementar nº 214, de 2025, com a especificação das respectivas classificações da NBS e da NCM/SH, observado o art. 142 da Lei Complementar nº 214, de 2025.", + ], + "200045": [ + "Operações relacionadas a projetos de reabilitação urbana de zonas históricas e de áreas críticas de recuperação e reconversão urbanística", + "Operações relacionadas a projetos de reabilitação urbana de zonas históricas e de áreas críticas de recuperação e reconversão urbanística dos Municípios ou do Distrito Federal, a serem delimitadas por lei municipal ou distrital, observado o art. 158 da Lei Complementar nº 214, de 2025.", + ], + "200046": [ + "Operações com bens imóveis", + "Operações com bens imóveis, observado o art. 261 da Lei Complementar nº 214, de 2025.", + ], + "200047": [ + "Bares e Restaurantes", + "Bares e Restaurantes, observado o art. 275 da Lei Complementar nº 214, de 2025.", + ], + "200048": [ + "Hotelaria, Parques de Diversão e Parques Temáticos", + "Hotelaria, Parques de Diversão e Parques Temáticos, observado o art. 281 da Lei Complementar nº 214, de 2025.", + ], + "200049": [ + "Transporte coletivo de passageiros rodoviário, ferroviário e hidroviário", + "Transporte coletivo de passageiros rodoviário, ferroviário e hidroviário intermunicipais e interestaduais, observado o art. 286 da Lei Complementar nº 214, de 2025.", + ], + "200050": [ + "Serviços de transporte aéreo regional coletivo de passageiros ou de carga", + "Serviços de transporte aéreo regional coletivo de passageiros ou de carga, observado o art. 287 da Lei Complementar nº 214, de 2025.", + ], + "200051": [ + "Agências de Turismo", + "Agências de Turismo, observado o art. 289 da Lei Complementar nº 214, de 2025.", + ], + "200052": [ + "Prestação de serviços de profissões intelectuais", + "Prestação de serviços das seguintes profissões intelectuais de natureza científica, literária ou artística, submetidas à fiscalização por conselho profissional: administradores, advogados, arquitetos e urbanistas, assistentes sociais, bibliotecários, biólogos, contabilistas, economistas, economistas domésticos, profissionais de educação física, engenheiros e agrônomos, estatísticos, médicos veterinários e zootecnistas, museólogos, químicos, profissionais de relações públicas, técnicos industriais e técnicos agrícolas, observado o art. 127 da Lei Complementar nº 214, de 2025.", + ], + "200053": [ + "Fornecimento de medicamentos registrados na Anvisa, quando classificados como soros ou vacinas", + "Fornecimento de medicamentos registrados na Anvisa, quando classificados como soros ou vacinas, observado o art. 146 da Lei Complementar nº 214, de 2025.", + ], + "200054": [ + "Fornecimento de bem material pela cooperativa de produção agropecuária a associado não sujeito ao regime regular do IBS e da CBS", + "Fornecimento de bem material pela cooperativa de produção agropecuária a associado não sujeito ao regime regular do IBS e da CBS com anulação de créditos referentes ao bem fornecido, observado o art. 271 da Lei Complementar nº 214, de 2025.", + ], + "221001": [ + "Locação, cessão onerosa ou arrendamento de bem imóvel com alíquota sobre a receita bruta", + "Locação, cessão onerosa ou arrendamento de bem imóvel com alíquota sobre a receita bruta, observado o art. 487 da Lei Complementar nº 214, de 2025.", + ], + "221002": [ + "Incorporação imobiliária submetida ao regime especial de tributação", + "Incorporação imobiliária submetida ao regime especial de tributação, observado o art. 485 da Lei Complementar nº 214, de 2025.", + ], + "221003": [ + "Incorporação imobiliária submetida ao regime especial de tributação", + "Incorporação imobiliária submetida ao regime especial de tributação, observado o art. 485 da Lei Complementar nº 214, de 2025.", + ], + "221004": [ + "Alienação de imóvel decorrente de parcelamento do solo", + "Alienação de imóvel decorrente de parcelamento do solo, observado o art. 486 da Lei Complementar nº 214, de 2025.", + ], + "222001": [ + "Transporte internacional de passageiros, caso os trechos de ida e volta sejam vendidos em conjunto", + "Transporte internacional de passageiros, caso os trechos de ida e volta sejam vendidos em conjunto, a base de cálculo será a metade do valor cobrado, observado o Art. 12 § 8º da Lei Complementar nº 214, de 2025.", + ], + "400001": [ + "Fornecimento de serviços de transporte público coletivo de passageiros rodoviário e metroviário", + "Fornecimento de serviços de transporte público coletivo de passageiros rodoviário e metroviário de caráter urbano, semiurbano e metropolitano, sob regime de autorização, permissão ou concessão pública, observado o art. 157 da Lei Complementar nº 214, de 2025.", + ], + "400002": [ + "Fornecimento de serviços de transporte público coletivo de passageiros rodoviário e metroviário com medição por quilômetro rodado", + "Fornecimento de serviços de transporte público coletivo de passageiros rodoviário e metroviário de caráter urbano, semiurbano e metropolitano, sob regime de autorização, permissão ou concessão pública, com medição por quilômetro rodado, observado o art. 157 da Lei Complementar nº 214, de 2025.", + ], + "410001": [ + "Fornecimento de bonificações quando constem no documento fiscal e que não dependam de evento posterior", + "Fornecimento de bonificações quando constem do respectivo documento fiscal e que não dependam de evento posterior, observado o art. 5º da Lei Complementar nº 214, de 2025.", + ], + "410002": [ + "Transferências entre estabelecimentos pertencentes ao mesmo contribuinte", + "Transferências entre estabelecimentos pertencentes ao mesmo contribuinte, observado o art. 6º da Lei Complementar nº 214, de 2025.", + ], + "410003": [ + "Doações sem contraprestação em benefício do doador", + "Doações que não tenham por objeto bens ou serviços que tenham permitido a apropriação de créditos pelo doador, observado o art. 6º da Lei Complementar nº 214, de 2025.", + ], + "410004": [ + "Exportações de bens e serviços", + "Exportações de bens e serviços, observado o art. 8º da Lei Complementar nº 214, de 2025.", + ], + "410005": [ + "Fornecimentos realizados pela União, pelos Estados, pelo Distrito Federal e pelos Municípios", + "Fornecimentos realizados pela União, pelos Estados, pelo Distrito Federal e pelos Municípios, observado o art. 9º da Lei Complementar nº 214, de 2025.", + ], + "410006": [ + "Fornecimentos realizados por entidades religiosas e templos de qualquer culto", + "Fornecimentos realizados por entidades religiosas e templos de qualquer culto, inclusive suas organizações assistenciais e beneficentes, observado o art. 9º da Lei Complementar nº 214, de 2025.", + ], + "410007": [ + "Fornecimentos realizados por partidos políticos, entidades sindicais e instituições de educação e de assistência social", + "Fornecimentos realizados por partidos políticos, inclusive suas fundações, entidades sindicais dos trabalhadores e instituições de educação e de assistência social, sem fins lucrativos, observado o art. 9º da Lei Complementar nº 214, de 2025.", + ], + "410008": [ + "Fornecimentos de livros, jornais, periódicos e do papel destinado a sua impressão", + "Fornecimentos de livros, jornais, periódicos e do papel destinado a sua impressão, observado o art. 9º da Lei Complementar nº 214, de 2025.", + ], + "410009": [ + "Fornecimentos de fonogramas e videofonogramas musicais produzidos no Brasil", + "Fornecimentos de fonogramas e videofonogramas musicais produzidos no Brasil contendo obras musicais ou literomusicais de autores brasileiros e/ou obras em geral interpretadas por artistas brasileiros, bem como os suportes materiais ou arquivos digitais que os contenham, salvo na etapa de replicação industrial de mídias ópticas de leitura a laser, observado o art. 9º da Lei Complementar nº 214, de 2025.", + ], + "410010": [ + "Fornecimentos de serviço de comunicação nas modalidades de radiodifusão sonora e de sons e imagens de recepção livre e gratuita", + "Fornecimentos de serviço de comunicação nas modalidades de radiodifusão sonora e de sons e imagens de recepção livre e gratuita, observado o art. 9º da Lei Complementar nº 214, de 2025.", + ], + "410011": [ + "Fornecimentos de ouro, quando definido em lei como ativo financeiro ou instrumento cambial", + "Fornecimentos de ouro, quando definido em lei como ativo financeiro ou instrumento cambial, observado o art. 9º da Lei Complementar nº 214, de 2025.", + ], + "410012": [ + "Fornecimento de condomínio edilício não optante pelo regime regular", + "Fornecimento de condomínio edilício não optante pelo regime regular, observado o art. 26 da Lei Complementar nº 214, de 2025.", + ], + "410013": [ + "Exportações de combustíveis", + "Exportações de combustíveis, observado o art. 98 da Lei Complementar nº 214, de 2025.", + ], + "410014": [ + "Fornecimento de produtor rural não contribuinte", + "Fornecimento de produtor rural não contribuinte, observado o art. 164 da Lei Complementar nº 214, de 2025.", + ], + "410015": [ + "Fornecimento por transportador autônomo não contribuinte", + "Fornecimento por transportador autônomo não contribuinte, observado o art. 169 da Lei Complementar nº 214, de 2025.", + ], + "410016": [ + "Fornecimento ou aquisição de resíduos sólidos", + "Fornecimento ou aquisição de resíduos sólidos, observado o art. 170 da Lei Complementar nº 214, de 2025.", + ], + "410017": [ + "Aquisição de bem móvel com crédito presumido sob condição de revenda realizada", + "Aquisição de bem móvel com crédito presumido sob condição de revenda realizada, observado o art. 171 da Lei Complementar nº 214, de 2025.", + ], + "410018": [ + "Operações relacionadas aos fundos garantidores e executores de políticas públicas", + "Operações relacionadas aos fundos garantidores e executores de políticas públicas, inclusive de habitação, previstos em lei, assim entendidas os serviços prestados ao fundo pelo seu agente operador e por entidade encarregada da sua administração, observado o art. 213 da Lei Complementar nº 214, de 2025.", + ], + "410019": [ + "Exclusão da gorjeta na base de cálculo no fornecimento de alimentação", + "Exclusão da gorjeta na base de cálculo no fornecimento de alimentação, observado o art. 274 da Lei Complementar nº 214, de 2025.", + ], + "410020": [ + "Exclusão do valor de intermediação na base de cálculo no fornecimento de alimentação", + "Exclusão do valor de intermediação na base de cálculo no fornecimento de alimentação, observado o art. 274 da Lei Complementar nº 214, de 2025.", + ], + "410021": [ + "Contribuição de que trata o art. 149-A da Constituição Federal", + "Contribuição de que trata o art. 149-A da Constituição Federal, observado o art. 12 da Lei Complementar nº 214, de 2025.", + ], + "410022": [ + "Consolidação da propriedade do bem pelo credor", + "Consolidação da propriedade pelo credor de bens móveis ou imóveis que tenham sido objeto de garantia, observado o art. 200 da Lei Complementar nº 214, de 2025.", + ], + "410023": [ + "Alienação de bens móveis ou imóveis que tenham sido objeto de garantia em que o prestador da garantia não seja contribuinte", + "Alienação de bens móveis ou imóveis que tenham sido objeto de garantia constituída em favor de credor em que o prestador da garantia não seja contribuinte, observado o art. 200 da Lei Complementar nº 214, de 2025.", + ], + "410024": [ + "Consolidação da propriedade do bem pelo grupo de consórcio", + "Consolidação da propriedade pelo grupo de consórcio de bem que tenha sido objeto de garantia, observado o art. 204 da Lei Complementar nº 214, de 2025.", + ], + "410025": [ + "Alienação de bem que tenha sido objeto de garantia em que o prestador da garantia não seja contribuinte", + "Alienação de bem que tenha sido objeto de garantia constituída em favor do grupo de consórcio em que o prestador da garantia não seja contribuinte, observado o art. 204 da Lei Complementar nº 214, de 2025.", + ], + "410026": [ + "Doação com anulação de crédito", + "Doações sem contraprestação em benefício do doador, com anulação de crédito apropriados pelo doador referente ao fornecimento doado, observado o art. 6º da Lei Complementar nº 214, de 2025.", + ], + "410027": [ + "Exportação de serviço ou de bem imaterial", + "Fornecimento de bens e serviços, desde que vinculados direta e exclusivamente à exportação de bens materiais ou associados à entrega no exterior de bens materiais, observado o art. 6º da Lei Complementar nº 214, de 2025.", + ], + "410028": [ + "Operações com bens imóveis realizadas por pessoas físicas não consideradas contribuintes", + "Operações com bens imóveis realizadas por pessoas físicas não consideradas contribuintes do regime regular do IBS e da CBS, observado o art. 251 da Lei Complementar nº 214, de 2025.", + ], + "410029": [ + "Operações acobertadas somente pelo ICMS", + "Operações não sujeitas à incidência de IBS e de CBS, alcançadas apenas por obrigação acessória do ICMS, observado o art. 4º da Lei Complementar nº 214, de 2025.", + ], + "410030": [ + "Estorno de crédito por perecimento, deteriorização, roubo, furto ou extravio.", + "Estorno de crédito apropriado de bens adquiridos e venham a perecer, deteriorar-se ou ser objeto de roubo, furto ou extravio, observado o art. 47 da Lei Complementar nº 214, de 2025.", + ], + "410031": [ + "Fornecimento em período anterior ao início de vigência de incidências de CBS e IBS", + "Fornecimento em período anterior ao início de vigência de incidências de CBS e IBS, observado o art. 544 da Lei Complementar nº 214, de 2025.", + ], + "410032": [ + "Tributos incidentes na operação que não integram a base de cálculo do IBS e da CBS", + "Tributos incidentes na operação que não integram a base de cálculo do IBS e da CBS, observado o art. 12 da Lei Complementar nº 214, de 2025.", + ], + "410033": [ + "Operações de Fundos de Investimento Imobiliário (FII) e Fundos de Investimento nas Cadeias Produtivas do Agronegócio (Fiagro)", + "Operações com bens imóveis, inclusive operações com direitos reais sobre bens imóveis, realizadas por Fundos de Investimento Imobiliário (FII) e Fundos de Investimento nas Cadeias Produtivas do Agronegócio (Fiagro), observado o art. 26 da Lei Complementar nº 214, de 2025.", + ], + "410034": [ + "Operações de fundos de investimento", + "Fundos de investimento cujo patrimônio seja constituído exclusivamente por aplicações em participações societárias, certificados, direitos, títulos, valores mobiliários e demais ativos financeiros permitidos pela Comissão de Valores Mobiliários, observado o art. 26 da Lei Complementar nº 214, de 2025.", + ], + "410035": [ + "Fornecimento realizado por nanoempreendedor", + "Fornecimento realizado por nanoempreendedor, observado o art. 26 da Lei Complementar nº 214, de 2025.", + ], + "410036": [ + "Descontos incondicionais", + "Descontos incondicionais, observado o art. 12 da Lei Complementar nº 214, de 2025.", + ], + "410037": [ + "Importação os bens materiais sem incidência de IBS e CBS", + "Importação os bens materiais sem incidência de IBS e CBS, observado o art. 66 da Lei Complementar nº 214, de 2025.", + ], + "410999": [ + "Operações não onerosas sem previsão de tributação, não especificadas anteriormente", + "Operações não onerosas sem previsão de tributação, não especificadas anteriormente, observado o art. 4º da Lei Complementar nº 214, de 2025.", + ], + "510001": [ + "Operações, sujeitas a diferimento, com energia elétrica, relativas à importação, geração, comercialização, distribuição e transmissão", + "Operações, sujeitas a diferimento, com energia elétrica ou com direitos a ela relacionados, relativas à importação, geração, comercialização, distribuição e transmissão, observado o art. 28 da Lei Complementar nº 214, de 2025.", + ], + "515001": [ + "Operações, sujeitas a diferimento, com insumos agropecuários e aquícolas (Anexo IX)", + "Operações, sujeitas a diferimento, com insumos agropecuários e aquícolas, observado o art. 138 da Lei Complementar nº 214, de 2025.", + ], + "550001": [ + "Exportações de bens materiais", + "Exportações de bens materiais, observado o art. 82 da Lei Complementar nº 214, de 2025.", + ], + "550002": [ + "Regime de Trânsito", + "Regime de Trânsito, observado o art. 84 da Lei Complementar nº 214, de 2025.", + ], + "550003": [ + "Regimes de Depósito (art. 85)", + "Regimes de Depósito, observado o art. 85 da Lei Complementar nº 214, de 2025.", + ], + "550004": [ + "Regimes de Depósito (art. 87)", + "Regimes de Depósito, observado o art. 87 da Lei Complementar nº 214, de 2025.", + ], + "550005": [ + "Regimes de Depósito (art. 87, Parágrafo único)", + "Regimes de Depósito, observado o art. 87 da Lei Complementar nº 214, de 2025.", + ], + "550006": [ + "Regimes de Permanência Temporária", + "Regimes de Permanência Temporária, observado o art. 88 da Lei Complementar nº 214, de 2025.", + ], + "550007": [ + "Regimes de Aperfeiçoamento", + "Regimes de Aperfeiçoamento, observado o art. 90 da Lei Complementar nº 214, de 2025.", + ], + "550008": [ + "Importação de bens para o Regime de Repetro-Temporário", + "Importação de bens para o Regime de Repetro-Temporário, de que tratam o inciso I do art. 93 da Lei Complementar nº 214, de 2025.", + ], + "550009": [ + "GNL-Temporário", + "GNL-Temporário, de que trata o inciso II do art. 93 da Lei Complementar nº 214, de 2025.", + ], + "550010": [ + "Repetro-Permanente", + "Repetro-Permanente, de que trata o inciso III do art. 93 da Lei Complementar nº 214, de 2025.", + ], + "550011": [ + "Repetro-Industrialização", + "Repetro-Industrialização, de que trata o inciso IV do art. 93 da Lei Complementar nº 214, de 2025.", + ], + "550012": [ + "Repetro-Nacional", + "Repetro-Nacional, de que trata o inciso V do art. 93 da Lei Complementar nº 214, de 2025.", + ], + "550013": [ + "Repetro-Entreposto", + "Repetro-Entreposto, de que trata o inciso VI do art. 93 da Lei Complementar nº 214, de 2025.", + ], + "550014": [ + "Zona de Processamento de Exportação", + "Zona de Processamento de Exportação, observado os arts. 99, 100 e 102 da Lei Complementar nº 214, de 2025.", + ], + "550015": [ + "Regime Tributário para Incentivo à Modernização e à Ampliação da Estrutura Portuária", + "Regime Tributário para Incentivo à Modernização e à Ampliação da Estrutura Portuária - Reporto, observado o art. 105 da Lei Complementar nº 214, de 2025.", + ], + "550016": [ + "Regime Especial de Incentivos para o Desenvolvimento da Infraestrutura", + "Regime Especial de Incentivos para o Desenvolvimento da Infraestrutura - Reidi, observado o art. 106 da Lei Complementar nº 214, de 2025.", + ], + "550017": [ + "Regime Tributário para Incentivo à Atividade Naval - Renaval (Art. 107, I)", + "Fornecimentos de embarcações registradas ou pré-registradas no Registro Especial Brasileiro - REB para incorporação ao ativo imobilizado de adquirente sujeito ao regime regular do IBS e da CBS, observado o art. 107 da Lei Complementar nº 214, de 2025.", + ], + "550018": [ + "Desoneração da aquisição de bens de capital", + "Desoneração da aquisição de bens de capital, observado o art. 109 da Lei Complementar nº 214, de 2025.", + ], + "550019": [ + "Importação de bem material por indústria incentivada para utilização na ZFM", + "Importação de bem material por indústria incentivada para utilização na Zona Franca de Manaus, observado o art. 443 da Lei Complementar nº 214, de 2025.", + ], + "550020": [ + "Áreas de livre comércio", + "Áreas de livre comércio, observado o art. 461 da Lei Complementar nº 214, de 2025.", + ], + "550021": [ + "Industrialização destinada a exportações", + "Fornecimento de produtos agropecuários in natura para contribuinte do regime regular que promova industrialização destinada a exportação, observado o art. 82 da Lei Complementar nº 214, de 2025.", + ], + "550022": [ + "Regime Especial de Incentivos para a Produção de Hidrogênio de Baixa Emissão de Carbono (Rehidro)", + "Regime Especial de Incentivos para a Produção de Hidrogênio de Baixa Emissão de Carbono (Rehidro), observado o art. 106 da Lei Complementar nº 214, de 2025.", + ], + "550023": [ + "Operações com hidrocarbonetos líquidos derivados de petróleo não combustíveis ou de gás natural, inclusive nafta", + "Operações com hidrocarbonetos líquidos derivados de petróleo não combustíveis ou de gás natural, inclusive nafta, observado o art. 172 da Lei Complementar nº 214, de 2025.", + ], + "550024": [ + "Regime Tributário para Incentivo à Atividade Naval - Renaval (Art. 107, II)", + "Importações e nas aquisições no mercado interno de máquinas, equipamentos e veículos destinados a utilização nas atividades de que trata o inciso IIIdo art. 107 efetuadas para incorporação a seu ativo imobilizado, observado o art. 107 da Lei Complementar nº 214, de 2025.", + ], + "550025": [ + "Regime Tributário para Incentivo à Atividade Naval - Renaval (Art. 107, III)", + "Importações e nas aquisições no mercado interno de matérias-primas, produtos intermediários, partes, peças e componentes para utilização na construção, conservação, modernização e reparo de embarcações pré-registradas ou registradas no REB, observado o art. 107 da Lei Complementar nº 214, de 2025.", + ], + "620001": [ + "Tributação monofásica sobre combustíveis", + "Tributação monofásica sobre combustíveis, observados os art. 172 da Lei Complementar nº 214, de 2025.", + ], + "620002": [ + "Tributação monofásica com responsabilidade pela retenção sobre combustíveis", + "Tributação monofásica com responsabilidade pela retenção sobre combustíveis, observado o art. 178 da Lei Complementar nº 214, de 2025.", + ], + "620003": [ + "Tributação monofásica com responsabilidade de retenção de tributos por terceiros", + "Tributação monofásica com responsabilidade de retenção de tributos por terceiros, observado o art. 178 da Lei Complementar nº 214, de 2025.", + ], + "620004": [ + "Tributação monofásica sobre mistura de EAC com gasolina A em percentual superior ao obrigatório", + "Tributação monofásica sobre mistura de EAC com gasolina A em percentual superior ou inferior ao obrigatório, observado o art. 179 da Lei Complementar nº 214, de 2025.", + ], + "620005": [ + "Tributação monofásica sobre mistura de EAC com gasolina A em percentual inferior ao obrigatório", + "Tributação monofásica sobre mistura de EAC com gasolina A em percentual superior ou inferior ao obrigatório, observado o art. 179 da Lei Complementar nº 214, de 2025.", + ], + "620006": [ + "Tributação monofásica sobre combustíveis cobrada anteriormente", + "Tributação monofásica sobre combustíveis cobrada anteriormente, observador o art. 180 da Lei Complementar nº 214, de 2025.", + ], + "620007": [ + "Perecimento, deteriorização, roubo, furto ou extravio no regime monofásico", + "Perecimento, deteriorização, roubo, furto ou extravio no regime monofásico sem estorno de crédito, observado o art. 47 da Lei Complementar nº 214, de 2025.", + ], + "800001": [ + "Fusão, cisão ou incorporação", + "Fusão, cisão ou incorporação, observado o art. 55 da Lei Complementar nº 214, de 2025.", + ], + "800002": [ + "Transferência de crédito do associado, inclusive as cooperativas singulares", + "Transferência de crédito do associado, inclusive as cooperativas singulares, para cooperativa de que participa das operações antecedentes às operações em que fornece bens e serviços e os créditos presumidos, observado o art. 272 da Lei Complementar nº 214, de 2025.", + ], + "810001": [ + "Crédito presumido de IBS sobre o valor apurado nos fornecimentos a partir da ZFM", + "Crédito presumido de IBS sobre o valor apurado nos fornecimentos a partir da Zona Franca de Manaus, observado o art. 450 da Lei Complementar nº 214, de 2025.", + ], + "811001": [ + "Anulação de Crédito por Saídas Imunes/Isentas", + "Anulação de crédito proporcional ao valor das operações imunes e isentas, observado o art. 51 da Lei Complementar nº 214, de 2025.", + ], + "811002": [ + "Débitos de notas fiscais não processadas na apuração", + "Débitos de notas fiscais não processadas na apuração, observado o art. 45 da Lei Complementar nº 214, de 2025.", + ], + "811003": [ + "Desenquadramento do Simples Nacional", + "Débitos apurados após o desenquadramento do regime Simples Nacional, observado o art. 41 da Lei Complementar nº 214, de 2025.", + ], + "820001": [ + "Documento com informações de fornecimento de serviços de planos de assistência à saúde elencados no art. 234 da Lei Complementar nº214, de 2025", + "Documento com informações de fornecimento de serviços de planos de assistência à saúde elencados no art. 234 da Lei Complementar nº 214, de 2025, mas com tributação realizada por outro meio", + ], + "820002": [ + "Documento com informações de fornecimento de serviços de planos de assistência funerária", + "Documento com informações de fornecimento de serviços de planos de assinstência funerária, mas com tributação realizada por outro meio, observado o art. 236 da Lei Complementar nº 214, de 2025.", + ], + "820003": [ + "Documento com informações de fornecimento de serviços de planos de assistência à saúde de animais domésticos", + "Documento com informações de fornecimento de serviços de planos de assinstência à saúde de animais domésticos, mas com tributação realizada por outro meio, observado o art. 243 da Lei Complementar nº 214, de 2025.", + ], + "820004": [ + "Documento com informações de prestação de serviços de consursos de prognósticos", + "Documento com informações de prestação de serviços de consursos de prognósticos, mas com tributação realizada por outro meio, observado o art. 248 da Lei Complementar nº 214, de 2025.", + ], + "820005": [ + "Documento com informações de alienação de bens imóveis", + "Documento com informações de alienação de bens imóveis, mas com tributação realizada por outro meio, observado o art. 254 da Lei Complementar nº 214, de 2025.", + ], + "820006": [ + "Documento com informações de fornecimento de serviços de exploração de via", + "Documento com informações de fornecimento de serviços de exploração de via, mas com tributação realizada por outro meio, observado o art. 11 da Lei Complementar nº 214, de 2025.", + ], + "820007": [ + "Documento com informações de fornecimento de serviços financeiros", + "Documento com informações de fornecimento de serviços financeiros, mas com tributação realizada por outro meio, observado o art. 181 da Lei Complementar nº 214, de 2025.", + ], + "820008": [ + "Documento com informações de fornecimento de serviço continuado, mas com tributação realizada em fatura anterior", + "Documento com informações de fornecimento de serviço continuado, mas com tributação realizada em fatura anterior, observado o art. 10 da Lei Complementar nº 214, de 2025.", + ], + "820009": [ + "Cobrança relativa a fornecimentos declarados em outro documento", + "Cobrança relativa a fornecimentos declarados em outro documento, observado o art. 60 da Lei Complementar nº 214, de 2025.", + ], + "830001": [ + "Documento com exclusão da BC da CBS e do IBS de energia elétrica fornecida pela distribuidora à UC", + "Documento com exclusão da base de cálculo da CBS e do IBS refrente à energia elétrica fornecida pela distribuidora à unidade consumidora, conforme Art 28, parágrafos 3° e 4°.", + ], +}; + +/** Shape a CST-IBS/CBS has to be written in: the 3 digits of the field `CST` (UB13, N 3). */ +export const CST_IBS_CBS_FORMAT_REGEX = /^\d{3}$/; + +/** Shape a cClassTrib has to be written in: the 6 digits of the field `cClassTrib` (UB14, N 6). */ +export const CLASS_TRIB_FORMAT_REGEX = /^\d{6}$/; + +/** Width of a CST-IBS/CBS, which is also the prefix a cClassTrib shares with its CST. */ +export const CST_IBS_CBS_LENGTH = 3; + +/** Width of a cClassTrib. */ +export const CLASS_TRIB_LENGTH = 6; diff --git a/src/get-class-trib/get-class-trib.test.ts b/src/get-class-trib/get-class-trib.test.ts new file mode 100644 index 00000000..275a8725 --- /dev/null +++ b/src/get-class-trib/get-class-trib.test.ts @@ -0,0 +1,148 @@ +import * as fc from "fast-check"; + +import { CLASS_TRIB_CODES, CLASS_TRIB_TABLE } from "../_internals/constants/ibs-cbs"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getCstIbsCbs } from "../get-cst-ibs-cbs/get-cst-ibs-cbs"; +import { isValidClassTrib } from "../is-valid-class-trib/is-valid-class-trib"; +import { getClassTrib, type ClassTrib } from "./get-class-trib"; + +describe("getClassTrib", () => { + it("should return the entry of a known code, with its CST, its name and its description", () => { + expect(getClassTrib("000002")).toEqual({ + code: "000002", + cst: "000", + name: "Exploração de via", + description: "Exploração de via, observado o art. 11 da Lei Complementar nº 214, de 2025.", + }); + expect(getClassTrib("410999")).toEqual({ + code: "410999", + cst: "410", + name: "Operações não onerosas sem previsão de tributação, não especificadas anteriormente", + description: + "Operações não onerosas sem previsão de tributação, não especificadas anteriormente, observado o art. 4º da Lei Complementar nº 214, de 2025.", + }); + }); + + it("should return the entry of a code given as a number", () => { + expect(getClassTrib(811_001)).toEqual({ + code: "811001", + cst: "811", + name: "Anulação de Crédito por Saídas Imunes/Isentas", + description: + "Anulação de crédito proporcional ao valor das operações imunes e isentas, observado o art. 51 da Lei Complementar nº 214, de 2025.", + }); + }); + + it("should pad a value with leading zeros, as a number or as a string", () => { + const expected = { + code: "000001", + cst: "000", + name: "Situações tributadas integralmente pelo IBS e CBS.", + description: "Situações tributadas integralmente pelo IBS e CBS.", + }; + + expect(getClassTrib(1)).toEqual(expected); + expect(getClassTrib("1")).toEqual(expected); + expect(getClassTrib(" 000001 ")).toEqual(expected); + expect(getClassTrib(10_002)?.cst).toBe("010"); + }); + + it("should carry a code Informe Técnico 2025.002 v.1.60 created (620007)", () => { + expect(getClassTrib("620007")).toEqual({ + code: "620007", + cst: "620", + name: "Perecimento, deteriorização, roubo, furto ou extravio no regime monofásico", + description: + "Perecimento, deteriorização, roubo, furto ou extravio no regime monofásico sem estorno de crédito, observado o art. 47 da Lei Complementar nº 214, de 2025.", + }); + }); + + it("should keep the workbook text on one line, without the line breaks of its cells", () => { + expect(getClassTrib("830001")?.name).toBe( + "Documento com exclusão da BC da CBS e do IBS de energia elétrica fornecida pela distribuidora à UC", + ); + }); + + it("should return a fresh object on every call", () => { + expect(getClassTrib("200001")).not.toBe(getClassTrib("200001")); + }); + + it("should return null for the classifications of CST 220 Informe Técnico 2025.002 v.1.60 excluded", () => { + expect(getClassTrib("220001")).toBeNull(); + expect(getClassTrib("220002")).toBeNull(); + expect(getClassTrib("220003")).toBeNull(); + }); + + it("should return null for a code the table does not carry", () => { + expect(getClassTrib("999999")).toBeNull(); + expect(getClassTrib("000000")).toBeNull(); + expect(getClassTrib("2000010")).toBeNull(); + expect(getClassTrib("200")).toBeNull(); + }); + + it("should return null for a string that is not bare digits", () => { + expect(getClassTrib("c200001")).toBeNull(); + expect(getClassTrib("200.001")).toBeNull(); + expect(getClassTrib("")).toBeNull(); + expect(getClassTrib("__proto__")).toBeNull(); + }); + + it("should return null for a number that is not a non-negative safe integer", () => { + expect(getClassTrib(-200_001)).toBeNull(); + expect(getClassTrib(200_001.5)).toBeNull(); + expect(getClassTrib(2 ** 53)).toBeNull(); + }); + + it("should return null for a value that is not a string or a number", () => { + // @ts-expect-error not a string or number + expect(getClassTrib(null)).toBeNull(); + // @ts-expect-error not a string or number + expect(getClassTrib()).toBeNull(); + expect(getClassTrib(Object.create(null))).toBeNull(); + }); + + describe("properties", () => { + test("should never throw, regardless of the input", () => { + expectNeverThrows(getClassTrib, anyGarbage); + }); + + test("should resolve every code isValidClassTrib knows, under a CST getCstIbsCbs knows", () => { + fc.assert( + fc.property(fc.constantFrom(...CLASS_TRIB_CODES), (code) => { + const entry = getClassTrib(code); + + expect(entry?.code).toBe(code); + expect(getCstIbsCbs(entry?.cst ?? "")).not.toBeNull(); + expect(isValidClassTrib(code, { cst: entry?.cst })).toBe(true); + }), + ); + }); + + test("should describe exactly the codes isValidClassTrib accepts", () => { + expect(Object.keys(CLASS_TRIB_TABLE).sort()).toEqual([...CLASS_TRIB_CODES]); + }); + + test("should resolve a value exactly when isValidClassTrib accepts it", () => { + fc.assert( + fc.property(fc.integer({ min: 0, max: 999_999 }), (value) => { + expect(getClassTrib(value) !== null).toBe(isValidClassTrib(value)); + }), + ); + }); + }); +}); + +describe("getClassTrib types", () => { + test("should take a string or number and return a ClassTrib or null", () => { + expectTypeOf(getClassTrib).parameter(0).toEqualTypeOf(); + expectTypeOf(getClassTrib).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + code: string; + cst: string; + name: string; + description: string; + }>(); + }); +}); diff --git a/src/get-class-trib/get-class-trib.ts b/src/get-class-trib/get-class-trib.ts new file mode 100644 index 00000000..6a28e8d5 --- /dev/null +++ b/src/get-class-trib/get-class-trib.ts @@ -0,0 +1,89 @@ +import { + CLASS_TRIB_FORMAT_REGEX, + CLASS_TRIB_LENGTH, + CLASS_TRIB_TABLE, + CST_IBS_CBS_LENGTH, +} from "../_internals/constants/ibs-cbs"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { padLookupCode } from "../_internals/pad-lookup-code/pad-lookup-code"; + +/** + * A cClassTrib (Código de Classificação Tributária do IBS e da CBS) code. + */ +export type ClassTrib = { + /** The 6 digit cClassTrib code. */ + code: string; + /** The 3 digit CST-IBS/CBS the classification belongs to, the first 3 digits of the code. */ + cst: string; + /** The short name the official table gives for display ("Nome cClassTrib"). */ + name: string; + /** The situation the classification refers to ("Descrição cClassTrib"). */ + description: string; +}; + +/** + * Looks a cClassTrib (Código de Classificação Tributária do IBS e da CBS) up in the official + * table, the code the field `cClassTrib` of the group `IBSCBS` carries next to the CST-IBS/CBS + * in the electronic fiscal documents of the tax reform (Lei Complementar nº 214/2025). + * + * Every classification belongs to exactly one CST-IBS/CBS, the first 3 digits of its code, so + * the entry carries it as `cst` and the lookup needs no CST to narrow it. The table holds the + * classifications in force: one the Informe Técnico excluded by closing its validity (220001, + * 220002 and 220003 in v.1.60) gives `null`. The legal wording the workbook also prints for + * each row (the article of the law and of both regulations) is not shipped. + * + * A string is only read as a code when it is written as bare digits, with optional surrounding + * whitespace: the field has no mask, so anything else (`"c200001"`) 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. A value narrower than 6 digits is left padded with zeros, as a string or as a number, + * since the codes start with zeros a numeric field drops: `1`, `"1"` and `"000001"` are all the + * code `000001`. + * + * @param {string|number} value - The cClassTrib to look up, e.g. `"200001"` or `200001`. + * @returns {ClassTrib|null} The matching entry, or null when the code is unknown or invalid. + * + * @example + * ```typescript + * getClassTrib("000002"); + * // { + * // code: "000002", + * // cst: "000", + * // name: "Exploração de via", + * // description: "Exploração de via, observado o art. 11 da Lei Complementar nº 214, de 2025.", + * // } + * getClassTrib(2)?.code; // "000002" + * getClassTrib("999999"); // null + * getClassTrib("220001"); // null (excluded by Informe Técnico 2025.002 v.1.60) + * getClassTrib("c200001"); // null (not a documented form) + * ``` + * + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=/NJarYc9nus= + * "Documentos" > "Diversos" of the Portal Nacional da NF-e, which publishes every version of the + * "Tabela de Classificação Tributária do IBS e CBS" workbook (sheets CST and cClassTrib). + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=hXzemuyNHW4= + * Informe Técnico 2025.002 (v.1.60 of 22/06/2026), which divulges both tables, defines their + * columns and states that the first three digits of a cClassTrib are its CST-IBS/CBS. + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=04BIflQt1aY= + * Nota Técnica 2025.002-RTC (v.1.51), fields UB13 `CST` (N, 3 digits) and UB14 `cClassTrib` (N, + * 6 digits) and the rejections 1020 (unknown CST), 1023 (unknown cClassTrib) and 1024 + * (cClassTrib incompatible with the CST). + * @see Official: https://dfe-portal.svrs.rs.gov.br/DFE/TabelaClassificacaoTributaria + * The same tables online, on the Portal dos Documentos Fiscais Eletrônicos (SVRS). + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm + * Lei Complementar nº 214/2025, which institutes the IBS and the CBS. + */ +export const getClassTrib = (value: string | number): ClassTrib | null => { + if (!isLookupCode(value)) return null; + + const code = padLookupCode(value, CLASS_TRIB_LENGTH); + + if (!CLASS_TRIB_FORMAT_REGEX.test(code)) return null; + + const entry = CLASS_TRIB_TABLE[code]; + + if (entry === undefined) return null; + + const [name, description] = entry; + + return { code, cst: code.slice(0, CST_IBS_CBS_LENGTH), name, description }; +}; diff --git a/src/get-cst-ibs-cbs/get-cst-ibs-cbs.test.ts b/src/get-cst-ibs-cbs/get-cst-ibs-cbs.test.ts new file mode 100644 index 00000000..b1243b3d --- /dev/null +++ b/src/get-cst-ibs-cbs/get-cst-ibs-cbs.test.ts @@ -0,0 +1,107 @@ +import * as fc from "fast-check"; + +import { CST_IBS_CBS_TABLE } from "../_internals/constants/ibs-cbs"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { isValidCstIbsCbs } from "../is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs"; +import { getCstIbsCbs, type CstIbsCbs } from "./get-cst-ibs-cbs"; + +describe("getCstIbsCbs", () => { + it("should return the entry of a known code", () => { + expect(getCstIbsCbs("000")).toEqual({ code: "000", description: "Tributação integral" }); + expect(getCstIbsCbs("410")).toEqual({ + code: "410", + description: "Imunidade e não incidência", + }); + expect(getCstIbsCbs(620)).toEqual({ code: "620", description: "Tributação monofásica" }); + }); + + it("should take the description from the CST sheet, not the per classification wording of the cClassTrib sheet", () => { + expect(getCstIbsCbs("200")?.description).toBe("Alíquota reduzida"); + expect(getCstIbsCbs("011")?.description).toBe("Tributação com alíquotas uniformes reduzidas"); + }); + + it("should carry the wording Informe Técnico 2025.002 v.1.40 gave CST 820", () => { + expect(getCstIbsCbs("820")?.description).toBe("Tributação em documento específico"); + }); + + it("should carry the codes later versions of the table added (222, 515 and 811)", () => { + expect(getCstIbsCbs("222")?.description).toBe("Redução de base de cálculo"); + expect(getCstIbsCbs("515")?.description).toBe("Diferimento com redução de alíquota"); + expect(getCstIbsCbs("811")?.description).toBe("Ajustes"); + }); + + it("should pad a value with leading zeros, as a number or as a string", () => { + const expected = { code: "010", description: "Tributação com alíquotas uniformes" }; + + expect(getCstIbsCbs(10)).toEqual(expected); + expect(getCstIbsCbs("10")).toEqual(expected); + expect(getCstIbsCbs(" 010 ")).toEqual(expected); + expect(getCstIbsCbs(0)?.code).toBe("000"); + }); + + it("should return a fresh object on every call", () => { + expect(getCstIbsCbs("000")).not.toBe(getCstIbsCbs("000")); + }); + + it("should return null for a code the table does not carry", () => { + expect(getCstIbsCbs("100")).toBeNull(); + expect(getCstIbsCbs("060")).toBeNull(); + expect(getCstIbsCbs("0000")).toBeNull(); + expect(getCstIbsCbs("200001")).toBeNull(); + }); + + it("should return null for a string that is not bare digits", () => { + expect(getCstIbsCbs("cst200")).toBeNull(); + expect(getCstIbsCbs("2-00")).toBeNull(); + expect(getCstIbsCbs("")).toBeNull(); + expect(getCstIbsCbs("__proto__")).toBeNull(); + }); + + it("should return null for a number that is not a non-negative safe integer", () => { + expect(getCstIbsCbs(-200)).toBeNull(); + expect(getCstIbsCbs(20.5)).toBeNull(); + expect(getCstIbsCbs(2 ** 53)).toBeNull(); + }); + + it("should return null for a value that is not a string or a number", () => { + // @ts-expect-error not a string or number + expect(getCstIbsCbs(null)).toBeNull(); + // @ts-expect-error not a string or number + expect(getCstIbsCbs()).toBeNull(); + expect(getCstIbsCbs(Object.create(null))).toBeNull(); + }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...Object.keys(CST_IBS_CBS_TABLE)); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(getCstIbsCbs, anyGarbage); + }); + + test("should resolve a value exactly when isValidCstIbsCbs accepts it", () => { + fc.assert( + fc.property(fc.integer({ min: 0, max: 1200 }), (value) => { + expect(getCstIbsCbs(value) !== null).toBe(isValidCstIbsCbs(value)); + }), + ); + }); + + test("should hand every code of the table back under its own code", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + expect(getCstIbsCbs(code)?.code).toBe(code); + }), + ); + }); + }); +}); + +describe("getCstIbsCbs types", () => { + test("should take a string or number and return a CstIbsCbs or null", () => { + expectTypeOf(getCstIbsCbs).parameter(0).toEqualTypeOf(); + expectTypeOf(getCstIbsCbs).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ code: string; description: string }>(); + }); +}); diff --git a/src/get-cst-ibs-cbs/get-cst-ibs-cbs.ts b/src/get-cst-ibs-cbs/get-cst-ibs-cbs.ts new file mode 100644 index 00000000..eeda7b44 --- /dev/null +++ b/src/get-cst-ibs-cbs/get-cst-ibs-cbs.ts @@ -0,0 +1,72 @@ +import { + CST_IBS_CBS_FORMAT_REGEX, + CST_IBS_CBS_LENGTH, + CST_IBS_CBS_TABLE, +} from "../_internals/constants/ibs-cbs"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { padLookupCode } from "../_internals/pad-lookup-code/pad-lookup-code"; + +/** + * A CST-IBS/CBS (Código de Situação Tributária do IBS e da CBS) code. + */ +export type CstIbsCbs = { + /** The 3 digit CST-IBS/CBS code. */ + code: string; + /** The description the official CST table gives the code. */ + description: string; +}; + +/** + * Looks a CST-IBS/CBS (Código de Situação Tributária do IBS e da CBS) up in the official table, + * the code the field `CST` of the group `IBSCBS` carries in the NF-e, NFC-e, CT-e, NFS-e and the + * other electronic fiscal documents of the tax reform (Lei Complementar nº 214/2025). + * + * It is a table of its own, not one more tax of `isValidCst`: IBS and CBS share it, and its 3 + * digit codes (`000`, `200`, `410`, ...) would be misread as the ICMS origin plus Tabela B form. + * + * A string is only read as a code when it is written as bare digits, with optional surrounding + * whitespace: the field has no mask, so anything else (`"cst200"`) 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. + * A value narrower than 3 digits is left padded with zeros, as a string or as a number, since + * the codes start with zeros a numeric field drops: `0`, `"0"` and `"000"` are all the code `000`. + * + * @param {string|number} value - The CST-IBS/CBS to look up, e.g. `"200"`, `"000"` or `200`. + * @returns {CstIbsCbs|null} The matching entry, or null when the code is unknown or invalid. + * + * @example + * ```typescript + * getCstIbsCbs("000"); // { code: "000", description: "Tributação integral" } + * getCstIbsCbs(410); // { code: "410", description: "Imunidade e não incidência" } + * getCstIbsCbs(10); // { code: "010", description: "Tributação com alíquotas uniformes" } + * getCstIbsCbs("100"); // null + * getCstIbsCbs("cst200"); // null (not a documented form) + * ``` + * + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=/NJarYc9nus= + * "Documentos" > "Diversos" of the Portal Nacional da NF-e, which publishes every version of the + * "Tabela de Classificação Tributária do IBS e CBS" workbook (sheets CST and cClassTrib). + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=hXzemuyNHW4= + * Informe Técnico 2025.002 (v.1.60 of 22/06/2026), which divulges both tables, defines their + * columns and states that the first three digits of a cClassTrib are its CST-IBS/CBS. + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=04BIflQt1aY= + * Nota Técnica 2025.002-RTC (v.1.51), fields UB13 `CST` (N, 3 digits) and UB14 `cClassTrib` (N, + * 6 digits) and the rejections 1020 (unknown CST), 1023 (unknown cClassTrib) and 1024 + * (cClassTrib incompatible with the CST). + * @see Official: https://dfe-portal.svrs.rs.gov.br/DFE/TabelaClassificacaoTributaria + * The same tables online, on the Portal dos Documentos Fiscais Eletrônicos (SVRS). + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm + * Lei Complementar nº 214/2025, which institutes the IBS and the CBS. + */ +export const getCstIbsCbs = (value: string | number): CstIbsCbs | null => { + if (!isLookupCode(value)) return null; + + const code = padLookupCode(value, CST_IBS_CBS_LENGTH); + + if (!CST_IBS_CBS_FORMAT_REGEX.test(code)) return null; + + const description = CST_IBS_CBS_TABLE[code]; + + if (description === undefined) return null; + + return { code, description }; +}; diff --git a/src/index.test.ts b/src/index.test.ts index e319d1fd..e3569839 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -12,11 +12,13 @@ import { type CertidaoInfo, type CertidaoType, type Cfop, + type ClassTrib, type Cnae, type CnpjInfo, type ConvertDateToWordsOptions, type ConvertNumberToWordsOptions, type CpfInfo, + type CstIbsCbs, type FormatBoletoOptions, type FormatCaepfOptions, type FormatCeiOptions, @@ -70,6 +72,7 @@ import { type IsValidBankAccountOptions, type IsValidBankAccountParams, type IsValidCertidaoOptions, + type IsValidClassTribOptions, type IsValidCnpjOptions, type IsValidCstOptions, type IsValidGtinOptions, @@ -187,9 +190,11 @@ const PUBLIC = [ "getCertidaoInfo", "getCfop", "getCities", + "getClassTrib", "getCnae", "getCnpjInfo", "getCpfInfo", + "getCstIbsCbs", "getFormatLicensePlate", "getGtinInfo", "getHolidays", @@ -228,6 +233,7 @@ const PUBLIC = [ "isValidCep", "isValidCertidao", "isValidCfop", + "isValidClassTrib", "isValidCnae", "isValidCnh", "isValidCno", @@ -237,6 +243,7 @@ const PUBLIC = [ "isValidCreditCard", "isValidCsosn", "isValidCst", + "isValidCstIbsCbs", "isValidEmail", "isValidGtin", "isValidIE", @@ -336,11 +343,13 @@ describe("Public API", () => { CertidaoInfo: CertidaoInfo; CertidaoType: CertidaoType; Cfop: Cfop; + ClassTrib: ClassTrib; Cnae: Cnae; CnpjInfo: CnpjInfo; ConvertDateToWordsOptions: ConvertDateToWordsOptions; ConvertNumberToWordsOptions: ConvertNumberToWordsOptions; CpfInfo: CpfInfo; + CstIbsCbs: CstIbsCbs; FormatBoletoOptions: FormatBoletoOptions; FormatCaepfOptions: FormatCaepfOptions; FormatCeiOptions: FormatCeiOptions; @@ -394,6 +403,7 @@ describe("Public API", () => { IsValidBankAccountOptions: IsValidBankAccountOptions; IsValidBankAccountParams: IsValidBankAccountParams; IsValidCertidaoOptions: IsValidCertidaoOptions; + IsValidClassTribOptions: IsValidClassTribOptions; IsValidCnpjOptions: IsValidCnpjOptions; IsValidCstOptions: IsValidCstOptions; IsValidGtinOptions: IsValidGtinOptions; diff --git a/src/index.ts b/src/index.ts index df01a6a9..2c1a03f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,9 +116,11 @@ export { } from "./get-certidao-info/get-certidao-info"; export { type Cfop, getCfop } from "./get-cfop/get-cfop"; export { getCities } from "./get-cities/get-cities"; +export { type ClassTrib, getClassTrib } from "./get-class-trib/get-class-trib"; export { type Cnae, getCnae } from "./get-cnae/get-cnae"; export { type CnpjInfo, getCnpjInfo, type GetCnpjInfoOptions } from "./get-cnpj-info/get-cnpj-info"; export { type CpfInfo, getCpfInfo } from "./get-cpf-info/get-cpf-info"; +export { type CstIbsCbs, getCstIbsCbs } from "./get-cst-ibs-cbs/get-cst-ibs-cbs"; export { getFormatLicensePlate, type LicensePlateFormat, @@ -202,6 +204,10 @@ export { isValidCertidao, } from "./is-valid-certidao/is-valid-certidao"; export { isValidCfop } from "./is-valid-cfop/is-valid-cfop"; +export { + type IsValidClassTribOptions, + isValidClassTrib, +} from "./is-valid-class-trib/is-valid-class-trib"; export { isValidCnae } from "./is-valid-cnae/is-valid-cnae"; export { isValidCnh } from "./is-valid-cnh/is-valid-cnh"; export { isValidCno } from "./is-valid-cno/is-valid-cno"; @@ -211,6 +217,7 @@ export { isValidCpf } from "./is-valid-cpf/is-valid-cpf"; export { isValidCreditCard } from "./is-valid-credit-card/is-valid-credit-card"; export { isValidCsosn } from "./is-valid-csosn/is-valid-csosn"; export { type IsValidCstOptions, isValidCst } from "./is-valid-cst/is-valid-cst"; +export { isValidCstIbsCbs } from "./is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs"; export { isValidEmail } from "./is-valid-email/is-valid-email"; export { type IsValidGtinOptions, isValidGtin } from "./is-valid-gtin/is-valid-gtin"; export { isValidIban } from "./is-valid-iban/is-valid-iban"; diff --git a/src/is-valid-class-trib/is-valid-class-trib.test.ts b/src/is-valid-class-trib/is-valid-class-trib.test.ts new file mode 100644 index 00000000..8cb7adbb --- /dev/null +++ b/src/is-valid-class-trib/is-valid-class-trib.test.ts @@ -0,0 +1,179 @@ +import * as fc from "fast-check"; + +import { CLASS_TRIB_CODES } from "../_internals/constants/ibs-cbs"; +import { anyGarbage, digitsOfOtherLength } from "../_internals/test/arbitraries"; +import { expectNeverThrowsWithOptions } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { isValidClassTrib, type IsValidClassTribOptions } from "./is-valid-class-trib"; + +describe("isValidClassTrib", () => { + it("should validate a known code, the first and the last of the table included", () => { + expect(isValidClassTrib("000001")).toBe(true); + expect(isValidClassTrib("200001")).toBe(true); + expect(isValidClassTrib("410999")).toBe(true); + expect(isValidClassTrib("830001")).toBe(true); + }); + + it("should validate a code given as a number", () => { + expect(isValidClassTrib(200_001)).toBe(true); + }); + + it("should pad a value with leading zeros, as a number or as a string", () => { + expect(isValidClassTrib(1)).toBe(true); + expect(isValidClassTrib("1")).toBe(true); + expect(isValidClassTrib(10_001)).toBe(true); + expect(isValidClassTrib("11001")).toBe(true); + }); + + it("should validate a code with surrounding whitespace", () => { + expect(isValidClassTrib(" 200001 ")).toBe(true); + }); + + it("should validate the codes Informe Técnico 2025.002 v.1.50 and v.1.60 created", () => { + expect(isValidClassTrib("000005")).toBe(true); + expect(isValidClassTrib("200054")).toBe(true); + expect(isValidClassTrib("410036")).toBe(true); + expect(isValidClassTrib("410037")).toBe(true); + expect(isValidClassTrib("550024")).toBe(true); + expect(isValidClassTrib("550025")).toBe(true); + expect(isValidClassTrib("620007")).toBe(true); + expect(isValidClassTrib("221002")).toBe(true); + expect(isValidClassTrib("221003")).toBe(true); + expect(isValidClassTrib("221004")).toBe(true); + }); + + it("should reject the classifications of CST 220 Informe Técnico 2025.002 v.1.60 excluded", () => { + expect(isValidClassTrib("220001")).toBe(false); + expect(isValidClassTrib("220002")).toBe(false); + expect(isValidClassTrib("220003")).toBe(false); + }); + + it("should return false for a 6 digit code the table does not carry", () => { + expect(isValidClassTrib("999999")).toBe(false); + expect(isValidClassTrib("200000")).toBe(false); + expect(isValidClassTrib("000000")).toBe(false); + expect(isValidClassTrib(0)).toBe(false); + }); + + it("should return false for a value wider than 6 digits and for a CST alone", () => { + expect(isValidClassTrib("2000010")).toBe(false); + expect(isValidClassTrib("0200001")).toBe(false); + expect(isValidClassTrib("200")).toBe(false); + }); + + it("should return false for a string that is not bare digits", () => { + expect(isValidClassTrib("c200001")).toBe(false); + expect(isValidClassTrib("200.001")).toBe(false); + expect(isValidClassTrib("200 001")).toBe(false); + expect(isValidClassTrib("")).toBe(false); + expect(isValidClassTrib(" ")).toBe(false); + }); + + it("should return false for a number that is not a non-negative safe integer", () => { + expect(isValidClassTrib(-200_001)).toBe(false); + expect(isValidClassTrib(200_001.5)).toBe(false); + expect(isValidClassTrib(2 ** 53)).toBe(false); + }); + + it("should return false for a value that is not a string or a number", () => { + // @ts-expect-error not a string or number + expect(isValidClassTrib(null)).toBe(false); + // @ts-expect-error not a string or number + expect(isValidClassTrib()).toBe(false); + // @ts-expect-error not a string or number + expect(isValidClassTrib(["200001"])).toBe(false); + }); + + describe("options.cst", () => { + it("should accept a classification together with the CST it belongs to", () => { + expect(isValidClassTrib("200001", { cst: "200" })).toBe(true); + expect(isValidClassTrib("410999", { cst: 410 })).toBe(true); + expect(isValidClassTrib("620007", { cst: " 620 " })).toBe(true); + }); + + it("should pad the CST the way the code is padded", () => { + expect(isValidClassTrib(1, { cst: 0 })).toBe(true); + expect(isValidClassTrib("010001", { cst: 10 })).toBe(true); + expect(isValidClassTrib("011001", { cst: "11" })).toBe(true); + }); + + it("should reject a classification together with another CST (rejection 1024)", () => { + expect(isValidClassTrib("200001", { cst: "000" })).toBe(false); + expect(isValidClassTrib("000001", { cst: "200" })).toBe(false); + expect(isValidClassTrib("010001", { cst: "011" })).toBe(false); + }); + + it("should reject an unknown classification even when its first digits are the CST", () => { + expect(isValidClassTrib("200999", { cst: "200" })).toBe(false); + expect(isValidClassTrib("220001", { cst: "220" })).toBe(false); + }); + + it("should reject a CST that is not written as a code", () => { + expect(isValidClassTrib("200001", { cst: "" })).toBe(false); + expect(isValidClassTrib("200001", { cst: "2" })).toBe(false); + expect(isValidClassTrib("200001", { cst: "200001" })).toBe(false); + expect(isValidClassTrib("200001", { cst: "cst200" })).toBe(false); + expect(isValidClassTrib("200001", { cst: -200 })).toBe(false); + }); + + it("should reject a CST that is not a string or a number", () => { + // @ts-expect-error not a string or number + expect(isValidClassTrib("200001", { cst: null })).toBe(false); + // @ts-expect-error not a string or number + expect(isValidClassTrib("200001", { cst: ["200"] })).toBe(false); + // @ts-expect-error not a string or number + expect(isValidClassTrib("200001", { cst: true })).toBe(false); + }); + + it("should check the cClassTrib alone when the CST is omitted", () => { + expect(isValidClassTrib("200001", {})).toBe(true); + expect(isValidClassTrib("200001", { cst: undefined })).toBe(true); + expect(isValidClassTrib("999999", {})).toBe(false); + }); + }); + + it("should return false when options is not an object", () => { + // @ts-expect-error not an options object + expect(isValidClassTrib("200001", null)).toBe(false); + // @ts-expect-error not an options object + expect(isValidClassTrib("200001", "200")).toBe(false); + }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...CLASS_TRIB_CODES); + + test("should never throw, regardless of the input and the options", () => { + expectNeverThrowsWithOptions(isValidClassTrib, anyGarbage, anyGarbage); + expectNeverThrowsWithOptions(isValidClassTrib, codeArbitrary, fc.record({ cst: anyGarbage })); + }); + + test("should validate every code of the table, alone and with its first 3 digits as the CST", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + expect(isValidClassTrib(code)).toBe(true); + expect(isValidClassTrib(Number(code))).toBe(true); + expect(isValidClassTrib(code, { cst: code.slice(0, 3) })).toBe(true); + }), + ); + }); + + test("should reject every digit string that is empty or wider than 6 digits", () => { + fc.assert( + fc.property(digitsOfOtherLength(12, [1, 2, 3, 4, 5, 6]), (value) => { + expect(isValidClassTrib(value)).toBe(false); + }), + ); + }); + }); +}); + +describe("isValidClassTrib types", () => { + test("should take a string or number plus options and return a boolean", () => { + expectTypeOf(isValidClassTrib).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidClassTrib) + .parameter(1) + .toEqualTypeOf(); + expectTypeOf(isValidClassTrib).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ cst?: string | number }>(); + }); +}); diff --git a/src/is-valid-class-trib/is-valid-class-trib.ts b/src/is-valid-class-trib/is-valid-class-trib.ts new file mode 100644 index 00000000..977c3faf --- /dev/null +++ b/src/is-valid-class-trib/is-valid-class-trib.ts @@ -0,0 +1,91 @@ +import { + CLASS_TRIB_CODES, + CLASS_TRIB_FORMAT_REGEX, + CLASS_TRIB_LENGTH, + CST_IBS_CBS_LENGTH, +} from "../_internals/constants/ibs-cbs"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { padLookupCode } from "../_internals/pad-lookup-code/pad-lookup-code"; + +/** + * Options for `isValidClassTrib`. + */ +export type IsValidClassTribOptions = { + /** + * The CST-IBS/CBS the document carries next to the cClassTrib. When given, the classification + * also has to belong to it (its first 3 digits); omit it to check the cClassTrib alone. + */ + cst?: string | number; +}; + +/** + * Validates if a cClassTrib (Código de Classificação Tributária do IBS e da CBS) exists in the + * official table, the check behind the rejection 1023 "Classificação Tributária do IBS/CBS + * informada inexistente" of the NF-e. + * + * A document carries the cClassTrib next to a CST-IBS/CBS and the pair has to match: every + * classification belongs to one CST, the first 3 digits of its code. Pass that CST as + * `options.cst` to check the pair too, the rejection 1024 "Classificação Tributária do IBS e da + * CBS incompatível com o CST informado". A `cst` that is given and is not the CST of the + * classification, whatever it is, makes the result false. + * + * Only the classifications in force count: one the Informe Técnico excluded by closing its + * validity (220001, 220002 and 220003 in v.1.60) is rejected. Only the code list is bundled with + * this function, not the descriptions `getClassTrib` returns. + * + * A string is only read as a code when it is written as bare digits, with optional surrounding + * whitespace: the field has no mask, so anything else (`"c200001"`) 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. A value narrower than 6 digits is left padded with zeros, as a string or as a number, + * since the codes start with zeros a numeric field drops: `1`, `"1"` and `"000001"` are all the + * code `000001`. `options.cst` is read the same way, padded to 3 digits. + * + * @param {string|number} value - The cClassTrib to be validated, e.g. `"200001"` or `200001`. + * @param {IsValidClassTribOptions} [options] - The CST-IBS/CBS the code has to belong to. + * @returns {boolean} True when the code is in the cClassTrib table and, when `options.cst` is + * given, belongs to that CST; false otherwise. + * + * @example + * ```typescript + * isValidClassTrib("200001"); // true + * isValidClassTrib(1); // true (padded to "000001") + * isValidClassTrib("200001", { cst: "200" }); // true + * isValidClassTrib("200001", { cst: "000" }); // false (the classification belongs to CST 200) + * isValidClassTrib("999999"); // false + * isValidClassTrib("220001"); // false (excluded by Informe Técnico 2025.002 v.1.60) + * isValidClassTrib("c200001"); // false (not a documented form) + * ``` + * + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=/NJarYc9nus= + * "Documentos" > "Diversos" of the Portal Nacional da NF-e, which publishes every version of the + * "Tabela de Classificação Tributária do IBS e CBS" workbook (sheets CST and cClassTrib). + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=hXzemuyNHW4= + * Informe Técnico 2025.002 (v.1.60 of 22/06/2026), which divulges both tables, defines their + * columns and states that the first three digits of a cClassTrib are its CST-IBS/CBS. + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=04BIflQt1aY= + * Nota Técnica 2025.002-RTC (v.1.51), fields UB13 `CST` (N, 3 digits) and UB14 `cClassTrib` (N, + * 6 digits) and the rejections 1020 (unknown CST), 1023 (unknown cClassTrib) and 1024 + * (cClassTrib incompatible with the CST). + * @see Official: https://dfe-portal.svrs.rs.gov.br/DFE/TabelaClassificacaoTributaria + * The same tables online, on the Portal dos Documentos Fiscais Eletrônicos (SVRS). + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm + * Lei Complementar nº 214/2025, which institutes the IBS and the CBS. + */ +export const isValidClassTrib = ( + value: string | number, + options?: IsValidClassTribOptions, +): boolean => { + if (!isLookupCode(value)) return false; + if (options !== undefined && (options === null || typeof options !== "object")) return false; + + const code = padLookupCode(value, CLASS_TRIB_LENGTH); + + if (!CLASS_TRIB_FORMAT_REGEX.test(code) || !CLASS_TRIB_CODES.includes(code)) return false; + + const cst = options?.cst; + + if (cst === undefined) return true; + if (!isLookupCode(cst)) return false; + + return code.slice(0, CST_IBS_CBS_LENGTH) === padLookupCode(cst, CST_IBS_CBS_LENGTH); +}; diff --git a/src/is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs.test.ts b/src/is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs.test.ts new file mode 100644 index 00000000..4fa44ef9 --- /dev/null +++ b/src/is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs.test.ts @@ -0,0 +1,130 @@ +import * as fc from "fast-check"; + +import { CST_IBS_CBS_TABLE } from "../_internals/constants/ibs-cbs"; +import { anyGarbage, digitsOfOtherLength } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { isValidCstIbsCbs } from "./is-valid-cst-ibs-cbs"; + +describe("isValidCstIbsCbs", () => { + it("should validate the 18 codes of the CST table published on 23/06/2026", () => { + const codes = [ + "000", + "010", + "011", + "200", + "220", + "221", + "222", + "400", + "410", + "510", + "515", + "550", + "620", + "800", + "810", + "811", + "820", + "830", + ]; + + expect(codes.filter((code) => isValidCstIbsCbs(code))).toEqual(codes); + }); + + it("should validate a code given as a number", () => { + expect(isValidCstIbsCbs(200)).toBe(true); + expect(isValidCstIbsCbs(830)).toBe(true); + }); + + it("should pad a value with leading zeros, as a number or as a string", () => { + expect(isValidCstIbsCbs(0)).toBe(true); + expect(isValidCstIbsCbs("0")).toBe(true); + expect(isValidCstIbsCbs(10)).toBe(true); + expect(isValidCstIbsCbs("11")).toBe(true); + }); + + it("should validate a code with surrounding whitespace", () => { + expect(isValidCstIbsCbs(" 410 ")).toBe(true); + }); + + it("should keep CST 220 valid, which the CST table still lists after its classifications were excluded", () => { + expect(isValidCstIbsCbs("220")).toBe(true); + }); + + it("should return false for a 3 digit code the table does not carry", () => { + expect(isValidCstIbsCbs("100")).toBe(false); + expect(isValidCstIbsCbs("999")).toBe(false); + expect(isValidCstIbsCbs(20)).toBe(false); + }); + + it("should return false for an ICMS, IPI, PIS or COFINS CST", () => { + expect(isValidCstIbsCbs("060")).toBe(false); + expect(isValidCstIbsCbs("49")).toBe(false); + }); + + it("should return false for a wider value, a cClassTrib included", () => { + expect(isValidCstIbsCbs("0000")).toBe(false); + expect(isValidCstIbsCbs("200001")).toBe(false); + }); + + it("should return false for a string that is not bare digits", () => { + expect(isValidCstIbsCbs("cst200")).toBe(false); + expect(isValidCstIbsCbs("2.00")).toBe(false); + expect(isValidCstIbsCbs("200\n1")).toBe(false); + expect(isValidCstIbsCbs("")).toBe(false); + expect(isValidCstIbsCbs(" ")).toBe(false); + }); + + it("should return false for a number that is not a non-negative safe integer", () => { + expect(isValidCstIbsCbs(-200)).toBe(false); + expect(isValidCstIbsCbs(20.5)).toBe(false); + expect(isValidCstIbsCbs(2 ** 53)).toBe(false); + }); + + it("should return false for a value that is not a string or a number", () => { + // @ts-expect-error not a string or number + expect(isValidCstIbsCbs(null)).toBe(false); + // @ts-expect-error not a string or number + expect(isValidCstIbsCbs()).toBe(false); + // @ts-expect-error not a string or number + expect(isValidCstIbsCbs(["200"])).toBe(false); + }); + + it("should return false for a key of the prototype chain", () => { + expect(isValidCstIbsCbs("__proto__")).toBe(false); + expect(isValidCstIbsCbs("constructor")).toBe(false); + }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...Object.keys(CST_IBS_CBS_TABLE)); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(isValidCstIbsCbs, anyGarbage); + }); + + test("should validate every code of the table, as a string or a number", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + expect(isValidCstIbsCbs(code)).toBe(true); + expect(isValidCstIbsCbs(Number(code))).toBe(true); + }), + ); + }); + + test("should reject every digit string that is empty or wider than 3 digits", () => { + fc.assert( + fc.property(digitsOfOtherLength(12, [1, 2, 3]), (value) => { + expect(isValidCstIbsCbs(value)).toBe(false); + }), + ); + }); + }); +}); + +describe("isValidCstIbsCbs types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCstIbsCbs).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCstIbsCbs).returns.toEqualTypeOf(); + }); +}); diff --git a/src/is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs.ts b/src/is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs.ts new file mode 100644 index 00000000..777257b9 --- /dev/null +++ b/src/is-valid-cst-ibs-cbs/is-valid-cst-ibs-cbs.ts @@ -0,0 +1,59 @@ +import { + CST_IBS_CBS_FORMAT_REGEX, + CST_IBS_CBS_LENGTH, + CST_IBS_CBS_TABLE, +} from "../_internals/constants/ibs-cbs"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { padLookupCode } from "../_internals/pad-lookup-code/pad-lookup-code"; + +/** + * Validates if a CST-IBS/CBS (Código de Situação Tributária do IBS e da CBS) exists in the + * official table, the check behind the rejection 1020 "CST do IBS/CBS informado inexistente" of + * the NF-e. + * + * It is a function of its own, not a `tax` of `isValidCst`: IBS and CBS share one table, its 3 + * digit codes (`000`, `200`, `410`, ...) collide with the ICMS origin plus Tabela B form, and + * `isValidCst` without a `tax` accepts a code of any table, so adding this one to it would change + * what that default accepts. + * + * A string is only read as a code when it is written as bare digits, with optional surrounding + * whitespace: the field has no mask, so anything else (`"cst200"`) 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. + * A value narrower than 3 digits is left padded with zeros, as a string or as a number, since + * the codes start with zeros a numeric field drops: `0`, `"0"` and `"000"` are all the code `000`. + * + * @param {string|number} value - The CST-IBS/CBS to be validated, e.g. `"200"`, `"000"` or `200`. + * @returns {boolean} True when the code is in the CST-IBS/CBS table, false otherwise. + * + * @example + * ```typescript + * isValidCstIbsCbs("000"); // true + * isValidCstIbsCbs(410); // true + * isValidCstIbsCbs(10); // true (padded to "010") + * isValidCstIbsCbs("100"); // false + * isValidCstIbsCbs("cst200"); // false (not a documented form) + * isValidCstIbsCbs(-200); // false (not a non-negative safe integer) + * ``` + * + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=/NJarYc9nus= + * "Documentos" > "Diversos" of the Portal Nacional da NF-e, which publishes every version of the + * "Tabela de Classificação Tributária do IBS e CBS" workbook (sheets CST and cClassTrib). + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=hXzemuyNHW4= + * Informe Técnico 2025.002 (v.1.60 of 22/06/2026), which divulges both tables, defines their + * columns and states that the first three digits of a cClassTrib are its CST-IBS/CBS. + * @see Official: https://www.nfe.fazenda.gov.br/portal/listaConteudo.aspx?tipoConteudo=04BIflQt1aY= + * Nota Técnica 2025.002-RTC (v.1.51), fields UB13 `CST` (N, 3 digits) and UB14 `cClassTrib` (N, + * 6 digits) and the rejections 1020 (unknown CST), 1023 (unknown cClassTrib) and 1024 + * (cClassTrib incompatible with the CST). + * @see Official: https://dfe-portal.svrs.rs.gov.br/DFE/TabelaClassificacaoTributaria + * The same tables online, on the Portal dos Documentos Fiscais Eletrônicos (SVRS). + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm + * Lei Complementar nº 214/2025, which institutes the IBS and the CBS. + */ +export const isValidCstIbsCbs = (value: string | number): boolean => { + if (!isLookupCode(value)) return false; + + const code = padLookupCode(value, CST_IBS_CBS_LENGTH); + + return CST_IBS_CBS_FORMAT_REGEX.test(code) && code in CST_IBS_CBS_TABLE; +}; From fbee585ddff513895a01fea7e2de911e36596c02 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:44:03 -0300 Subject: [PATCH 2/6] fix(ibs-cbs): harden the workbook reader of the generator The xlsx reader inflated every entry of the downloaded archive with no bound, so a crafted or corrupted response could expand a small download into hundreds of megabytes: a 204 KB archive reached 200 MB of string. It now reads only the entries the workbook parser asks for, the shared strings and the worksheets, and caps each one at 64 MB, which throws ERR_BUFFER_TOO_LARGE into the existing catch. That also makes the doc comment true and skips the 14 entries nothing reads, the printer settings and the theme among them. Two smaller defects in the same reader: The in-force filter only looked at dFimVig, while its own comment and the pull request said it reads the two ended window of scripts/ncm.ts. It now requires dIniVig as well, so a classification published ahead of the date it starts to apply is not shipped as valid. Every row of the 23/06/2026 workbook starts on 01/01/2026, so the table does not change. A shared string joined the runs nested in an rPh element, the phonetic hint of the text, into the cell value, which would turn "NomeX" into "NomeX". The rPh elements are dropped before the runs are joined. The current workbook has none, so the table does not change. Re-running the generator reproduces src/_internals/constants/ibs-cbs.ts byte for byte. Part of #541 --- scripts/ibs-cbs.ts | 78 +++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/scripts/ibs-cbs.ts b/scripts/ibs-cbs.ts index 409c76f2..5e09b35e 100644 --- a/scripts/ibs-cbs.ts +++ b/scripts/ibs-cbs.ts @@ -50,6 +50,7 @@ const CST_DESCRIPTION_HEADER = "Descrição CST-IBS/CBS"; const CLASS_TRIB_HEADER = "cClassTrib"; const CLASS_TRIB_NAME_HEADER = "Nome cClassTrib"; const CLASS_TRIB_DESCRIPTION_HEADER = "Descrição cClassTrib"; +const START_OF_VALIDITY_HEADER = "dIniVig"; const END_OF_VALIDITY_HEADER = "dFimVig"; const XML_ENTITIES: Record = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }; @@ -127,11 +128,20 @@ const CENTRAL_DIRECTORY_ENTRY = 0x02_01_4b_50; const DEFLATE = 8; /** - * Reads the text files of a zip archive (an xlsx workbook is one) through its central directory. + * How much one entry may inflate to. The whole workbook is under 1 MB, so this leaves room for + * the table to grow while a crafted or corrupted download cannot expand without a bound. + */ +const MAXIMUM_ENTRY_SIZE = 64 * 1024 * 1024; + +/** + * Reads the entries of a zip archive (an xlsx workbook is one) that `isWanted` accepts, through + * its central directory. Nothing else is decompressed, and inflating stops at + * `MAXIMUM_ENTRY_SIZE` per entry, which throws `ERR_BUFFER_TOO_LARGE`. * @param {Buffer} zip - The archive. - * @returns {Map} The UTF-8 content of every entry, by path. + * @param {(name: string) => boolean} isWanted - Whether an entry path is one to read. + * @returns {Map} The UTF-8 content of the accepted entries, by path. */ -const unzip = (zip: Buffer): Map => { +const unzip = (zip: Buffer, isWanted: (name: string) => boolean): Map => { let end = zip.length - 22; while (end >= 0 && zip.readUInt32LE(end) !== END_OF_CENTRAL_DIRECTORY) end -= 1; @@ -146,15 +156,21 @@ const unzip = (zip: Buffer): Map => { throw new Error("The downloaded table has a broken zip central directory"); } - const method = zip.readUInt16LE(offset + 10); - const compressedSize = zip.readUInt32LE(offset + 20); const nameLength = zip.readUInt16LE(offset + 28); - const local = zip.readUInt32LE(offset + 42); const name = zip.toString("utf8", offset + 46, offset + 46 + nameLength); - const start = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28); - const data = zip.subarray(start, start + compressedSize); - files.set(name, (method === DEFLATE ? inflateRawSync(data) : data).toString("utf8")); + if (isWanted(name)) { + const method = zip.readUInt16LE(offset + 10); + const compressedSize = zip.readUInt32LE(offset + 20); + const local = zip.readUInt32LE(offset + 42); + const start = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28); + const data = zip.subarray(start, start + compressedSize); + const content = + method === DEFLATE ? inflateRawSync(data, { maxOutputLength: MAXIMUM_ENTRY_SIZE }) : data; + + files.set(name, content.toString("utf8")); + } + offset += 46 + nameLength + zip.readUInt16LE(offset + 30) + zip.readUInt16LE(offset + 32); } @@ -165,6 +181,7 @@ type Row = Record; const SHARED_STRING_REGEX = /([\s\S]*?)<\/si>/g; const TEXT_RUN_REGEX = /]*>([\s\S]*?)<\/t>/g; +const PHONETIC_RUN_REGEX = //g; const ROW_REGEX = /]*>([\s\S]*?)<\/row>/g; const CELL_REGEX = /]*?)(?:\/>|>([\s\S]*?)<\/c>)/g; const CELL_VALUE_REGEX = /([\s\S]*?)<\/v>/; @@ -207,41 +224,52 @@ const readSheet = (sheet: string, sharedStrings: string[]): Row[] => { }); }; +const SHARED_STRINGS_PATH = "xl/sharedStrings.xml"; + +const isWorksheet = (name: string): boolean => + name.startsWith("xl/worksheets/") && name.endsWith(".xml"); + /** - * Reads every worksheet of the workbook. + * Reads every worksheet of the workbook. The `rPh` elements of a shared string hold the phonetic + * hint of the text, not the text, so they are dropped before the runs are joined. * @param {Buffer} xlsx - The workbook. * @returns {Row[][]} The rows of each worksheet. */ const readWorkbook = (xlsx: Buffer): Row[][] => { - const files = unzip(xlsx); - const sharedStrings = [ - ...(files.get("xl/sharedStrings.xml") ?? "").matchAll(SHARED_STRING_REGEX), - ].map(([, item = ""]) => - decodeXml([...item.matchAll(TEXT_RUN_REGEX)].map(([, text = ""]) => text).join("")), - ); + const files = unzip(xlsx, (name) => name === SHARED_STRINGS_PATH || isWorksheet(name)); + const sharedStrings = [...(files.get(SHARED_STRINGS_PATH) ?? "").matchAll(SHARED_STRING_REGEX)] + .map(([, item = ""]) => item.replaceAll(PHONETIC_RUN_REGEX, "")) + .map((item) => + decodeXml([...item.matchAll(TEXT_RUN_REGEX)].map(([, text = ""]) => text).join("")), + ); return [...files] - .filter(([name]) => name.startsWith("xl/worksheets/") && name.endsWith(".xml")) + .filter(([name]) => isWorksheet(name)) .map(([, sheet]) => readSheet(sheet, sharedStrings)); }; const MS_PER_DAY = 86_400_000; +const fromSerialDate = (serial: string): number => + Date.UTC(1899, 11, 30) + Number(serial) * MS_PER_DAY; + /** - * Whether a row is in force on `today`. `dFimVig` is an Excel serial date (days since - * 30/12/1899) and is inclusive, the way `scripts/ncm.ts` reads the Siscomex window. A - * classification the Informe Técnico excludes is not deleted from the table, it gets a `dFimVig` - * (220001, 220002 and 220003 in v.1.60), so this is what keeps it out. + * Whether a row is in force on `today`. `dIniVig` and `dFimVig` are Excel serial dates (days + * since 30/12/1899) and both ends of the window are inclusive, the way `scripts/ncm.ts` reads the + * Siscomex window. A classification the Informe Técnico excludes is not deleted from the table, + * it gets a `dFimVig` (220001, 220002 and 220003 in v.1.60), so this is what keeps it out, and a + * classification published before it starts to apply stays out until its `dIniVig`. * @param {Row} row - A classification row. * @param {number} today - The reference date, in milliseconds at UTC midnight. - * @returns {boolean} True when the row has no end of validity, or one that has not passed. + * @returns {boolean} True when `today` is inside the validity window the row declares. */ const isInForce = (row: Row, today: number): boolean => { - const serial = row[END_OF_VALIDITY_HEADER]; + const start = row[START_OF_VALIDITY_HEADER]; + const end = row[END_OF_VALIDITY_HEADER]; - if (serial === undefined) return true; + if (start !== undefined && today < fromSerialDate(start)) return false; - return today <= Date.UTC(1899, 11, 30) + Number(serial) * MS_PER_DAY; + return end === undefined || today <= fromSerialDate(end); }; const serializeSorted = (data: Record): string => From 4602de18b1a09afa5a42fd8294eaf92359aee220 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 12:12:38 -0300 Subject: [PATCH 3/6] fix(ibs-cbs): bound the whole workbook, not one entry at a time The per entry inflate cap left the total unbounded: a zip central directory can list thousands of entries, so an archive whose entries each stay under the cap could still expand to any size. Reading stops once the entries kept hold more than 128 MB together, which is two orders of magnitude above the workbook the portal publishes. Verified with an archive of four 60 MB worksheets, each inside the per entry cap: it is rejected instead of holding 240 MB, and the official workbook still reads its two sheets. Part of #541 --- scripts/ibs-cbs.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/scripts/ibs-cbs.ts b/scripts/ibs-cbs.ts index 5e09b35e..b0ae4b55 100644 --- a/scripts/ibs-cbs.ts +++ b/scripts/ibs-cbs.ts @@ -128,15 +128,19 @@ const CENTRAL_DIRECTORY_ENTRY = 0x02_01_4b_50; const DEFLATE = 8; /** - * How much one entry may inflate to. The whole workbook is under 1 MB, so this leaves room for - * the table to grow while a crafted or corrupted download cannot expand without a bound. + * How much one entry may inflate to, and how much the accepted entries may hold together. The + * whole workbook is under 1 MB, so both leave room for the table to grow while a crafted or + * corrupted download cannot expand without a bound, whether it does so in one entry or across + * the thousands of entries a central directory can list. */ const MAXIMUM_ENTRY_SIZE = 64 * 1024 * 1024; +const MAXIMUM_WORKBOOK_SIZE = 128 * 1024 * 1024; /** * Reads the entries of a zip archive (an xlsx workbook is one) that `isWanted` accepts, through - * its central directory. Nothing else is decompressed, and inflating stops at - * `MAXIMUM_ENTRY_SIZE` per entry, which throws `ERR_BUFFER_TOO_LARGE`. + * its central directory. Nothing else is decompressed, inflating stops at `MAXIMUM_ENTRY_SIZE` + * per entry, which throws `ERR_BUFFER_TOO_LARGE`, and the entries kept stop at + * `MAXIMUM_WORKBOOK_SIZE` together. * @param {Buffer} zip - The archive. * @param {(name: string) => boolean} isWanted - Whether an entry path is one to read. * @returns {Map} The UTF-8 content of the accepted entries, by path. @@ -150,6 +154,7 @@ const unzip = (zip: Buffer, isWanted: (name: string) => boolean): Map(); let offset = zip.readUInt32LE(end + 16); + let total = 0; for (let index = 0; index < zip.readUInt16LE(end + 10); index += 1) { if (zip.readUInt32LE(offset) !== CENTRAL_DIRECTORY_ENTRY) { @@ -168,6 +173,14 @@ const unzip = (zip: Buffer, isWanted: (name: string) => boolean): Map MAXIMUM_WORKBOOK_SIZE) { + throw new Error( + `The entries read out of the downloaded table hold more than ${MAXIMUM_WORKBOOK_SIZE} bytes together; the archive is not the workbook`, + ); + } + files.set(name, content.toString("utf8")); } From 5aaf2e0cdee0deda8d548a4309f78b20b27c4135 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:36:50 -0300 Subject: [PATCH 4/6] refactor(ibs-cbs): read the workbook through the shared xlsx reader The generator carried its own zip and xlsx reader, a second copy of `scripts/read-xlsx-sheet.ts` from #569, and jscpd fails once both are on main. It now reads every sheet with `readXlsxSheets`, which keeps what this copy guarded against (the zip signatures, the inflate cap per file and for the whole workbook, the numeric character references, the phonetic runs of a shared string), turns each sheet into records keyed by its header row the way it did before, decodes the portal listing with the shared `decodeXml` and emits the tables with `serializeRecord`, whose values may now be any JSON value. Regenerating the tables writes `src/_internals/constants/ibs-cbs.ts` byte for byte. --- scripts/ibs-cbs.ts | 164 ++++-------------------------------- scripts/serialize-record.ts | 4 +- 2 files changed, 19 insertions(+), 149 deletions(-) diff --git a/scripts/ibs-cbs.ts b/scripts/ibs-cbs.ts index b0ae4b55..e390ac19 100644 --- a/scripts/ibs-cbs.ts +++ b/scripts/ibs-cbs.ts @@ -2,9 +2,11 @@ import { writeFile } from "node:fs/promises"; import { resolve } from "node:path"; -import { inflateRawSync } from "node:zlib"; import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; +import { decodeXml } from "./decode-xml.ts"; +import { readXlsxSheets } from "./read-xlsx-sheet.ts"; +import { serializeRecord } from "./serialize-record.ts"; const scriptsDir = import.meta.dirname; @@ -53,21 +55,6 @@ const CLASS_TRIB_DESCRIPTION_HEADER = "Descrição cClassTrib"; const START_OF_VALIDITY_HEADER = "dIniVig"; const END_OF_VALIDITY_HEADER = "dFimVig"; -const XML_ENTITIES: Record = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }; - -const decodeXml = (text: string): string => - text.replaceAll( - /&(?:#(\d+)|#x([\da-f]+)|(\w+));/gi, - (entity: string, ...groups: (string | undefined)[]) => { - const [decimal, hex, name = ""] = groups; - - if (decimal !== undefined) return String.fromCodePoint(Number(decimal)); - if (hex !== undefined) return String.fromCodePoint(Number.parseInt(hex, 16)); - - return XML_ENTITIES[name] ?? entity; - }, - ); - const normalizeText = (text: string): string => text.replaceAll(/\s+/g, " ").trim(); type Listing = { @@ -123,144 +110,30 @@ const fetchLatestListing = async (listingUrl: string, titleRegex: RegExp): Promi return latest; }; -const END_OF_CENTRAL_DIRECTORY = 0x06_05_4b_50; -const CENTRAL_DIRECTORY_ENTRY = 0x02_01_4b_50; -const DEFLATE = 8; - -/** - * How much one entry may inflate to, and how much the accepted entries may hold together. The - * whole workbook is under 1 MB, so both leave room for the table to grow while a crafted or - * corrupted download cannot expand without a bound, whether it does so in one entry or across - * the thousands of entries a central directory can list. - */ -const MAXIMUM_ENTRY_SIZE = 64 * 1024 * 1024; -const MAXIMUM_WORKBOOK_SIZE = 128 * 1024 * 1024; - -/** - * Reads the entries of a zip archive (an xlsx workbook is one) that `isWanted` accepts, through - * its central directory. Nothing else is decompressed, inflating stops at `MAXIMUM_ENTRY_SIZE` - * per entry, which throws `ERR_BUFFER_TOO_LARGE`, and the entries kept stop at - * `MAXIMUM_WORKBOOK_SIZE` together. - * @param {Buffer} zip - The archive. - * @param {(name: string) => boolean} isWanted - Whether an entry path is one to read. - * @returns {Map} The UTF-8 content of the accepted entries, by path. - */ -const unzip = (zip: Buffer, isWanted: (name: string) => boolean): Map => { - let end = zip.length - 22; - - while (end >= 0 && zip.readUInt32LE(end) !== END_OF_CENTRAL_DIRECTORY) end -= 1; - - if (end < 0) throw new Error("The downloaded table is not a zip archive (xlsx)"); - - const files = new Map(); - let offset = zip.readUInt32LE(end + 16); - let total = 0; - - for (let index = 0; index < zip.readUInt16LE(end + 10); index += 1) { - if (zip.readUInt32LE(offset) !== CENTRAL_DIRECTORY_ENTRY) { - throw new Error("The downloaded table has a broken zip central directory"); - } - - const nameLength = zip.readUInt16LE(offset + 28); - const name = zip.toString("utf8", offset + 46, offset + 46 + nameLength); - - if (isWanted(name)) { - const method = zip.readUInt16LE(offset + 10); - const compressedSize = zip.readUInt32LE(offset + 20); - const local = zip.readUInt32LE(offset + 42); - const start = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28); - const data = zip.subarray(start, start + compressedSize); - const content = - method === DEFLATE ? inflateRawSync(data, { maxOutputLength: MAXIMUM_ENTRY_SIZE }) : data; - - total += content.length; - - if (total > MAXIMUM_WORKBOOK_SIZE) { - throw new Error( - `The entries read out of the downloaded table hold more than ${MAXIMUM_WORKBOOK_SIZE} bytes together; the archive is not the workbook`, - ); - } - - files.set(name, content.toString("utf8")); - } - - offset += 46 + nameLength + zip.readUInt16LE(offset + 30) + zip.readUInt16LE(offset + 32); - } - - return files; -}; - type Row = Record; -const SHARED_STRING_REGEX = /([\s\S]*?)<\/si>/g; -const TEXT_RUN_REGEX = /]*>([\s\S]*?)<\/t>/g; -const PHONETIC_RUN_REGEX = //g; -const ROW_REGEX = /]*>([\s\S]*?)<\/row>/g; -const CELL_REGEX = /]*?)(?:\/>|>([\s\S]*?)<\/c>)/g; -const CELL_VALUE_REGEX = /([\s\S]*?)<\/v>/; - /** - * Reads a worksheet into rows keyed by the text of the header row (the first one). - * @param {string} sheet - The worksheet XML. - * @param {string[]} sharedStrings - The workbook's shared strings. - * @returns {Row[]} One record per data row, empty cells left out. + * Turns the rows of a worksheet into records keyed by the text of its header row (the first + * one). Every cell is whitespace-normalised and an empty one is left out. + * @param {string[][]} rows - The rows of the worksheet. + * @returns {Row[]} One record per data row. */ -const readSheet = (sheet: string, sharedStrings: string[]): Row[] => { - const rows = [...sheet.matchAll(ROW_REGEX)].map(([, row = ""]) => { - const cells: Row = {}; - - for (const [, column = "", attributes = "", body = ""] of row.matchAll(CELL_REGEX)) { - const raw = CELL_VALUE_REGEX.exec(body)?.[1]; - - if (raw === undefined) continue; - - const value = attributes.includes('t="s"') ? sharedStrings[Number(raw)] : decodeXml(raw); - - cells[column] = normalizeText(value ?? ""); - } - - return cells; - }); - - const [header = {}, ...body] = rows; +const toRecords = (rows: string[][]): Row[] => { + const [header = [], ...body] = rows.map((cells) => cells.map((cell) => normalizeText(cell))); return body.map((cells) => { const row: Row = {}; - for (const [column, value] of Object.entries(cells)) { + for (const [column, value] of cells.entries()) { const key = header[column]; - if (key !== undefined && value !== "") row[key] = value; + if (key !== undefined && key !== "" && value !== "") row[key] = value; } return row; }); }; -const SHARED_STRINGS_PATH = "xl/sharedStrings.xml"; - -const isWorksheet = (name: string): boolean => - name.startsWith("xl/worksheets/") && name.endsWith(".xml"); - -/** - * Reads every worksheet of the workbook. The `rPh` elements of a shared string hold the phonetic - * hint of the text, not the text, so they are dropped before the runs are joined. - * @param {Buffer} xlsx - The workbook. - * @returns {Row[][]} The rows of each worksheet. - */ -const readWorkbook = (xlsx: Buffer): Row[][] => { - const files = unzip(xlsx, (name) => name === SHARED_STRINGS_PATH || isWorksheet(name)); - const sharedStrings = [...(files.get(SHARED_STRINGS_PATH) ?? "").matchAll(SHARED_STRING_REGEX)] - .map(([, item = ""]) => item.replaceAll(PHONETIC_RUN_REGEX, "")) - .map((item) => - decodeXml([...item.matchAll(TEXT_RUN_REGEX)].map(([, text = ""]) => text).join("")), - ); - - return [...files] - .filter(([name]) => isWorksheet(name)) - .map(([, sheet]) => readSheet(sheet, sharedStrings)); -}; - const MS_PER_DAY = 86_400_000; const fromSerialDate = (serial: string): number => @@ -285,12 +158,6 @@ const isInForce = (row: Row, today: number): boolean => { return end === undefined || today <= fromSerialDate(end); }; -const serializeSorted = (data: Record): string => - `{${Object.keys(data) - .sort() - .map((key) => `${JSON.stringify(key)}:${JSON.stringify(data[key])}`) - .join(",")}}`; - type Tables = { /** CST description by 3 digit code. */ csts: Record; @@ -370,7 +237,10 @@ const main = async (): Promise => { const now = new Date(); const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); const workbook = Buffer.from(await response.arrayBuffer()); - const { csts, classifications } = buildTables(readWorkbook(workbook).flat(), today); + const { csts, classifications } = buildTables( + [...readXlsxSheets(workbook).values()].flatMap((rows) => toRecords(rows)), + today, + ); const codes = Object.keys(classifications).sort(); if (Object.keys(csts).length < MINIMUM_CST_CODES || codes.length < MINIMUM_CLASSIFICATIONS) { @@ -403,7 +273,7 @@ const main = async (): Promise => { * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm * Lei Complementar nº 214/2025, which institutes the IBS and the CBS. */ -export const CST_IBS_CBS_TABLE: Record = ${serializeSorted(csts)}; +export const CST_IBS_CBS_TABLE: Record = ${serializeRecord(csts)}; /** * cClassTrib (Código de Classificação Tributária do IBS e da CBS) codes in force on the @@ -421,7 +291,7 @@ export const CLASS_TRIB_CODES: readonly string[] = ${JSON.stringify(codes)}; * situation the classification refers to). The legal wording columns ("LC Redação", * "Regulamento CBS", "Regulamento IBS") are not shipped. */ -export const CLASS_TRIB_TABLE: Record = ${serializeSorted(classifications)}; +export const CLASS_TRIB_TABLE: Record = ${serializeRecord(classifications)}; /** Shape a CST-IBS/CBS has to be written in: the 3 digits of the field \`CST\` (UB13, N 3). */ export const CST_IBS_CBS_FORMAT_REGEX = /^\\d{3}$/; diff --git a/scripts/serialize-record.ts b/scripts/serialize-record.ts index 55fdb404..4c8e31a1 100644 --- a/scripts/serialize-record.ts +++ b/scripts/serialize-record.ts @@ -5,10 +5,10 @@ * from sorted keys is not always emitted sorted. Writing the entries out in order keeps the * generated tables readable and their diffs small. * - * @param {Record} data - The entries to serialize. + * @param {Record} data - The entries to serialize. * @returns {string} The object literal, sorted by key. */ -export const serializeRecord = (data: Record): string => +export const serializeRecord = (data: Record): string => `{${Object.keys(data) .sort() .map((key) => `${JSON.stringify(key)}:${JSON.stringify(data[key])}`) From ba2d1d8503156b8d706db0eaafbf18dff9af8143 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:58:59 -0300 Subject: [PATCH 5/6] docs(ibs-cbs): say that the cst option belongs to isValidClassTrib only "Same table and input rules as isValidClassTrib" under getClassTrib could be read as the lookup taking the `cst` option too. It does not: the entry it returns already carries the CST of the code as `cst`. The sentence now says it shares the table and the rules for reading the code, and that the option exists only on `isValidClassTrib`, in both languages and in `docs/llms-full.txt`. --- docs/pt-br/utilities.md | 3 ++- docs/utilities.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 6dbf830e..cc9dfbcd 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -3116,7 +3116,8 @@ isValidClassTrib('c200001'); // false (não é uma forma documentada) Busca um cClassTrib e retorna a sua classificação oficial. O resultado é um registro `ClassTrib`: `{ code, cst, name, description }`. -- Valem a mesma tabela e as mesmas regras de entrada de `isValidClassTrib`. Retorna `null` quando o código é desconhecido ou o valor não está em uma forma documentada. +- Valem a mesma tabela e as mesmas regras de leitura do código de `isValidClassTrib`. Retorna `null` quando o código é desconhecido ou o valor não está em uma forma documentada. +- A opção `cst` existe só em `isValidClassTrib`, já que o registro retornado aqui já traz o seu CST em `cst`. - `cst` é o CST-IBS/CBS a que a classificação pertence, os 3 primeiros dígitos do seu código; `name` é o nome reduzido que a tabela oficial dá para apresentação (a coluna "Nome cClassTrib") e `description` a situação a que se refere (a coluna "Descrição cClassTrib"). - A redação legal que a planilha também traz em cada linha (o artigo da Lei Complementar nº 214/2025 e dos dois regulamentos) não é distribuída. diff --git a/docs/utilities.md b/docs/utilities.md index d386120d..6c7298d3 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -3116,7 +3116,8 @@ isValidClassTrib('c200001'); // false (not a documented form) Look a cClassTrib up and get its official classification. The result is a `ClassTrib` record: `{ code, cst, name, description }`. -- Same table and input rules as `isValidClassTrib`. Returns `null` when the code is unknown or the value is not in a documented form. +- Same table and the same rules for reading the code as `isValidClassTrib`. Returns `null` when the code is unknown or the value is not in a documented form. +- The `cst` option exists only on `isValidClassTrib`, since the entry returned here already carries its CST as `cst`. - `cst` is the CST-IBS/CBS the classification belongs to, the first 3 digits of its code; `name` is the short name the official table gives for display (the column "Nome cClassTrib") and `description` the situation it refers to (the column "Descrição cClassTrib"). - The legal wording the workbook also prints for each row (the article of Lei Complementar nº 214/2025 and of both regulations) is not shipped. From 00ea76fb6fc0a778bc7f88d045ad84515a5194ed Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:14:06 -0300 Subject: [PATCH 6/6] fix(ibs-cbs): fail on a validity date the workbook does not write as a number dIniVig and dFimVig were read with Number and used straight away. NaN compares false against anything, so a malformed dIniVig let a classification that is not in force yet into the table and a malformed dFimVig dropped a classification in force out of it, silently in both directions: the minimum-count guard only counts the rows that survive, so a handful of them can go missing under it. A cell that does not read as a finite serial date now throws and names the column, the way every other guard of this generator stops the datasets workflow on a workbook that changed shape. --- scripts/ibs-cbs.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/scripts/ibs-cbs.ts b/scripts/ibs-cbs.ts index e390ac19..3ceed81a 100644 --- a/scripts/ibs-cbs.ts +++ b/scripts/ibs-cbs.ts @@ -136,8 +136,21 @@ const toRecords = (rows: string[][]): Row[] => { const MS_PER_DAY = 86_400_000; -const fromSerialDate = (serial: string): number => - Date.UTC(1899, 11, 30) + Number(serial) * MS_PER_DAY; +/** + * Reads an Excel serial date (days since 30/12/1899) as milliseconds at UTC midnight. + * @param {string} serial - The cell as the sheet writes it. + * @param {string} header - The column the cell comes from, for the error message. + * @returns {number} The date in milliseconds. + * @throws {Error} When the cell is not a number, so a malformed date is never read as an open + * end of the validity window. + */ +const fromSerialDate = (serial: string, header: string): number => { + const days = Number(serial); + + if (!Number.isFinite(days)) throw new Error(`${header} is not a serial date: "${serial}"`); + + return Date.UTC(1899, 11, 30) + days * MS_PER_DAY; +}; /** * Whether a row is in force on `today`. `dIniVig` and `dFimVig` are Excel serial dates (days @@ -145,17 +158,21 @@ const fromSerialDate = (serial: string): number => * Siscomex window. A classification the Informe Técnico excludes is not deleted from the table, * it gets a `dFimVig` (220001, 220002 and 220003 in v.1.60), so this is what keeps it out, and a * classification published before it starts to apply stays out until its `dIniVig`. + * + * A date that does not read as a serial number throws instead of being ignored: a malformed + * `dFimVig` would otherwise drop the row from the table without a word. * @param {Row} row - A classification row. * @param {number} today - The reference date, in milliseconds at UTC midnight. * @returns {boolean} True when `today` is inside the validity window the row declares. + * @throws {Error} When either end of the window is not a serial date. */ const isInForce = (row: Row, today: number): boolean => { const start = row[START_OF_VALIDITY_HEADER]; const end = row[END_OF_VALIDITY_HEADER]; - if (start !== undefined && today < fromSerialDate(start)) return false; + if (start !== undefined && today < fromSerialDate(start, START_OF_VALIDITY_HEADER)) return false; - return end === undefined || today <= fromSerialDate(end); + return end === undefined || today <= fromSerialDate(end, END_OF_VALIDITY_HEADER); }; type Tables = {