From 12b30b6c2cfc6ebdd73797a123fd7804e824364a Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Sat, 19 Sep 2026 11:12:35 -0300
Subject: [PATCH 01/12] feat(nfse): add the NBS and LC 116/2003 service list
lookups
The national NFS-e identifies a service by an NBS code (cNBS) and by a
subitem of the list annexed to the Lei Complementar 116/2003 (the first four
digits of cTribNac), and the package had no lookup for either.
isValidNbs, getNbs and formatNbs read the NBS 2.0 table the MDIC publishes as
a CSV: 920 complete codes of 9 digits, printed as N.NNNN.NN.NN, as the Anexo I
of the Portaria Conjunta RFB/SCS 2.000/2018 forms them. The headings of the
nomenclature are left out, since they classify nothing by themselves.
isValidServiceItem and getServiceItem read the 200 subitems in force of the
law from the ANEXO B of the Sistema Nacional NFS-e, the only machine readable
official form of the list: vetoed subitems, the national splits and item 99
of the national list are not part of the law and are left out. The workbook
is an xlsx, so the generator comes with a small reader built on node:zlib
instead of a new dependency, and finds the versioned file name on the
documentation page so a new version is picked up by the datasets workflow.
---
CONTRIBUTING.md | 6 +-
docs/getting-started.md | 2 +
docs/pt-br/getting-started.md | 2 +
docs/pt-br/utilities.md | 93 ++
docs/utilities.md | 93 ++
jsr.json | 5 +
reports/api/brazilian-utils.api.md | 27 +
scripts/data.ts | 4 +
scripts/nbs.ts | 105 ++
scripts/read-xlsx-sheet.ts | 121 ++
scripts/service-items.ts | 127 ++
src/_internals/constants/nbs.ts | 1207 +++++++++++++++++
src/_internals/constants/service-items.ts | 315 +++++
src/format-nbs/format-nbs.test.ts | 80 ++
src/format-nbs/format-nbs.ts | 31 +
src/get-nbs/get-nbs.test.ts | 148 ++
src/get-nbs/get-nbs.ts | 67 +
src/get-service-item/get-service-item.test.ts | 150 ++
src/get-service-item/get-service-item.ts | 72 +
src/index.test.ts | 9 +
src/index.ts | 5 +
src/is-valid-nbs/is-valid-nbs.test.ts | 63 +
src/is-valid-nbs/is-valid-nbs.ts | 35 +
.../is-valid-service-item.test.ts | 61 +
.../is-valid-service-item.ts | 31 +
25 files changed, 2856 insertions(+), 3 deletions(-)
create mode 100644 scripts/nbs.ts
create mode 100644 scripts/read-xlsx-sheet.ts
create mode 100644 scripts/service-items.ts
create mode 100644 src/_internals/constants/nbs.ts
create mode 100644 src/_internals/constants/service-items.ts
create mode 100644 src/format-nbs/format-nbs.test.ts
create mode 100644 src/format-nbs/format-nbs.ts
create mode 100644 src/get-nbs/get-nbs.test.ts
create mode 100644 src/get-nbs/get-nbs.ts
create mode 100644 src/get-service-item/get-service-item.test.ts
create mode 100644 src/get-service-item/get-service-item.ts
create mode 100644 src/is-valid-nbs/is-valid-nbs.test.ts
create mode 100644 src/is-valid-nbs/is-valid-nbs.ts
create mode 100644 src/is-valid-service-item/is-valid-service-item.test.ts
create mode 100644 src/is-valid-service-item/is-valid-service-item.ts
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index afc3fbe8e..c098bce3f 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, 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),
+ 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/docs/getting-started.md b/docs/getting-started.md
index 3dbe79b89..853c578b5 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -73,8 +73,10 @@ A few utils embed an official dataset and weigh far more than everything else co
| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 114.2 KB | 24.6 KB |
| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 119.1 KB | 30.6 KB |
| `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 |
| `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 |
The root of the package is a single ESM module, so a bundler cannot split one of these datasets out of it: importing a heavy util from the root puts its whole dataset in your main bundle, and a dynamic `import()` of the root does not help. To lazy-load one, import it from its own subpath:
diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md
index 86e5f018f..a3543b4a7 100644
--- a/docs/pt-br/getting-started.md
+++ b/docs/pt-br/getting-started.md
@@ -73,8 +73,10 @@ Alguns utilitários embutem uma base de dados oficial e pesam muito mais que tod
| `isValidNcm` | códigos NCM (Nomenclatura Comum do Mercosul) | 114,2 KB | 24,6 KB |
| `isValidCbo` · `getCbo` | títulos das ocupações da CBO 2002 | 119,1 KB | 30,6 KB |
| `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 |
| `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 |
A raiz do pacote é um único módulo ESM, então o bundler não consegue separar uma dessas bases de dados dele: importar um utilitário pesado da raiz coloca a base inteira no seu bundle principal, e um `import()` dinâmico da raiz não ajuda. Para carregar sob demanda, importe do subpath próprio:
diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md
index dd178e170..7ac470f91 100644
--- a/docs/pt-br/utilities.md
+++ b/docs/pt-br/utilities.md
@@ -2791,6 +2791,99 @@ parseNcm('8471.30.12'); // '84713012'
parseNcm('8471'); // '8471' (um código parcial é mantido como está)
```
+### isValidNbs
+
+Valida um código NBS (Nomenclatura Brasileira de Serviços, Intangíveis e Outras Operações que Produzam Variações no Patrimônio) contra a tabela oficial da NBS 2.0, o código que a NFS-e nacional leva em `cNBS`.
+
+- O código tem 9 dígitos, impressos como `N.NNNN.NN.NN`: o algarismo 1, o capítulo, a posição, os dois níveis de subposição, o item e o subitem.
+- Aceita uma string com os 9 dígitos ou com a máscara, com um único separador entre os grupos e espaços opcionais nas extremidades, ou um inteiro seguro não negativo. Qualquer outra string é rejeitada em vez de ter os dígitos pinçados.
+- Só códigos completos são válidos: os títulos de capítulo (`1.01`), posição (`1.0101`) e subposição (`1.0101.1`) não classificam nada por si sós.
+- O ANEXO B do Sistema Nacional NFS-e lista os mesmos 920 códigos menos três (`1.0402.29.00`, `1.0403.29.00` e `1.0904.40.00`), então um código válido aqui ainda pode ser recusado pela NFS-e.
+
+```javascript
+import { isValidNbs } from '@brazilian-utils/brazilian-utils';
+
+isValidNbs('1.0101.11.00'); // true
+isValidNbs('101011100'); // true
+isValidNbs(101011100); // true
+isValidNbs('1.0101'); // false (título de posição, não um código completo)
+isValidNbs('1.9999.99.99'); // false
+isValidNbs('1.0101abc11.00'); // false (não é uma forma documentada)
+```
+
+### formatNbs
+
+Formata um código NBS (Nomenclatura Brasileira de Serviços) na máscara `N.NNNN.NN.NN` em que a nomenclatura o imprime. Só a estrutura muda; use `isValidNbs` para conferir um código com a tabela.
+
+- Todo código NBS começa com 1, então, diferente do `formatNcm`, não há opção `pad`.
+- No resto, mesmas regras de `formatCnae`: a máscara é aplicada até onde o valor vai, os caracteres fora dela são descartados e um número é lido como a string dos seus dígitos.
+
+```javascript
+import { formatNbs } from '@brazilian-utils/brazilian-utils';
+
+formatNbs('101011100'); // 1.0101.11.00
+formatNbs(101011100); // 1.0101.11.00
+formatNbs('10101'); // 1.0101 (mascarado até onde vai)
+formatNbs('abc101011100'); // 1.0101.11.00 (só os dígitos são lidos)
+```
+
+### getNbs
+
+Consulta um código NBS (Nomenclatura Brasileira de Serviços) e retorna a sua descrição oficial. O resultado é um registro `Nbs`: `{ code, description }`.
+
+- Mesmas regras de `isValidNbs`. `code` são os 9 dígitos, sem a máscara. Retorna `null` quando o código é desconhecido ou o valor não está em uma forma documentada.
+
+```javascript
+import { getNbs } from '@brazilian-utils/brazilian-utils';
+
+getNbs('1.0101.11.00');
+// { code: '101011100', description: 'Serviços de construção de edificações residenciais de um e dois pavimentos' }
+
+getNbs(126050000); // { code: '126050000', description: 'Serviços domésticos' }
+getNbs('1.0101'); // null (título de posição, não um código completo)
+getNbs('1.9999.99.99'); // null
+```
+
+Fonte: [tabela da NBS 2.0 publicada pelo MDIC](https://www.gov.br/mdic/pt-br/assuntos/sdic/comercio-e-servicos/nbs-nomenclatura-brasileira-de-servicos), aprovada pela Portaria Conjunta RFB/SCS 1.429/2018 e alterada pela Portaria Conjunta RFB/SCS 2.000/2018.
+
+### isValidServiceItem
+
+Verifica se um valor é um subitem em vigor da lista de serviços anexa à Lei Complementar 116/2003, a lista dos serviços sobre os quais incide o ISS.
+
+- A lei numera o subitem como o item, um ponto e dois dígitos, de `1.01` a `40.01`.
+- Aceita essa forma, o item preenchido com zero (`'01.01'`) ou os dígitos puros (`'0101'`, `'101'` ou o inteiro `101`), que são os quatro primeiros dígitos do código `cTribNac` da NFS-e nacional, com espaços opcionais nas extremidades.
+- Um número só é lido quando é um inteiro seguro não negativo, então o decimal `1.01` é rejeitado: escreva a forma com ponto como string.
+- Os subitens vetados (`3.01`, `7.14`, `7.15`, `13.01` e `17.07`), os títulos de item, os códigos nacionais de 6 dígitos em que um subitem se desdobra e o item 99 da lista nacional, que não faz parte da lei, não são válidos. Códigos municipais de serviço estão fora do escopo.
+
+```javascript
+import { isValidServiceItem } from '@brazilian-utils/brazilian-utils';
+
+isValidServiceItem('1.01'); // true
+isValidServiceItem('01.01'); // true
+isValidServiceItem('0101'); // true
+isValidServiceItem(101); // true
+isValidServiceItem('3.01'); // false (vetado)
+isValidServiceItem('99.01'); // false (só da lista nacional, não da lei)
+isValidServiceItem(1.01); // false (não é um inteiro seguro não negativo)
+```
+
+### getServiceItem
+
+Consulta um subitem da lista de serviços anexa à Lei Complementar 116/2003 e retorna a sua descrição oficial. O resultado é um registro `ServiceItem`: `{ code, description }`.
+
+- Mesmas regras de `isValidServiceItem`. `code` é a forma em que a lei o imprime (`'1.01'`). Retorna `null` quando o subitem é desconhecido ou o valor não está em uma forma documentada.
+
+```javascript
+import { getServiceItem } from '@brazilian-utils/brazilian-utils';
+
+getServiceItem('1.01'); // { code: '1.01', description: 'Análise e desenvolvimento de sistemas.' }
+getServiceItem('0101'); // { code: '1.01', description: 'Análise e desenvolvimento de sistemas.' }
+getServiceItem('40.01'); // { code: '40.01', description: 'Obras de arte sob encomenda.' }
+getServiceItem('3.01'); // null (vetado)
+```
+
+Fonte: [Lei Complementar 116/2003](https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp116.htm) e a planilha `LISTA.SERV.NAC.` do [ANEXO B do Sistema Nacional NFS-e](https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual), a lista em vigor em formato legível por máquina.
+
### isValidCfop
Valida um código CFOP (Código Fiscal de Operações e Prestações) contra a tabela oficial, o Anexo II consolidado do Convênio SINIEF s/nº 1970 em vigor.
diff --git a/docs/utilities.md b/docs/utilities.md
index 33281df41..68fcb0884 100644
--- a/docs/utilities.md
+++ b/docs/utilities.md
@@ -2791,6 +2791,99 @@ parseNcm('8471.30.12'); // '84713012'
parseNcm('8471'); // '8471' (a partial code is kept as written)
```
+### isValidNbs
+
+Check if an NBS (Nomenclatura Brasileira de Serviços, Intangíveis e Outras Operações que Produzam Variações no Patrimônio) code exists in the official NBS 2.0 table, the code the national NFS-e carries in `cNBS`.
+
+- A code has 9 digits, printed as `N.NNNN.NN.NN`: the digit 1, the chapter, the position, the two subposition levels, the item and the subitem.
+- Accepts a string with the 9 digits or with the mask, with a single separator between the groups and optional surrounding whitespace, or a non-negative safe integer. Any other string is rejected instead of having its digits picked out.
+- Only complete codes are valid: the chapter (`1.01`), position (`1.0101`) and subposition (`1.0101.1`) headings classify nothing by themselves.
+- The ANEXO B of the Sistema Nacional NFS-e lists the same 920 codes except three (`1.0402.29.00`, `1.0403.29.00` and `1.0904.40.00`), so a code valid here can still be refused by the NFS-e.
+
+```javascript
+import { isValidNbs } from '@brazilian-utils/brazilian-utils';
+
+isValidNbs('1.0101.11.00'); // true
+isValidNbs('101011100'); // true
+isValidNbs(101011100); // true
+isValidNbs('1.0101'); // false (a position heading, not a complete code)
+isValidNbs('1.9999.99.99'); // false
+isValidNbs('1.0101abc11.00'); // false (not a documented form)
+```
+
+### formatNbs
+
+Format an NBS (Nomenclatura Brasileira de Serviços) code into the `N.NNNN.NN.NN` mask the nomenclature prints. Only the structure changes; use `isValidNbs` to check a code against the table.
+
+- Every NBS code starts with 1, so, unlike `formatNcm`, there is no `pad` option.
+- Same rules as `formatCnae` otherwise: the mask is applied as far as the value goes, characters outside it are dropped, and a number is read as the string of its digits.
+
+```javascript
+import { formatNbs } from '@brazilian-utils/brazilian-utils';
+
+formatNbs('101011100'); // 1.0101.11.00
+formatNbs(101011100); // 1.0101.11.00
+formatNbs('10101'); // 1.0101 (masked as far as it goes)
+formatNbs('abc101011100'); // 1.0101.11.00 (only the digits are read)
+```
+
+### getNbs
+
+Look an NBS (Nomenclatura Brasileira de Serviços) code up and get its official description. The result is an `Nbs` record: `{ code, description }`.
+
+- Same rules as `isValidNbs`. `code` is the 9 digits, without the mask. Returns `null` when the code is unknown or the value is not in a documented form.
+
+```javascript
+import { getNbs } from '@brazilian-utils/brazilian-utils';
+
+getNbs('1.0101.11.00');
+// { code: '101011100', description: 'Serviços de construção de edificações residenciais de um e dois pavimentos' }
+
+getNbs(126050000); // { code: '126050000', description: 'Serviços domésticos' }
+getNbs('1.0101'); // null (a position heading, not a complete code)
+getNbs('1.9999.99.99'); // null
+```
+
+Source: [NBS 2.0 table published by the MDIC](https://www.gov.br/mdic/pt-br/assuntos/sdic/comercio-e-servicos/nbs-nomenclatura-brasileira-de-servicos), approved by the Portaria Conjunta RFB/SCS 1.429/2018 and amended by the Portaria Conjunta RFB/SCS 2.000/2018.
+
+### isValidServiceItem
+
+Check if a value is a subitem in force of the service list annexed to the Lei Complementar 116/2003, the list of the services the ISS is levied on.
+
+- The law numbers a subitem as the item, a dot and two digits, `1.01` to `40.01`.
+- Accepts that form, a zero padded item (`'01.01'`) or the bare digits (`'0101'`, `'101'` or the integer `101`), which are the first four digits of the `cTribNac` code of the national NFS-e, with optional surrounding whitespace.
+- A number is only read when it is a non-negative safe integer, so the float `1.01` is rejected: write the dotted form as a string.
+- The vetoed subitems (`3.01`, `7.14`, `7.15`, `13.01` and `17.07`), the item headings, the 6 digit national codes a subitem is split into and item 99 of the national list, which is not part of the law, are not valid. Municipal service codes are out of scope.
+
+```javascript
+import { isValidServiceItem } from '@brazilian-utils/brazilian-utils';
+
+isValidServiceItem('1.01'); // true
+isValidServiceItem('01.01'); // true
+isValidServiceItem('0101'); // true
+isValidServiceItem(101); // true
+isValidServiceItem('3.01'); // false (vetoed)
+isValidServiceItem('99.01'); // false (national list only, not the law)
+isValidServiceItem(1.01); // false (not a non-negative safe integer)
+```
+
+### getServiceItem
+
+Look a subitem of the service list annexed to the Lei Complementar 116/2003 up and get its official description. The result is a `ServiceItem` record: `{ code, description }`.
+
+- Same rules as `isValidServiceItem`. `code` is the form the law prints (`'1.01'`). Returns `null` when the subitem is unknown or the value is not in a documented form.
+
+```javascript
+import { getServiceItem } from '@brazilian-utils/brazilian-utils';
+
+getServiceItem('1.01'); // { code: '1.01', description: 'Análise e desenvolvimento de sistemas.' }
+getServiceItem('0101'); // { code: '1.01', description: 'Análise e desenvolvimento de sistemas.' }
+getServiceItem('40.01'); // { code: '40.01', description: 'Obras de arte sob encomenda.' }
+getServiceItem('3.01'); // null (vetoed)
+```
+
+Source: [Lei Complementar 116/2003](https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp116.htm) and the sheet `LISTA.SERV.NAC.` of the [ANEXO B of the Sistema Nacional NFS-e](https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual), the list in force in machine readable form.
+
### isValidCfop
Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table, the consolidated Anexo II of Convênio SINIEF s/nº 1970 in force.
diff --git a/jsr.json b/jsr.json
index a1cf29431..ef7ead8d8 100644
--- a/jsr.json
+++ b/jsr.json
@@ -26,6 +26,7 @@
"./format-iban": "./src/format-iban/format-iban.ts",
"./format-legal-nature": "./src/format-legal-nature/format-legal-nature.ts",
"./format-license-plate": "./src/format-license-plate/format-license-plate.ts",
+ "./format-nbs": "./src/format-nbs/format-nbs.ts",
"./format-ncm": "./src/format-ncm/format-ncm.ts",
"./format-nfe-key": "./src/format-nfe-key/format-nfe-key.ts",
"./format-passport": "./src/format-passport/format-passport.ts",
@@ -74,10 +75,12 @@
"./get-municipalities": "./src/get-municipalities/get-municipalities.ts",
"./get-municipality": "./src/get-municipality/get-municipality.ts",
"./get-municipality-by-code": "./src/get-municipality-by-code/get-municipality-by-code.ts",
+ "./get-nbs": "./src/get-nbs/get-nbs.ts",
"./get-nfe-key-info": "./src/get-nfe-key-info/get-nfe-key-info.ts",
"./get-nfse-key-info": "./src/get-nfse-key-info/get-nfse-key-info.ts",
"./get-pix-key-info": "./src/get-pix-key-info/get-pix-key-info.ts",
"./get-pix-payload-info": "./src/get-pix-payload-info/get-pix-payload-info.ts",
+ "./get-service-item": "./src/get-service-item/get-service-item.ts",
"./get-state-by-cep": "./src/get-state-by-cep/get-state-by-cep.ts",
"./get-state-by-ibge-code": "./src/get-state-by-ibge-code/get-state-by-ibge-code.ts",
"./get-state-code-by-name": "./src/get-state-code-by-name/get-state-code-by-name.ts",
@@ -111,6 +114,7 @@
"./is-valid-legal-nature": "./src/is-valid-legal-nature/is-valid-legal-nature.ts",
"./is-valid-license-plate": "./src/is-valid-license-plate/is-valid-license-plate.ts",
"./is-valid-mobile-phone": "./src/is-valid-mobile-phone/is-valid-mobile-phone.ts",
+ "./is-valid-nbs": "./src/is-valid-nbs/is-valid-nbs.ts",
"./is-valid-ncm": "./src/is-valid-ncm/is-valid-ncm.ts",
"./is-valid-nfe-key": "./src/is-valid-nfe-key/is-valid-nfe-key.ts",
"./is-valid-nfse-key": "./src/is-valid-nfse-key/is-valid-nfse-key.ts",
@@ -122,6 +126,7 @@
"./is-valid-processo-juridico": "./src/is-valid-processo-juridico/is-valid-processo-juridico.ts",
"./is-valid-registro-profissional": "./src/is-valid-registro-profissional/is-valid-registro-profissional.ts",
"./is-valid-renavam": "./src/is-valid-renavam/is-valid-renavam.ts",
+ "./is-valid-service-item": "./src/is-valid-service-item/is-valid-service-item.ts",
"./is-valid-service-phone": "./src/is-valid-service-phone/is-valid-service-phone.ts",
"./is-valid-suframa": "./src/is-valid-suframa/is-valid-suframa.ts",
"./is-valid-vin": "./src/is-valid-vin/is-valid-vin.ts",
diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md
index 766bd0494..59af722b0 100644
--- a/reports/api/brazilian-utils.api.md
+++ b/reports/api/brazilian-utils.api.md
@@ -281,6 +281,9 @@ export type FormatLegalNatureOptions = {
// @public
export const formatLicensePlate: (value: string) => string;
+// @public
+export const formatNbs: (value: string | number) => string;
+
// @public
export const formatNcm: (value: string | number, options?: FormatNcmOptions) => string;
@@ -614,6 +617,9 @@ export type GetMunicipalityOptions = GetMunicipalityParams;
// @public
export type GetMunicipalityParams = GetMunicipalityByCodeParams | GetMunicipalityByNameParams;
+// @public
+export const getNbs: (value: string | number) => Nbs | null;
+
// @public
export const getNfeKeyInfo: (value: string) => NfeKeyInfo | null;
@@ -626,6 +632,9 @@ export const getPixKeyInfo: (value: string) => PixKeyInfo | null;
// @public
export const getPixPayloadInfo: (value: string) => PixPayloadInfo | null;
+// @public
+export const getServiceItem: (value: string | number) => ServiceItem | null;
+
// @public
export const getStateByCep: (value: string | number) => State | null;
@@ -828,6 +837,9 @@ export type IsValidMobilePhoneOptions = {
version?: PhoneVersion;
};
+// @public
+export const isValidNbs: (value: string | number) => boolean;
+
// @public
export const isValidNcm: (value: string | number) => boolean;
@@ -882,6 +894,9 @@ export type IsValidRegistroProfissionalParams = {
// @public
export const isValidRenavam: (renavam: string | number) => boolean;
+// @public
+export const isValidServiceItem: (value: string | number) => boolean;
+
// @public
export const isValidServicePhone: (value: string) => boolean;
@@ -922,6 +937,12 @@ export type Municipality = {
stateCode: StateCode;
};
+// @public
+export type Nbs = {
+ code: string;
+ description: string;
+};
+
// @public
export type NfeKeyInfo = {
stateCode: StateCode;
@@ -1089,6 +1110,12 @@ export type RegistroProfissionalCouncil = "OAB" | "CRM" | "CRO" | "CRP" | "CRC";
// @public
export const removeAccents: (value: string) => string;
+// @public
+export type ServiceItem = {
+ code: string;
+ description: string;
+};
+
// @public
export type StandardSchemaV1 = {
readonly "~standard": StandardSchemaV1Props;
diff --git a/scripts/data.ts b/scripts/data.ts
index 266eedbab..dff8174bb 100644
--- a/scripts/data.ts
+++ b/scripts/data.ts
@@ -26,7 +26,9 @@ const generators = [
"cities.ts",
"cnae.ts",
"legal-natures.ts",
+ "nbs.ts",
"ncm.ts",
+ "service-items.ts",
"states.ts",
];
@@ -36,6 +38,8 @@ const generatedFiles = [
"./src/_internals/constants/cfop.ts",
"./src/_internals/constants/cnae.ts",
"./src/_internals/constants/municipalities.ts",
+ "./src/_internals/constants/nbs.ts",
+ "./src/_internals/constants/service-items.ts",
"./src/_internals/constants/states.ts",
"./src/is-valid-legal-nature/constants.ts",
"./src/is-valid-ncm/constants.ts",
diff --git a/scripts/nbs.ts b/scripts/nbs.ts
new file mode 100644
index 000000000..018423361
--- /dev/null
+++ b/scripts/nbs.ts
@@ -0,0 +1,105 @@
+#!/usr/bin/env node
+
+import { writeFile } from "node:fs/promises";
+import { resolve } from "node:path";
+
+import { fetchSortedRecord } from "./fetch-sorted-record.ts";
+
+const scriptsDir = import.meta.dirname;
+
+/** The official CSV is published in ISO-8859-1, not UTF-8. */
+const NBS_CSV_DECODER = new TextDecoder("iso-8859-1");
+
+const NBS_CSV_HEADER = "NBS 2.0;DESCRIÇÃO";
+
+/**
+ * A complete code, e.g. `1.0101.11.00`. The CSV also carries the chapter (`1.01`), position
+ * (`1.0101`) and subposition (`1.0101.1`) headings, which classify nothing by themselves and are
+ * left out of the table.
+ */
+const NBS_CODE_REGEX = /^\d\.\d{4}\.\d{2}\.\d{2}$/;
+
+/**
+ * Smallest number of complete codes the table yields. NBS 2.0 carries 920 and only a new version
+ * of the nomenclature changes that, so a result far below it means the CSV layout changed.
+ */
+const MINIMUM_CODES = 900;
+
+/**
+ * Reads the `NBS 2.0;DESCRIÇÃO` rows of the official NBS 2.0 CSV.
+ * @param {string} csv - The decoded CSV.
+ * @returns {Record} One entry per complete code, keyed by its 9 digits.
+ */
+const parseCsv = (csv: string): Record => {
+ const [header, ...rows] = csv.split(/\r?\n/);
+
+ if (header?.trim() !== NBS_CSV_HEADER) {
+ throw new Error(`NBS CSV header is not "${NBS_CSV_HEADER}"`);
+ }
+
+ const data: Record = {};
+
+ for (const row of rows) {
+ const separator = row.indexOf(";");
+ const code = row.slice(0, Math.max(separator, 0)).trim();
+ const description = row
+ .slice(separator + 1)
+ .trim()
+ .replaceAll(/\s+/g, " ");
+
+ if (!NBS_CODE_REGEX.test(code) || description === "") continue;
+
+ data[code.replaceAll(".", "")] = description;
+ }
+
+ return data;
+};
+
+const main = async (): Promise => {
+ const sorted = await fetchSortedRecord(
+ "https://www.gov.br/mdic/pt-br/images/REPOSITORIO/scs/decos/NBS/NBSa_2-0.csv",
+ "NBS CSV",
+ async (response) => {
+ const data = parseCsv(NBS_CSV_DECODER.decode(await response.arrayBuffer()));
+
+ if (Object.keys(data).length < MINIMUM_CODES) {
+ throw new Error(`NBS CSV holds fewer than ${MINIMUM_CODES} codes`);
+ }
+
+ return data;
+ },
+ );
+
+ await writeFile(
+ resolve(scriptsDir, "..", "./src/_internals/constants/nbs.ts"),
+ `/**
+ * NBS 2.0 (Nomenclatura Brasileira de Serviços, Intangíveis e Outras Operações que Produzam
+ * Variações no Patrimônio) descriptions, indexed by the raw 9 digit code.
+ *
+ * Built from the official \`NBSa_2-0.csv\` the MDIC publishes on gov.br, a plain
+ * \`NBS 2.0;DESCRIÇÃO\` CSV in ISO-8859-1, the version 2.0 as amended by the Portaria Conjunta
+ * RFB/SCS 2.000 of 18 December 2018. Only the complete codes are kept: the chapter, position and
+ * subposition headings of the CSV classify nothing by themselves.
+ *
+ * Generated by \`node ./scripts/nbs.ts\`. Do not edit by hand.
+ *
+ * @see Official: https://www.gov.br/mdic/pt-br/assuntos/sdic/comercio-e-servicos/nbs-nomenclatura-brasileira-de-servicos
+ * The NBS page of the MDIC, which links the CSV and the Anexo I of the Portaria.
+ * @see Official: https://www.gov.br/mdic/pt-br/images/REPOSITORIO/scs/decos/NBS/NBSa_2-0.csv
+ * The NBS 2.0 table in CSV.
+ */
+export const NBS_DESCRIPTIONS: Record = ${JSON.stringify(sorted)};
+
+/**
+ * Shape an NBS code has to be written in: the 9 digits, optionally split into the printed
+ * groups of 1, 4, 2 and 2 by a single whitespace or mask character.
+ */
+export const NBS_FORMAT_REGEX = /^\\d[\\s.\\-/]?\\d{4}[\\s.\\-/]?\\d{2}[\\s.\\-/]?\\d{2}$/;
+`,
+ );
+};
+
+await main().catch((error) => {
+ console.error(error instanceof Error ? error.message : error);
+ process.exit(1);
+});
diff --git a/scripts/read-xlsx-sheet.ts b/scripts/read-xlsx-sheet.ts
new file mode 100644
index 000000000..741bf4b30
--- /dev/null
+++ b/scripts/read-xlsx-sheet.ts
@@ -0,0 +1,121 @@
+import { inflateRawSync } from "node:zlib";
+
+const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06_05_4b_50;
+
+const CENTRAL_DIRECTORY_HEADER_SIZE = 46;
+
+const LOCAL_HEADER_SIZE = 30;
+
+const DEFLATE = 8;
+
+const ALPHABET_LENGTH = 26;
+
+const CODE_BEFORE_A = 64;
+
+const XML_ENTITIES: Record = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ """: '"',
+ "'": "'",
+};
+
+const decodeXml = (text: string): string =>
+ text.replaceAll(/&(?:amp|lt|gt|quot|apos);/g, (entity) => XML_ENTITIES[entity] ?? entity);
+
+/**
+ * Reads the files of a zip archive out of its central directory, inflating the deflated ones.
+ * @param {Buffer} archive - The whole archive.
+ * @returns {Map} The UTF-8 content of every file, keyed by its path.
+ */
+const unzip = (archive: Buffer): Map => {
+ let end = archive.length - 22;
+
+ while (end >= 0 && archive.readUInt32LE(end) !== END_OF_CENTRAL_DIRECTORY_SIGNATURE) end--;
+
+ if (end < 0) throw new Error("not a zip archive");
+
+ const files = new Map();
+ let offset = archive.readUInt32LE(end + 16);
+
+ for (let index = archive.readUInt16LE(end + 10); index > 0; index--) {
+ const method = archive.readUInt16LE(offset + 10);
+ const size = archive.readUInt32LE(offset + 20);
+ const nameLength = archive.readUInt16LE(offset + 28);
+ const skipped = archive.readUInt16LE(offset + 30) + archive.readUInt16LE(offset + 32);
+ const local = archive.readUInt32LE(offset + 42);
+ const nameStart = offset + CENTRAL_DIRECTORY_HEADER_SIZE;
+ const name = archive.toString("utf8", nameStart, nameStart + nameLength);
+ const start =
+ local +
+ LOCAL_HEADER_SIZE +
+ archive.readUInt16LE(local + 26) +
+ archive.readUInt16LE(local + 28);
+ const data = archive.subarray(start, start + size);
+
+ files.set(name, (method === DEFLATE ? inflateRawSync(data) : data).toString("utf8"));
+ offset = nameStart + nameLength + skipped;
+ }
+
+ return files;
+};
+
+/**
+ * Turns the letters of a cell reference into a zero based column index: `A` is 0, `AA` is 26.
+ * @param {string} letters - The column letters of the reference.
+ * @returns {number} The zero based index of the column.
+ */
+const columnIndex = (letters: string): number => {
+ let index = 0;
+
+ for (let position = 0; position < letters.length; position++) {
+ index = index * ALPHABET_LENGTH + letters.charCodeAt(position) - CODE_BEFORE_A;
+ }
+
+ return index - 1;
+};
+
+const readFile = (files: Map, path: string): string => {
+ const content = files.get(path);
+
+ if (content === undefined) throw new Error(`the workbook has no ${path}`);
+
+ return content;
+};
+
+/**
+ * Reads one sheet of an `.xlsx` workbook, enough of the format for the plain tables the
+ * government publishes: shared strings (rich text runs joined), numbers and empty cells.
+ * @param {Buffer} workbook - The `.xlsx` file.
+ * @param {string} sheetName - The name of the sheet, as its tab shows it.
+ * @returns {string[][]} The rows of the sheet, every cell as text and an absent cell as `""`.
+ */
+export const readXlsxSheet = (workbook: Buffer, sheetName: string): string[][] => {
+ const files = unzip(workbook);
+ const sheets = readFile(files, "xl/workbook.xml").matchAll(/]*>/g);
+ const sheet = [...sheets].find((tag) => decodeXml(tag[0]).includes(`name="${sheetName}"`));
+ const relationId = /r:id="([^"]+)"/.exec(sheet?.[0] ?? "")?.[1];
+ const relations = readFile(files, "xl/_rels/workbook.xml.rels").matchAll(/]*>/g);
+ const relation = [...relations].find((tag) => tag[0].includes(`Id="${relationId}"`));
+ const target = /Target="([^"]+)"/.exec(relation?.[0] ?? "")?.[1];
+
+ if (target === undefined) throw new Error(`the workbook has no sheet named ${sheetName}`);
+
+ const strings = [...readFile(files, "xl/sharedStrings.xml").matchAll(/(.*?)<\/si>/gs)].map(
+ (item) =>
+ [...item[1].matchAll(/]*>([^<]*)<\/t>/g)].map((run) => decodeXml(run[1])).join(""),
+ );
+
+ return [...readFile(files, `xl/${target}`).matchAll(/]*>(.*?)<\/row>/gs)].map((row) => {
+ const cells: string[] = [];
+
+ for (const cell of row[1].matchAll(/]*?)(?:\/>|>(.*?)<\/c>)/gs)) {
+ const column = columnIndex(cell[1]);
+ const value = /([^<]*)<\/v>/.exec(cell[3] ?? "")?.[1] ?? "";
+
+ cells[column] = cell[2].includes('t="s"') ? strings[Number(value)] : value;
+ }
+
+ return Array.from(cells, (cell) => cell ?? "");
+ });
+};
diff --git a/scripts/service-items.ts b/scripts/service-items.ts
new file mode 100644
index 000000000..cbc23f7fe
--- /dev/null
+++ b/scripts/service-items.ts
@@ -0,0 +1,127 @@
+#!/usr/bin/env node
+
+import { writeFile } from "node:fs/promises";
+import { resolve } from "node:path";
+
+import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts";
+import { readXlsxSheet } from "./read-xlsx-sheet.ts";
+
+const scriptsDir = import.meta.dirname;
+
+const DOCUMENTATION_URL =
+ "https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual";
+
+/**
+ * The ANEXO B is published under a file name that carries its version and date, e.g.
+ * `anexo_b-nbs2-lista_servico_nacional-snnfse-v1-01-20260122.xlsx`, so the link is read from the
+ * documentation page instead of being pinned here: a new version is picked up by the next run.
+ */
+const ANNEX_LINK_REGEX = /href="([^"]*anexo_b-nbs2-lista_servico_nacional[^"]*\.xlsx)"/i;
+
+const SHEET_NAME = "LISTA.SERV.NAC.";
+
+const SHEET_HEADER = "CÓDIGO DE TRIBUTAÇÃO NACIONAL|ITEM|SUBITEM|DESDOBRO NACIONAL|DESCRIÇÃO";
+
+/**
+ * Item 99, "Serviços sem a incidência de ISSQN e ICMS", is an addition of the national list: the
+ * list annexed to the Lei Complementar 116/2003 ends at item 40.
+ */
+const LAST_LAW_ITEM = 40;
+
+/**
+ * Smallest number of subitems a complete list yields. The law carries 200 in force today and is
+ * only ever amended by another lei complementar, so a result far below it means the sheet
+ * changed, not that subitems were revoked.
+ */
+const MINIMUM_SUBITEMS = 190;
+
+const fetchOk = async (url: string, label: string): Promise => {
+ const response = await fetchWithRetry(url);
+
+ if (!response.ok) throw new Error(`${label} request failed with status ${response.status}`);
+
+ return response;
+};
+
+/**
+ * Reads the subitems of the law out of the national service list. The sheet prints three levels:
+ * the item (`SUBITEM` 0), the subitem of the law (`DESDOBRO NACIONAL` 0) and the national codes
+ * the Comitê Gestor splits a subitem into. Only the second level is the list annexed to the law.
+ * @param {string[][]} rows - The rows of the sheet, header included.
+ * @returns {Record} One entry per subitem, keyed by the 4 digits (`0101`).
+ */
+const parseSheet = (rows: string[][]): Record => {
+ const [header, ...body] = rows;
+
+ if (header?.join("|") !== SHEET_HEADER) {
+ throw new Error(`the ${SHEET_NAME} header is not "${SHEET_HEADER}"`);
+ }
+
+ const data: Record = {};
+
+ for (const [, item, subitem, split, description] of body) {
+ const isLawSubitem = Number(item) <= LAST_LAW_ITEM && Number(subitem) > 0 && split === "0";
+ const text = (description ?? "").trim().replaceAll(/\s+/g, " ");
+
+ if (!isLawSubitem || text === "") continue;
+
+ data[`${item.padStart(2, "0")}${subitem.padStart(2, "0")}`] = text;
+ }
+
+ return data;
+};
+
+const main = async (): Promise => {
+ const pageResponse = await fetchOk(DOCUMENTATION_URL, "NFS-e documentation page");
+ const page = await pageResponse.text();
+ const link = ANNEX_LINK_REGEX.exec(page)?.[1];
+
+ if (link === undefined) throw new Error("the NFS-e documentation page links no ANEXO B");
+
+ const workbookResponse = await fetchOk(link, "NFS-e ANEXO B");
+ const workbook = await workbookResponse.arrayBuffer();
+ const data = parseSheet(readXlsxSheet(Buffer.from(workbook), SHEET_NAME));
+ const sorted: Record = {};
+
+ for (const key of Object.keys(data).sort()) sorted[key] = data[key];
+
+ if (Object.keys(sorted).length < MINIMUM_SUBITEMS) {
+ throw new Error(`the national service list holds fewer than ${MINIMUM_SUBITEMS} subitems`);
+ }
+
+ await writeFile(
+ resolve(scriptsDir, "..", "./src/_internals/constants/service-items.ts"),
+ `/**
+ * The subitems in force of the service list annexed to the Lei Complementar 116/2003, indexed by
+ * the raw 4 digits of the subitem (\`0101\` for \`1.01\`), the first four digits of the national
+ * \`cTribNac\` code.
+ *
+ * Built from the sheet \`${SHEET_NAME}\` of the \`ANEXO_B-NBS2-LISTA_SERVICO_NACIONAL\` workbook
+ * of the Sistema Nacional NFS-e, which prints the list in force, vetoed subitems left out and
+ * the amendments of later leis complementares applied. The item headings, the national codes a
+ * subitem is split into and item 99, an addition of the national list, are not part of the law
+ * and are left out.
+ *
+ * Generated by \`node ./scripts/service-items.ts\` from \`${link.slice(link.lastIndexOf("/") + 1)}\`.
+ * Do not edit by hand.
+ *
+ * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp116.htm
+ * Lei Complementar 116/2003, "Lista de serviços anexa".
+ * @see Official: ${DOCUMENTATION_URL}
+ * Sistema Nacional NFS-e, current technical documentation, ANEXO B.
+ */
+export const SERVICE_ITEM_DESCRIPTIONS: Record = ${JSON.stringify(sorted)};
+
+/**
+ * Shape a subitem has to be written in: one or two digits of the item and the two of the
+ * subitem, with the dot the law prints between them or without it.
+ */
+export const SERVICE_ITEM_FORMAT_REGEX = /^\\d{1,2}\\.?\\d{2}$/;
+`,
+ );
+};
+
+await main().catch((error) => {
+ console.error(error instanceof Error ? error.message : error);
+ process.exit(1);
+});
diff --git a/src/_internals/constants/nbs.ts b/src/_internals/constants/nbs.ts
new file mode 100644
index 000000000..030011475
--- /dev/null
+++ b/src/_internals/constants/nbs.ts
@@ -0,0 +1,1207 @@
+/**
+ * NBS 2.0 (Nomenclatura Brasileira de Serviços, Intangíveis e Outras Operações que Produzam
+ * Variações no Patrimônio) descriptions, indexed by the raw 9 digit code.
+ *
+ * Built from the official `NBSa_2-0.csv` the MDIC publishes on gov.br, a plain
+ * `NBS 2.0;DESCRIÇÃO` CSV in ISO-8859-1, the version 2.0 as amended by the Portaria Conjunta
+ * RFB/SCS 2.000 of 18 December 2018. Only the complete codes are kept: the chapter, position and
+ * subposition headings of the CSV classify nothing by themselves.
+ *
+ * Generated by `node ./scripts/nbs.ts`. Do not edit by hand.
+ *
+ * @see Official: https://www.gov.br/mdic/pt-br/assuntos/sdic/comercio-e-servicos/nbs-nomenclatura-brasileira-de-servicos
+ * The NBS page of the MDIC, which links the CSV and the Anexo I of the Portaria.
+ * @see Official: https://www.gov.br/mdic/pt-br/images/REPOSITORIO/scs/decos/NBS/NBSa_2-0.csv
+ * The NBS 2.0 table in CSV.
+ */
+export const NBS_DESCRIPTIONS: Record = {
+ "101011100": "Serviços de construção de edificações residenciais de um e dois pavimentos",
+ "101011200": "Serviços de construção de edificações residenciais com mais de dois pavimentos",
+ "101012100": "Serviços de construção de edificações industriais",
+ "101012200": "Serviços de construção de edificações comerciais",
+ "101012900":
+ "Serviços de construção de edificações não residenciais não classificados em subposições anteriores",
+ "101013000": "Serviços de construção de edificações de uso misto (residencial e não residencial)",
+ "101021100":
+ "Serviços de construção de autoestradas (exceto autoestradas elevadas), ruas e estradas",
+ "101021200": "Serviços de construção de ferrovias",
+ "101021300":
+ "Serviços de construção de pistas de pouso e decolagem em aeroportos e de infraestrutura aeroportuária",
+ "101022000": "Serviços de construção de pontes, autoestradas elevadas e túneis",
+ "101023100": "Serviços de construção de infraestrutura de proteção e acesso aquaviário",
+ "101023200": "Serviços de construção de infraestrutura de acostagem aquaviária",
+ "101023300":
+ "Serviços de construção de infraestrutura terrestre e de obras de engenharia afins nos portos",
+ "101023400": "Serviços de construção de barragens",
+ "101023510": "Serviços de construção de adutoras em conduto livre",
+ "101023520": "Serviços de construção de sistemas de irrigação",
+ "101023530":
+ "Serviços de construção relacionada ao controle dos cursos de água, inclusive canalização",
+ "101024110":
+ "Serviços de construção de dutos de longo curso para o transporte de petróleo, seus derivados, e gás",
+ "101024120":
+ "Serviços de construção de dutos de longo curso para o transporte e escoamento de águas",
+ "101024190":
+ "Serviços de construção de dutos de longo curso não classificados em subposições anteriores",
+ "101024210": "Serviços de construção de linhas de comunicação de longo curso",
+ "101024220": "Serviços de construção de linhas de transmissão de alta tensão",
+ "101025100": "Serviços de construção de dutos locais",
+ "101025210": "Serviços de construção de linhas locais de comunicação",
+ "101025220": "Serviços de construção de linhas locais de transmissão de baixa e média tensão",
+ "101025310": "Serviços de construção de sistemas de esgotos",
+ "101025320":
+ "Serviços de construção de sistemas de estações para elevação, tratamento e purificação de água",
+ "101026100": "Serviços de construção de usinas de geração de energia",
+ "101026900":
+ "Serviços de construção de instalações industriais não classificados em subposições anteriores",
+ "101027000": "Serviços de construção de minas e suas unidades industriais",
+ "101028000":
+ "Serviços de construção de instalações para recreação e atividades desportivas ao ar livre",
+ "101029000":
+ "Serviços de construção de obras de engenharia civil não classificados em subposições anteriores",
+ "101031000": "Serviços de demolição",
+ "101032000": "Serviços de preparação de terrenos e de canteiros de obras",
+ "101033000": "Serviços de escavação e remoção de terra",
+ "101034100": "Serviços de perfuração de poços de água",
+ "101034200": "Serviços de instalação de sistemas sépticos",
+ "101040000": "Serviços de montagem e de edificação de construções pré-fabricadas",
+ "101051100": "Serviços de estaqueamento",
+ "101051200": "Serviços de fundação",
+ "101052100": "Serviços de construção de estruturas de edificações",
+ "101052200": "Serviços de construção de estruturas de telhados e coberturas",
+ "101053000": "Serviços de construção de telhados e coberturas e serviços de impermeabilização",
+ "101054000": "Serviços de concretagem",
+ "101055000": "Serviços de montagem de estruturas de aço",
+ "101056000": "Serviços de alvenaria",
+ "101057000": "Serviços de andaimes",
+ "101059000": "Serviços especializados de construção não classificados em subposições anteriores",
+ "101061100": "Serviços de instalação de fiação elétrica e componentes",
+ "101061200": "Serviços de instalação de alarmes contra incêndio",
+ "101061300": "Serviços de instalação de sistemas de alarmes antifurto",
+ "101061400": "Serviços de instalação de antenas residenciais",
+ "101061900": "Serviços de instalação elétrica não classificados em subposições anteriores",
+ "101062100": "Serviços de instalação de tubulação para fornecimento de água",
+ "101062200": "Serviços de instalação de tubulação para escoamento de água",
+ "101063100": "Serviços de instalação de equipamentos de aquecimento",
+ "101063200": "Serviços de instalação de equipamentos de ventilação e de ar condicionado",
+ "101064000": "Serviços de instalação de gás",
+ "101065000": "Serviços de instalação de isolamentos",
+ "101066000": "Serviços de instalação de elevadores, esteiras e escadas rolantes",
+ "101069000": "Serviços de instalação não classificados em subposições anteriores",
+ "101071000": "Serviços de vidraçaria",
+ "101072000": "Serviços de gesso e de estuque",
+ "101073000": "Serviços de pintura",
+ "101074000": "Serviços de revestimento de pisos e de paredes",
+ "101075000": "Serviços de carpintaria e de serralharia",
+ "101076000": "Serviços de instalação de cercas e grades",
+ "101079000": "Serviços de acabamento não classificados em subposições anteriores",
+ "102010000": "Serviços de intermediação na distribuição de mercadorias",
+ "102020000": "Comércio atacadista",
+ "102030000": "Comércio varejista",
+ "102040000": "Serviços de despacho aduaneiro",
+ "102050000": "Serviços de intermediação na comercialização de energia elétrica",
+ "103011000": "Fornecimento de refeições acompanhado de serviços de restaurante",
+ "103012100": "Fornecimento de refeições pelo sistema de autosserviço (self-service)",
+ "103012200": "Fornecimento de comidas rápidas (fast-food)",
+ "103012900":
+ "Fornecimento de refeições com serviços limitados de restaurante não classificado em subposições anteriores",
+ "103013100": "Fornecimento de alimentação para eventos",
+ "103013200":
+ "Fornecimento de alimentação para operadores de transportes (comissaria ou catering)",
+ "103013900":
+ "Fornecimento de alimentação, incluindo refeições, sob contrato não classificado em subposições anteriores",
+ "103019000": "Fornecimento de alimentação não classificado em subposições anteriores",
+ "103020000": "Fornecimento de bebidas em bares, cervejarias e outros",
+ "103031100":
+ "Serviços de hospedagem em quartos ou unidades de hospedagem para visitantes, com serviços diários de faxina",
+ "103031200":
+ "Serviços de hospedagem em quartos ou unidades de hospedagem para visitantes, sem serviços diários de faxina",
+ "103031300":
+ "Serviços de hospedagem em quartos ou unidades de hospedagem para visitantes, em propriedades partilhadas",
+ "103031400":
+ "Serviços de hospedagem em quartos ou unidades de hospedagem para visitantes, em quartos de múltipla ocupação",
+ "103032000": "Serviços de acampamentos turísticos (camping)",
+ "103039000": "Serviços de hospedagem para visitantes não classificados em subposições anteriores",
+ "103041000":
+ "Serviços de hospedagem em quartos ou unidades de hospedagem para estudantes em residências estudantis",
+ "103042000":
+ "Serviços de hospedagem em quartos ou unidades de hospedagem para trabalhadores em hotéis ou campos",
+ "103049000":
+ "Serviços de hospedagem, exceto para visitantes não classificados em subposições anteriores",
+ "104011111":
+ "Serviços de transporte rodoviário local prestados exclusivamente por meio de ônibus",
+ "104011119":
+ "Serviços de transporte rodoviário local regular de passageiros, exceto em áreas metropolitanas não classificados em subitens anteriores",
+ "104011120":
+ "Serviços de transporte rodoviário local regular de passageiros, em áreas metropolitanas",
+ "104011210": "Serviços de transporte escolar",
+ "104011220": "Serviços de transporte para aeroportos (shuttle)",
+ "104011290":
+ "Serviços especiais de transporte rodoviário local regular de passageiros não classificados em itens anteriores",
+ "104011300": "Serviços de táxi",
+ "104011400": "Serviços de carro com motorista, exceto táxi",
+ "104011510": "Serviços de fretamento contínuo, local",
+ "104011520": "Serviços de fretamento eventual ou turístico, local",
+ "104011610": "Serviços de transporte ferroviário local de passageiros",
+ "104011620": "Serviços de transporte metroviário (metrô) local de passageiros",
+ "104011690":
+ "Serviços de transporte local de passageiros por veículos sobre trilhos não classificados em itens anteriores",
+ "104011710": "Serviços de transporte rodoviário para passeios turísticos (sightseeing)",
+ "104011720": "Serviços de transporte ferroviário para passeios turísticos (sightseeing)",
+ "104011790":
+ "Serviços de transporte terrestre para passeios turísticos (sightseeing) não classificados em itens anteriores",
+ "104011900":
+ "Serviços de transporte terrestre local de passageiros não classificados em subposições anteriores",
+ "104012110":
+ "Serviços de transporte aquaviário local de passageiros, por navegação interior, em embarcações para travessia",
+ "104012120":
+ "Serviços de transporte aquaviário local de passageiros, por navegação interior, em embarcações para cruzeiros",
+ "104012190":
+ "Serviços de transporte aquaviário local de passageiros, por navegação interior não classificados em itens anteriores",
+ "104012200": "Serviços de transporte aquaviário local de passageiros por fretamento",
+ "104012300": "Serviços de transporte aquaviário para passeios turísticos (sightseeing)",
+ "104012900":
+ "Serviços de transporte aquaviário local de passageiros não classificados em subposições anteriores",
+ "104013000": "Serviços de transporte integrado local de passageiros",
+ "104014100": "Serviços de táxi aéreo local",
+ "104014200": "Serviços de transporte aéreo local de passageiros por fretamento",
+ "104014300": "Serviços de transporte aéreo para passeios turísticos (sightseeing)",
+ "104014900":
+ "Serviços de transporte aéreo local de passageiros não classificados em subposições anteriores",
+ "104019000":
+ "Serviços de transporte local de passageiros não classificados não classificados em subposições anteriores",
+ "104021110":
+ "Serviços de transporte rodoviário nacional, exceto local, prestados exclusivamente por meio de ônibus",
+ "104021190":
+ "Serviços de transporte rodoviário nacional, exceto local, de passageiros, não classificados não classificados em subitens anteriores",
+ "104021200":
+ "Serviços especiais de transporte rodoviário nacional, exceto local, regular de passageiros",
+ "104021310": "Serviços de fretamento contínuo, nacional, exceto local",
+ "104021320": "Serviços de fretamento eventual ou turístico, nacional, exceto local",
+ "104021400": "Serviços de transporte ferroviário nacional, exceto local, de passageiros",
+ "104021900":
+ "Serviços de transporte terrestre nacional, exceto local, de passageiros não classificados não classificados em subposições anteriores",
+ "104022110":
+ "Serviços de transporte aquaviário de passageiros, por navegação interior, em embarcações para travessia",
+ "104022120":
+ "Serviços de transporte aquaviário de passageiros, por navegação interior, em embarcações para cruzeiros",
+ "104022190":
+ "Serviços de transporte aquaviário nacional, exceto local, de passageiros, por navegação interior não classificados em itens anteriores",
+ "104022200": "Serviços de transporte aquaviário nacional costeiro de passageiros",
+ "104022300":
+ "Serviços de transporte aquaviário nacional, exceto local, de passageiros por fretamento",
+ "104022900":
+ "Serviços de transporte aquaviário nacional, exceto local, de passageiros não classificados em subposições anteriores",
+ "104023100": "Serviços de transporte aéreo nacional, exceto local, regular de passageiros",
+ "104023200": "Serviços de táxi aéreo nacional, exceto local",
+ "104023300":
+ "Serviços de transporte aéreo nacional, exceto local, de passageiros, por fretamento",
+ "104023900":
+ "Serviços de transporte aéreo nacional, exceto local, de passageiros não classificados em subposições anteriores",
+ "104029000":
+ "Serviços de transporte nacional, exceto local, de passageiros não classificados em subposições anteriores",
+ "104031110": "Serviços de transporte de passageiros prestados exclusivamente por meio de ônibus",
+ "104031190":
+ "Serviços de transporte rodoviário internacional de passageiros não classificados em itens anteriores",
+ "104031200": "Serviços de transporte rodoviário internacional de passageiros por fretamento",
+ "104031310": "Serviços de transporte internacional ferroviário de passageiros",
+ "104031390":
+ "Serviços de transporte internacional de passageiros por veículos sobre trilhos não classificados em itens anteriores",
+ "104031900":
+ "Serviços de transporte terrestre internacional de passageiros não classificados em subposições anteriores",
+ "104032110":
+ "Serviços de transporte aquaviário internacional de passageiros, por navegação interior, em embarcações para travessia",
+ "104032120":
+ "Serviços de transporte aquaviário internacional de passageiros, por navegação interior, em embarcações para cruzeiros",
+ "104032190":
+ "Serviços de transporte aquaviário internacional de passageiros, por navegação interior não classificados em itens anteriores",
+ "104032200": "Serviços de transporte aquaviário internacional costeiro de passageiros",
+ "104032300": "Serviços de transporte aquaviário internacional transoceânico de passageiros",
+ "104032400": "Serviços de transporte aquaviário internacional de passageiros por fretamento",
+ "104032900":
+ "Serviços de transporte aquaviário internacional de passageiros não classificados em subposições anteriores",
+ "104033100": "Serviços de transporte aéreo internacional regular de passageiros",
+ "104033200": "Serviços de táxi aéreo internacional",
+ "104033300": "Serviços de transporte aéreo internacional de passageiros por fretamento",
+ "104033900":
+ "Serviços de transporte aéreo internacional de passageiros não classificados em subposições anteriores",
+ "104039000":
+ "Serviços de transporte internacional de passageiros não classificados em subposições anteriores",
+ "104041000": "Locação de veículos rodoviários de passageiros com motorista",
+ "104042000": "Locação de embarcações de passageiros com tripulação",
+ "104043000": "Locação de aeronaves de passageiros com tripulação",
+ "104050000": "Afretamento de embarcações de passageiros por tempo",
+ "105011110": "Serviços de transporte rodoviário de cargas sólidas a granel",
+ "105011120": "Serviços de transporte rodoviário de cargas líquidas, ou liquefeitas, a granel",
+ "105011130": "Serviços de transporte rodoviário de cargas gasosas a granel",
+ "105011210": "Serviços de transporte rodoviário de carga solta, não unitizada",
+ "105011220": "Serviços de transporte rodoviário de carga unitizada",
+ "105011230": "Serviços de transporte rodoviário de carga frigorificada ou climatizada",
+ "105011310":
+ "Serviços de transporte rodoviário de cargas em contêineres frigorificados ou climatizados",
+ "105011320":
+ "Serviços de transporte rodoviário de cargas em contêineres não frigorificados ou climatizados",
+ "105011410": "Serviços de transporte rodoviário de cargas vivas",
+ "105011420":
+ "Serviços de transporte rodoviário de mudanças domésticas e de mobília e outros objetos de escritório",
+ "105011430": "Serviços de transporte rodoviário de cargas de grande porte",
+ "105011440": "Serviços de transporte rodoviário de veículos",
+ "105011451":
+ "Serviços de transporte rodoviário de combustíveis, lubrificantes e GLP, inclusive apresentados em botijões metálicos",
+ "105011452":
+ "Serviços de transporte rodoviário de produtos químicos perigosos, exceto lubrificantes e GLP",
+ "105011459":
+ "Serviços de transporte rodoviário de produtos perigosos não classificados em subitens anteriores",
+ "105011500": "Serviços de transporte rodoviário de cargas postais e malotes",
+ "105011900":
+ "Serviços de transporte rodoviário de cargas não classificados em subposições anteriores",
+ "105012110": "Serviços de transporte ferroviário de cargas sólidas a granel",
+ "105012120": "Serviços de transporte ferroviário de cargas líquidas, ou liquefeitas, a granel",
+ "105012130": "Serviços de transporte ferroviário de cargas gasosas a granel",
+ "105012210": "Serviços de transporte ferroviário de carga solta, não unitizada",
+ "105012220": "Serviços de transporte ferroviário de carga unitizada",
+ "105012230": "Serviços de transporte ferroviário de carga frigorificada ou climatizada",
+ "105012310":
+ "Serviços de transporte ferroviário de cargas em contêineres frigorificados ou climatizados",
+ "105012320":
+ "Serviços de transporte ferroviário de cargas em contêineres não frigorificados ou climatizados",
+ "105012410": "Serviços de transporte ferroviário de cargas vivas",
+ "105012421":
+ "Serviços de transporte ferroviário de combustíveis, lubrificantes e GLP, inclusive apresentados em botijões metálicos",
+ "105012422":
+ "Serviços de transporte ferroviário de produtos químicos perigosos, exceto lubrificantes e GLP",
+ "105012429":
+ "Serviços de transporte ferroviário de produtos perigosos não classificados em subposições anteriores",
+ "105012500": "Serviços de transporte ferroviário de bens e valores",
+ "105012900":
+ "Serviços de transporte ferroviário de cargas não classificados em subposições anteriores",
+ "105013100": "Serviços de transporte de petróleo, gás natural e combustível por meio de dutos",
+ "105013200": "Serviços de transporte de minérios por meio de dutos",
+ "105013900":
+ "Serviços de transporte de cargas por meio de dutos não classificados em subposições anteriores",
+ "105021110":
+ "Serviços de transporte aquaviário por navegação interior de cargas sólidas, a granel",
+ "105021120":
+ "Serviços de transporte aquaviário por navegação interior de cargas líquidas, ou liquefeitas, a granel",
+ "105021130":
+ "Serviços de transporte aquaviário por navegação interior de cargas gasosas a granel",
+ "105021210":
+ "Serviços de transporte aquaviário por navegação interior de carga solta, não unitizada",
+ "105021220": "Serviços de transporte aquaviário por navegação interior de carga unitizada",
+ "105021230":
+ "Serviços de transporte aquaviário por navegação interior de carga frigorificada ou climatizada",
+ "105021310":
+ "Serviços de transporte aquaviário por navegação interior de cargas em contêineres frigorificados ou climatizados",
+ "105021320":
+ "Serviços de transporte aquaviário por navegação interior de cargas em contêineres não frigorificados ou climatizados",
+ "105021410": "Serviços de transporte aquaviário por navegação interior de cargas vivas",
+ "105021420":
+ "Serviços de transporte aquaviário por navegação interior de mudanças domésticas e de mobília e outros objetos de escritório",
+ "105021430": "Serviços de transporte aquaviário por navegação interior de cargas de grande porte",
+ "105021440": "Serviços de transporte aquaviário por navegação interior de veículos",
+ "105021451":
+ "Serviços de transporte aquaviário por navegação interior de combustíveis, lubrificantes e GLP, inclusive apresentado em botijões metálicos",
+ "105021452":
+ "Serviços de transporte aquaviário por navegação interior de produtos químicos perigosos, exceto lubrificantes e GLP",
+ "105021459":
+ "Serviços de transporte aquaviário por navegação interior de produtos perigosos não classificados em subposições anteriores",
+ "105021490":
+ "Serviços de transporte aquaviário por navegação interior de cargas especiais não classificados em subposições anteriores",
+ "105021900":
+ "Serviços de transporte aquaviário por navegação interior de cargas não classificados em subposições anteriores",
+ "105022110": "Serviços de transporte aquaviário costeiro de cargas sólidas a granel",
+ "105022120":
+ "Serviços de transporte aquaviário costeiro de cargas líquidas ou liquefeitas, a granel",
+ "105022130": "Serviços de transporte aquaviário costeiro de cargas gasosas a granel",
+ "105022210": "Serviços de transporte aquaviário costeiro de carga solta, não unitizada",
+ "105022220": "Serviços de transporte aquaviário costeiro de carga unitizada",
+ "105022230": "Serviços de transporte aquaviário costeiro de carga frigorificada ou climatizada",
+ "105022310":
+ "Serviços de transporte aquaviário costeiro de cargas em contêineres frigorificados ou climatizados",
+ "105022320":
+ "Serviços de transporte aquaviário costeiro de cargas em contêineres não classificados em subposições anteriores",
+ "105022410": "Serviços de transporte aquaviário costeiro de cargas vivas",
+ "105022420":
+ "Serviços de transporte aquaviário costeiro de mudanças domésticas e de mobília e outros objetos de escritório",
+ "105022430": "Serviços de transporte aquaviário costeiro de cargas de grande porte",
+ "105022440": "Serviços de transporte aquaviário costeiro de veículos",
+ "105022451":
+ "Serviços de transporte aquaviário costeiro de combustíveis, lubrificantes e GLP, inclusive apresentado em botijões metálicos",
+ "105022452":
+ "Serviços de transporte aquaviário costeiro de produtos químicos perigosos, exceto lubrificantes e GLP",
+ "105022459":
+ "Serviços de transporte aquaviário costeiro de produtos perigosos não classificados em subposições anteriores",
+ "105022900":
+ "Serviços de transportes aquaviário costeiro de cargas não classificados em subposições anteriores",
+ "105023110": "Serviços de transporte aquaviário transoceânico de cargas sólidas a granel",
+ "105023120":
+ "Serviços de transporte aquaviário transoceânico de cargas líquidas, ou liquefeitas, a granel",
+ "105023130": "Serviços de transporte aquaviário transoceânico de cargas gasosas a granel",
+ "105023210": "Serviços de transporte aquaviário transoceânico de carga solta, não unitizada",
+ "105023220": "Serviços de transporte aquaviário transoceânico de carga unitizada",
+ "105023230":
+ "Serviços de transporte aquaviário transoceânico de carga frigorificada ou climatizada",
+ "105023310":
+ "Serviços de transporte aquaviário transoceânico de cargas em contêineres frigorificados ou climatizados",
+ "105023320":
+ "Serviços de transporte aquaviário transoceânico de cargas em contêineres não frigorificados ou climatizados",
+ "105023410": "Serviços de transporte aquaviário transoceânico de cargas vivas",
+ "105023420":
+ "Serviços de transporte aquaviário transoceânico de mudanças domésticas e de mobília e outros objetos de escritório",
+ "105023430": "Serviços de transporte aquaviário transoceânico de cargas de grande porte",
+ "105023440": "Serviços de transporte aquaviário transoceânico de veículos",
+ "105023451":
+ "Serviços de transporte aquaviário transoceânico de combustíveis, lubrificantes e GLP, inclusive apresentado em botijões metálicos",
+ "105023452":
+ "Serviços de transporte aquaviário transoceânico de produtos químicos perigosos, exceto lubrificantes e GLP",
+ "105023459":
+ "Serviços de transporte aquaviário transoceânico de produtos perigosos não classificados em subposições anteriores",
+ "105023900":
+ "Serviços de transporte aquaviário transoceânico de cargas não classificados em subposições anteriores",
+ "105031100":
+ "Serviços de transporte aéreo de cargas em contêineres frigorificados ou climatizados",
+ "105031200":
+ "Serviços de transporte aéreo de cargas em contêineres não frigorificados ou climatizados",
+ "105032100": "Serviços de transporte aéreo de produtos perigosos",
+ "105032200": "Serviços de transporte aéreo de cargas vivas",
+ "105032300": "Serviços de transporte aéreo de máquinas e veículos",
+ "105032400": "Serviços de transporte aéreo de produtos perecíveis",
+ "105032500": "Serviços de transporte aéreo de cargas frágeis",
+ "105032600": "Serviços de transporte aéreo de cargas controladas",
+ "105032700": "Serviços de transporte aéreo de valores",
+ "105032800":
+ "Serviços de transporte aéreo de cargas postais, remessas expressas e cargas congêneres",
+ "105032900":
+ "Serviços de transporte aéreo de cargas especiais não classificados em subposições anteriores",
+ "105039000": "Serviços de transporte aéreo de cargas não classificados em subposições anteriores",
+ "105041100": "Serviços de transporte multimodal de cargas sólidas a granel",
+ "105041200": "Serviços de transporte multimodal de cargas líquidas, ou liquefeitas, a granel",
+ "105041300": "Serviços de transporte multimodal de cargas gasosas a granel",
+ "105042100": "Serviços de transporte multimodal de carga solta, não unitizada",
+ "105042200": "Serviços de transporte multimodal de carga unitizada",
+ "105042300": "Serviços de transporte multimodal de carga frigorificada ou climatizada",
+ "105043100":
+ "Serviços de transporte multimodal de cargas em contêineres frigorificados ou climatizados",
+ "105043200":
+ "Serviços de transporte multimodal de cargas em contêineres não frigorificados ou climatizados",
+ "105044100": "Serviços de transporte multimodal de cargas vivas",
+ "105044200":
+ "Serviços de transporte multimodal de mudanças domésticas e de mobília e outros objetos de escritório",
+ "105044300": "Serviços de transporte multimodal de cargas de grande porte",
+ "105044400": "Serviços de transporte multimodal de veículos",
+ "105044510":
+ "Serviços de transporte multimodal de combustíveis, lubrificantes e GLP, inclusive apresentado em botijões metálicos",
+ "105044520":
+ "Serviços de transporte multimodal de produtos químicos perigosos, exceto lubrificantes e GLP",
+ "105044590":
+ "Serviços de transporte multimodal de produtos perigosos não classificados em subposições anteriores",
+ "105044900":
+ "Serviços de transporte multimodal de cargas especiais não classificados em subposições anteriores",
+ "105049000":
+ "Serviços de transporte multimodal de cargas não classificados em subposições anteriores",
+ "105051000": "Locação de veículos rodoviários de carga com operador",
+ "105052000": "Locação de embarcações de carga com tripulação",
+ "105053000": "Locação de aeronaves de carga com tripulação",
+ "105060000": "Afretamento de embarcações de carga por tempo",
+ "106011000": "Serviços de manuseio de contêineres",
+ "106019000": "Serviços de manuseio de cargas não classificados em subposições anteriores",
+ "106021000": "Serviços de armazenagem frigorificada",
+ "106022100": "Serviços de armazenagem de petróleo e seus derivados",
+ "106022200":
+ "Serviços de armazenagem de combustíveis, lubrificantes e GLP, inclusive apresentado em botijões metálicos",
+ "106022300": "Serviços de armazenagem de produtos químicos perigosos",
+ "106022900":
+ "Serviços de armazenagem de produtos perigosos não classificados em subposições anteriores",
+ "106023100": "Serviços de armazenagem de granéis sólidos",
+ "106023200": "Serviços de armazenagem de granéis líquidos ou liquefeitos",
+ "106023300": "Serviços de armazenagem de granéis gasosos",
+ "106029000": "Serviços de armazenagem não classificados em subposições anteriores",
+ "106030000": "Serviços de apoio ao transporte ferroviário",
+ "106041000": "Serviços de estações rodoviárias",
+ "106042100": "Serviços de operação de rodovias",
+ "106042200": "Serviços de operação de pontes e túneis",
+ "106043000": "Serviços de estacionamento",
+ "106044000": "Serviços de reboque para veículos particulares e comerciais",
+ "106049000":
+ "Serviços de apoio ao transporte rodoviário não classificados em subposições anteriores",
+ "106051000": "Serviços de operação de portos e canais, exceto manuseio de cargas",
+ "106052000": "Serviços de praticagem e de atracação",
+ "106053000": "Serviços de salvamento de embarcações",
+ "106054000": "Serviços de navegação de apoio",
+ "106059000":
+ "Serviços de apoio ao transporte aquaviário não classificados em subposições anteriores",
+ "106061100": "Serviços de operação aeroportuária, exceto manuseio de cargas",
+ "106061200": "Serviços de controle de tráfego aéreo",
+ "106061900": "Serviços de apoio ao transporte aéreo não classificados em subposições anteriores",
+ "106062000": "Serviços de apoio ao transporte aeroespacial",
+ "106070000": "Serviços de agenciamento de transporte de cargas",
+ "106081000": "Serviços de coleta e entrega de cargas no transporte multimodal",
+ "106082000": "Serviços de unitização ou desunitização de cargas no transporte multimodal",
+ "106083000": "Serviços de movimentação de cargas no transporte multimodal",
+ "106084000":
+ "Serviços de consolidação ou desconsolidação documental de cargas no transporte multimodal",
+ "106089000":
+ "Serviços de apoio ao transporte multimodal de cargas não classificados em subposições anteriores",
+ "106090000": "Serviços de apoio aos transportes não classificados em posições anteriores",
+ "107010000": "Serviços postais e de telegrama",
+ "107020000":
+ "Serviços de coleta, transporte, remessa ou entrega de documentos ou encomendas, exceto remessas expressas",
+ "107030000": "Serviços de remessas expressas",
+ "108011000": "Serviços de transmissão de eletricidade",
+ "108012000": "Serviços de distribuição de eletricidade",
+ "108021000":
+ "Serviços de distribuição de água por meio de tubulações, exceto vapor de água e água quente",
+ "108022000":
+ "Serviços de distribuição de vapor de água, água quente e ar condicionado por meio de tubulações",
+ "108023000": "Serviços de distribuição de água, exceto por meio de tubulações",
+ "108030000": "Serviços de distribuição de gás canalizado",
+ "109011000": "Serviços de banco central",
+ "109012100": "Serviços de depósito para pessoas jurídicas",
+ "109012200": "Serviços de depósito para pessoas físicas",
+ "109012900": "Serviços de depósito para outros depositantes",
+ "109013100": "Serviços de financiamentos imobiliários residenciais",
+ "109013200": "Serviços de financiamentos imobiliários não residenciais",
+ "109013300": "Serviços de empréstimos e financiamentos pessoais",
+ "109013400": "Serviços de empréstimos e financiamentos comerciais",
+ "109013500": "Serviços de empréstimos e financiamentos industriais",
+ "109013600": "Serviços de empréstimos e financiamentos agropecuários",
+ "109013900": "Serviços de concessão de crédito não classificados em subposições anteriores",
+ "109014000": "Serviços de cartão de crédito",
+ "109015111":
+ "Arrendamento mercantil financeiro de veículos rodoviários automotores para o transporte de passageiros",
+ "109015112":
+ "Arrendamento mercantil financeiro de veículos rodoviários automotores para o transporte de mercadorias",
+ "109015113": "Arrendamento mercantil financeiro de veículos e equipamentos ferroviários",
+ "109015114":
+ "Arrendamento mercantil financeiro de outros equipamentos de transporte terrestre, inclusive de veículos de uso misto",
+ "109015115": "Arrendamento mercantil financeiro de navios e outras embarcações",
+ "109015116": "Arrendamento mercantil financeiro de aeronaves",
+ "109015117": "Arrendamento mercantil financeiro de contêineres",
+ "109015121": "Arrendamento mercantil financeiro de máquinas e equipamentos agrícolas",
+ "109015122": "Arrendamento mercantil financeiro de máquinas e equipamentos de construção",
+ "109015123":
+ "Arrendamento mercantil financeiro de máquinas e equipamentos para escritórios, exceto computadores",
+ "109015124": "Arrendamento mercantil financeiro de computadores",
+ "109015125": "Arrendamento mercantil financeiro de equipamentos de telecomunicação",
+ "109015129":
+ "Arrendamento mercantil financeiro de outras máquinas e equipamentos não classificado em subposições anteriores",
+ "109015210":
+ "Arrendamento mercantil financeiro de televisões e outros eletroeletrônicos domésticos, bem como seus acessórios",
+ "109015220": "Arrendamento mercantil financeiro de mídias gravadas",
+ "109015230": "Arrendamento mercantil financeiro de móveis e eletrodomésticos",
+ "109015240": "Arrendamento mercantil financeiro de equipamentos para diversão e lazer",
+ "109015250": "Arrendamento mercantil financeiro de artigos de cama, mesa e banho",
+ "109015290":
+ "Arrendamento mercantil financeiro de outras mercadorias não classificado em subposições anteriores",
+ "109019000":
+ "Serviços financeiros, exceto bancos de investimento, serviços de seguros e previdência complementar não classificados em subposições anteriores",
+ "109021000": "Serviços de valoração de ativos",
+ "109022000": "Serviços de subscrição de valores mobiliários",
+ "109023000": "Serviços de fusões e aquisições",
+ "109024000": "Serviços de capital de risco e finanças corporativas",
+ "109029000": "Serviços de banco de investimento não classificados em subposições anteriores",
+ "109031100": "Serviços de seguro de vida",
+ "109031200": "Serviços de previdência complementar aberta",
+ "109031300": "Serviços de previdência complementar fechada",
+ "109032100": "Serviços de seguro saúde",
+ "109032200": "Serviços de seguro de acidentes",
+ "109033100": "Serviços de seguro de veículos rodoviários",
+ "109033200":
+ "Serviços de seguro de veículos e equipamentos para transporte ferroviário, aquaviário e aéreo",
+ "109033300": "Serviços de seguro de cargas",
+ "109033400": "Serviços de seguro de outras propriedades",
+ "109033500": "Serviços de seguro por responsabilidade civil",
+ "109033600": "Serviços de seguro de crédito e de caução",
+ "109033700": "Serviços de seguro de viagem",
+ "109033800": "Serviços de seguro rural",
+ "109033900":
+ "Serviços de outros seguros, excluídos os serviços de resseguro não classificados em subposições anteriores",
+ "109041000": "Serviços de resseguro de vida",
+ "109042100": "Serviços de resseguro saúde",
+ "109042200": "Serviços de resseguro de acidentes",
+ "109043100": "Serviços de resseguro de veículos rodoviários",
+ "109043200":
+ "Serviços de resseguro de veículos e equipamentos para transporte ferroviário, aquaviário e aéreo",
+ "109043300": "Serviços de resseguro de cargas",
+ "109043400": "Serviços de resseguro de outras propriedades",
+ "109043500": "Serviços de resseguro de responsabilidade civil",
+ "109043600": "Serviços de resseguro de crédito e caução",
+ "109043700": "Serviços de resseguro rural",
+ "109043900": "Serviços de outros resseguros não classificados em subposições anteriores",
+ "109044000": "Serviços de retrocessão",
+ "109051100": "Serviços de corretagem de títulos",
+ "109051200": "Serviços de corretagem de derivativos e commodities",
+ "109051300":
+ "Serviços de compensação de transações financeiras, inclusive com ativos financeiros (clearinghouse)",
+ "109052100": "Serviços de gestão e administração de carteiras de ativos, exceto fundos de pensão",
+ "109052200": "Serviços de administração fundos de investimento e fundos de pensão",
+ "109052300": "Serviços de gestão e administração de trust",
+ "109053000": "Serviços de guarda e custódia",
+ "109054000": "Serviços relacionados à administração de mercados financeiros",
+ "109055000": "Serviços de consultoria financeira",
+ "109056000": "Serviços de câmbio",
+ "109057000": "Serviços de classificação de risco (rating)",
+ "109058000": "Serviços fiduciários",
+ "109059000":
+ "Serviços auxiliares aos serviços financeiros não classificados em subposições anteriores",
+ "109061100":
+ "Serviços de agenciamento e corretagem de seguros, resseguros e previdência complementar, exceto de seguros saúde",
+ "109061200": "Serviços de corretagem de seguros saúde",
+ "109062000": "Serviços de perícia e avaliação de seguros e resseguros",
+ "109063000": "Serviços atuariais",
+ "109064000": "Serviços de gestão de fundos de previdência complementar",
+ "109069000":
+ "Serviços auxiliares a seguros, resseguros e previdência complementar não classificados em subposições anteriores",
+ "109070000": "Serviços de seguro de recebíveis",
+ "109080000": "Fomento comercial (factoring)",
+ "109091000": "Serviços de holding de patrimônio de empresas subsidiárias",
+ "109092000":
+ "Serviços de holding de valores mobiliários e outros ativos financeiros e de fundos e entidades financeiras similares",
+ "109101000": "Serviços de planos privados de assistência à saúde",
+ "109102000": "Serviços de corretagem de planos privados de assistência à saúde",
+ "109109000":
+ "Serviços de planos privados de assistência à saúde e serviços relacionados não classificados em subposições anteriores",
+ "109110000":
+ "Serviços financeiros e serviços relacionados não classificados em posições anteriores",
+ "110011100": "Serviços de administração e locação de imóveis residenciais",
+ "110011210":
+ "Serviços de administração e locação, sublocação, arrendamento, direito de passagem ou permissão de uso, compartilhado ou não, de ferrovia, rodovia, postes, cabos, dutos e condutos de qualquer natureza",
+ "110011290": "Serviços de administração e locação de outros imóveis não residenciais",
+ "110012100": "Serviços de intermediação na compra e venda de imóveis residenciais",
+ "110012200": "Serviços de intermediação na compra e venda de imóveis não residenciais",
+ "110013000": "Serviços de avaliação de imóveis",
+ "110014000": "Serviços de consultoria imobiliária",
+ "110015000": "Serviços de assessoria de gestão condominial",
+ "110019000": "Serviços imobiliários não classificados em subposições anteriores",
+ "110021000": "Locação de imóveis residenciais",
+ "110022000": "Locação de imóveis não residenciais",
+ "111011100":
+ "Arrendamento mercantil operacional ou locação de veículos rodoviários automotores para o transporte de até oito passageiros, sem operador",
+ "111011200":
+ "Arrendamento mercantil operacional ou locação de veículos rodoviários automotores para o transporte de cargas, sem operador",
+ "111011300":
+ "Arrendamento mercantil operacional ou locação de veículos e equipamentos de transporte ferroviário, sem operador",
+ "111011400":
+ "Arrendamento mercantil operacional ou locação de outros equipamentos de transporte terrestre, inclusive de veículos de uso misto, sem operador",
+ "111011500":
+ "Arrendamento mercantil operacional ou locação de navios e outras embarcações, sem tripulação",
+ "111011600": "Arrendamento mercantil operacional ou locação de aeronaves, sem tripulação",
+ "111011700": "Arrendamento mercantil operacional ou locação de contêineres",
+ "111012000":
+ "Arrendamento mercantil operacional ou locação de máquinas e equipamentos agrícolas, sem operador",
+ "111013000":
+ "Arrendamento mercantil operacional ou locação de máquinas e equipamentos de construção, sem operador",
+ "111014000":
+ "Arrendamento mercantil operacional ou locação de máquinas e equipamentos para escritórios, exceto computadores, sem operador",
+ "111015000": "Arrendamento mercantil operacional ou locação de computadores, sem operador",
+ "111016000":
+ "Arrendamento mercantil operacional ou locação de equipamentos de telecomunicação, sem operador",
+ "111019000":
+ "Arrendamento mercantil operacional ou locação de máquinas e equipamentos, sem operador, não classificado em subposições anteriores",
+ "111021000":
+ "Arrendamento mercantil operacional ou locação de televisão e outros eletroeletrônicos, bem como seus acessórios",
+ "111022000": "Arrendamento mercantil operacional ou locação de mídias gravadas",
+ "111023000": "Arrendamento mercantil operacional ou locação de móveis e eletrodomésticos",
+ "111024000":
+ "Arrendamento mercantil operacional ou locação de equipamentos para diversão e lazer",
+ "111025000": "Arrendamento mercantil operacional ou locação de artigos de cama, mesa e banho",
+ "111026000": "Arrendamento mercantil operacional ou locação de roupas e calçados",
+ "111029000":
+ "Arrendamento mercantil operacional ou locação de outras mercadorias não classificado em subposições anteriores",
+ "111031000": "Licenciamento de direitos de obras literárias",
+ "111032100":
+ "Licenciamento de direitos de produção, distribuição ou comercialização de programas de computador (software)",
+ "111032200": "Licenciamento de direitos de uso de programas de computador (software)",
+ "111032300": "Licenciamento de direitos sobre bancos de dados",
+ "111032900":
+ "Licenciamento de direitos sobre programas de computador (software) e bancos de dados não classificado em subposições anteriores",
+ "111033100": "Licenciamento de direitos de autor de obras cinematográficas",
+ "111033200": "Licenciamento de direitos de autor de obras jornalísticas",
+ "111033300": "Licenciamento de direitos de autor de obras publicitárias",
+ "111033400":
+ "Licenciamento de direitos conexos de artistas intérpretes ou executantes em obras audiovisuais",
+ "111033500": "Licenciamento de direitos conexos de produtores de obras audiovisuais",
+ "111033610":
+ "Licenciamento de direitos de obras audiovisuais sobre transmissões de eventos esportivos",
+ "111033620":
+ "Licenciamento de direitos de obras audiovisuais sobre transmissões de programas televisivos",
+ "111033690":
+ "Licenciamento de direitos de obras audiovisuais sobre outras transmissões televisivas",
+ "111033900":
+ "Licenciamento de direitos de obras audiovisuais não classificado em subposições anteriores",
+ "111034100": "Licenciamento de direitos de autor de obras musicais ou literomusicais",
+ "111034200": "Licenciamento de direitos conexos de artistas intérpretes ou executantes",
+ "111034300": "Licenciamento de direitos conexos de produtores de fonogramas",
+ "111035000": "Licenciamento de direitos relacionados à radiodifusão",
+ "111039000":
+ "Licenciamento de direitos de autor e de direitos conexos não classificado em subposições anteriores",
+ "111041000": "Licenciamento de direitos sobre patentes",
+ "111042000": "Licenciamento de direitos sobre marcas",
+ "111043000": "Licenciamento de direitos sobre desenho industrial",
+ "111049000":
+ "Licenciamento de direitos sobre a propriedade industrial não classificado em subposições anteriores",
+ "111051000": "Licenciamento de direitos sobre cultivares",
+ "111052000": "Licenciamento de direitos sobre topografias de circuitos integrados",
+ "111053000": "Licenciamento de direitos relativos à informação não divulgada",
+ "111054100": "Exploração de recursos vegetais, inclusive florestais",
+ "111054200": "Exploração de recursos minerais",
+ "111055100":
+ "Licenciamento de direitos sobre conhecimento tradicional associado a recursos genéticos",
+ "111055900":
+ "Licenciamento de direitos sobre conhecimento tradicional não classificado em subposições anteriores",
+ "111056000":
+ "Licenciamento de direitos relativos ao acesso a recursos genéticos, exceto os decorrentes do conhecimento tradicional",
+ "111057000":
+ "Licenciamento de direitos sobre informações relativas à experiência adquirida no setor industrial, comercial ou científico (know-how)",
+ "111059000": "Licenciamento de outros direitos não classificado em subposições anteriores",
+ "111061000": "Cessão temporária de direitos de obras literárias",
+ "111062000": "Cessão temporária de direitos sobre programas de computador (software)",
+ "111063100": "Cessão temporária de direitos de autor de obras cinematográficas",
+ "111063200": "Cessão temporária de direitos de autor de obras jornalísticas",
+ "111063300": "Cessão temporária de direitos de autor de obras publicitárias",
+ "111063400":
+ "Cessão temporária de direitos conexos de artistas intérpretes ou executantes em obras audiovisuais",
+ "111063500": "Cessão temporária de direitos conexos de produtores de obras audiovisuais",
+ "111063610":
+ "Cessão temporária de direitos de obras audiovisuais sobre transmissões de eventos esportivos",
+ "111063620":
+ "Cessão temporária de direitos de obras audiovisuais sobre transmissões de programas televisivos",
+ "111063690":
+ "Cessão temporária de direitos de obras audiovisuais sobre outras transmissões televisivas",
+ "111063900":
+ "Cessão temporária de direitos de obras audiovisuais não classificada em subposições anteriores",
+ "111064100": "Cessão temporária de direitos de autor de obras musicais e literomusicais",
+ "111064200": "Cessão temporária de direitos conexos de artistas intérpretes ou executantes",
+ "111064300": "Cessão temporária de direitos conexos de produtores de fonogramas",
+ "111065000": "Cessão temporária de direitos relacionados à radiodifusão",
+ "111069000":
+ "Cessão temporária de direitos de autor e de direitos conexos não classificada em subposições anteriores",
+ "111071000": "Cessão definitiva de direitos de obras literárias",
+ "111072000": "Cessão definitiva de direitos sobre programas de computador (software)",
+ "111073100": "Cessão definitiva de direitos de obras cinematográficas",
+ "111073200": "Cessão definitiva de direitos de obras jornalísticas",
+ "111073300": "Cessão definitiva de direitos de obras publicitárias",
+ "111073900":
+ "Cessão definitiva de direitos de obras audiovisuais não classificada em subposições anteriores",
+ "111074000": "Cessão definitiva de direitos de obras musicais e fonogramas",
+ "111075000": "Cessão definitiva de direitos relacionados à radiodifusão",
+ "111079000":
+ "Cessão definitiva de direitos de autor e de direitos conexos não classificada em subposições anteriores",
+ "111081000": "Cessão definitiva de direitos sobre patentes",
+ "111082000": "Cessão definitiva de direitos sobre marcas",
+ "111083000": "Cessão definitiva de direitos sobre desenho industrial",
+ "111089000":
+ "Cessão definitiva de direitos sobre a propriedade industrial não classificada em subposições anteriores",
+ "111091000": "Cessão definitiva de direitos sobre cultivares",
+ "111092000": "Cessão definitiva de direitos sobre topografias de circuitos integrados",
+ "111093000": "Cessão definitiva de direitos relativos à informação não divulgada",
+ "111099000": "Cessão definitiva de outros direitos não classificada em subposições anteriores",
+ "111100000": "Franquia",
+ "112011100": "Serviços de pesquisa e desenvolvimento em ciências físicas",
+ "112011200": "Serviços de pesquisa e desenvolvimento em química e biologia",
+ "112011900":
+ "Serviços de pesquisa e desenvolvimento em ciências não classificadas em subposições anteriores",
+ "112012000": "Serviços de pesquisa e desenvolvimento em biotecnologia",
+ "112013100":
+ "Serviços de pesquisa e desenvolvimento em Tecnologia da Informação e Comunicação (TIC)",
+ "112013200": "Serviços de pesquisa e desenvolvimento em nanotecnologia",
+ "112013300": "Serviços de pesquisa e desenvolvimento em engenharia e tecnologia nucleares",
+ "112013400":
+ "Serviços de pesquisa e desenvolvimento em engenharia e tecnologia em micro-ondas de potência",
+ "112013900":
+ "Serviços de pesquisa e desenvolvimento em engenharia e tecnologia não classificados em subposições anteriores",
+ "112014000":
+ "Serviços de pesquisa e desenvolvimento em ciências médicas, odontológicas e farmacêuticas",
+ "112015000": "Serviços de pesquisa e desenvolvimento em ciências agrárias",
+ "112019000":
+ "Serviços de pesquisa e desenvolvimento em ciências, engenharia e tecnologia não classificados em subposições anteriores",
+ "112021000": "Serviços de pesquisa e desenvolvimento em psicologia",
+ "112022000": "Serviços de pesquisa e desenvolvimento em ciências econômicas",
+ "112023000": "Serviços de pesquisa e desenvolvimento em direito",
+ "112024000": "Serviços de pesquisa e desenvolvimento em línguas e literatura",
+ "112029000":
+ "Serviços de pesquisa e desenvolvimento em ciências sociais e humanidades não classificadas em subposições anteriores",
+ "112030000": "Serviços de pesquisa e desenvolvimento interdisciplinar",
+ "113011000": "Serviços de representação e consultoria jurídica criminal",
+ "113012000":
+ "Serviços de representação e consultoria jurídica em outras áreas do direito, exceto consultoria tributária",
+ "113013000":
+ "Serviços de documentação e certificação, exceto os serviços notariais e de registro",
+ "113014000": "Serviços de arbitragem, conciliação e mediação",
+ "113019000": "Serviços jurídicos não classificados em subposições anteriores",
+ "113021100": "Serviços de auditoria contábil",
+ "113021900": "Serviços de auditoria não classificados em subposições anteriores",
+ "113022100": "Serviços de contabilidade",
+ "113022200": "Serviços de escrituração mercantil",
+ "113022300": "Serviços de folha de pagamento",
+ "113031000": "Serviços de consultoria tributária para pessoas jurídicas",
+ "113032000": "Serviços de consultoria tributária para pessoas físicas",
+ "113040000": "Serviços notariais e de registro",
+ "114011100": "Serviços de consultoria em gestão estratégica",
+ "114011200": "Serviços de consultoria em gestão financeira",
+ "114011300": "Serviços de consultoria em gestão de recursos humanos",
+ "114011400": "Serviços de consultoria em gestão de marketing",
+ "114011500": "Serviços de consultoria em gestão operacional",
+ "114011600": "Serviços de consultoria em gestão energética",
+ "114011700": "Serviços de consultoria em gestão de cadeia logística",
+ "114011800": "Serviços de consultoria em gestão hospitalar",
+ "114011900":
+ "Serviços de consultoria em gestão empresarial não classificados em subposições anteriores",
+ "114012100": "Serviços de gestão em processos de negócios",
+ "114012200": "Serviços de gestão hospitalar",
+ "114012900": "Serviços de gestão não classificados em subposições anteriores",
+ "114013100": "Serviços de assessoria de imprensa",
+ "114013200": "Serviços de relações públicas",
+ "114013900": "Serviços de assessoria empresarial não classificados em subposições anteriores",
+ "114021100": "Serviços de consultoria em arquitetura",
+ "114021200": "Serviços de arquitetura para projetos de construções residenciais",
+ "114021300": "Serviços de arquitetura para projetos de construções não residenciais",
+ "114021400": "Serviços de arquitetura para restauração de prédios históricos",
+ "114021500":
+ "Serviços de arquitetura relativos ao acompanhamento e fiscalização da execução de projetos arquitetônicos e urbanísticos",
+ "114022100": "Serviços de planejamento urbano",
+ "114022200": "Serviços de planejamento de áreas rurais",
+ "114023100": "Serviços de consultoria em paisagismo",
+ "114023200": "Serviços de paisagismo, exceto consultoria",
+ "114029000":
+ "Serviços de arquitetura, de planejamento urbano e de áreas rurais e de paisagismo não classificados em subposições anteriores",
+ "114031000": "Serviços de consultoria em engenharia",
+ "114032110": "Serviços de engenharia para projetos de construção residencial",
+ "114032120": "Serviços de engenharia para projetos de construção não residencial",
+ "114032211": "Serviços de engenharia para projetos de exploração de minerais",
+ "114032212": "Serviços de engenharia para projetos de exploração de petróleo e gás",
+ "114032213": "Serviços de engenharia para projetos de refino de petróleo e petroquímica",
+ "114032214": "Serviços de engenharia para projetos de unidades de produção de biocombustíveis",
+ "114032221": "Serviços de engenharia para projetos de veículos terrestres",
+ "114032222": "Serviços de engenharia para projetos de embarcações",
+ "114032223": "Serviços de engenharia para projetos de veículos aéreos e aeroespaciais",
+ "114032290":
+ "Serviços de engenharia para outros projetos industriais e de fabricação, exceto para projetos de energia",
+ "114032300": "Serviços de engenharia para projetos de infraestrutura de transportes",
+ "114032400": "Serviços de engenharia para projetos de energia",
+ "114032500": "Serviços de engenharia para projetos de telecomunicações, radiodifusão e televisão",
+ "114032600":
+ "Serviços de engenharia para projetos de gerenciamento de resíduos (perigosos e não perigosos)",
+ "114032700": "Serviços de engenharia para projetos de distribuição de água e rede de esgoto",
+ "114032900": "Serviços de engenharia para outros projetos",
+ "114033000": "Serviços de gerenciamento de projetos de construção",
+ "114039000": "Serviços de engenharia não classificados em subposições anteriores",
+ "114041100": "Serviços de consultoria geológica e geofísica",
+ "114041200": "Serviços geofísicos",
+ "114041300": "Serviços geoquímicos",
+ "114041400": "Serviços de informações para avaliação e exploração de recursos naturais",
+ "114041900":
+ "Serviços geológicos, geofísicos e outros de prospecção não classificados em subposições anteriores",
+ "114042100": "Serviços topográficos",
+ "114042200": "Serviços cartográficos",
+ "114043000": "Serviços meteorológicos e de previsão do tempo",
+ "114044100": "Serviços de análise e de exames técnicos sobre pureza e composição",
+ "114044200": "Serviços de análise e de exames técnicos de propriedades físicas",
+ "114044300": "Serviços de análise e de exames técnicos de sistemas elétricos e mecânicos",
+ "114044400": "Serviços de inspeção técnica de veículos de transporte rodoviário",
+ "114044900":
+ "Serviços de análise e de exames técnicos não classificados em subposições anteriores",
+ "114051100": "Serviços hospitalares, com ou sem internação, para animais domésticos",
+ "114051200": "Serviços de atendimento, assistência ou tratamento para animais domésticos",
+ "114052100": "Serviços hospitalares, com ou sem internação, para animais de corte",
+ "114052200": "Serviços de atendimento, assistência ou tratamento para animais de corte",
+ "114053000": "Serviços funerários, de cremação e de embalsamamento de animais",
+ "114054000":
+ "Serviços de bancos de órgãos, sangue, sêmen, tecidos, óvulos e outros materiais biológicos",
+ "114055000": "Planos de atendimento e assistência médico-veterinária",
+ "114056000": "Serviços de guarda, adestramento, embelezamento e alojamento",
+ "114059000": "Serviços veterinários não classificados em subposições anteriores",
+ "114061100": "Serviços de campanhas publicitárias",
+ "114061200": "Serviços de marketing direto e mala direta",
+ "114061900": "Serviços de propaganda não classificados em subposições anteriores",
+ "114062000": "Aquisição ou venda de espaço ou tempo para propaganda, sob comissão",
+ "114063100": "Venda de espaço para propaganda em mídia impressa, exceto sob comissão",
+ "114063200": "Venda de tempo para propaganda em rádio e televisão, exceto sob comissão",
+ "114063300":
+ "Venda de espaço para propaganda na rede mundial de computadores, exceto sob comissão",
+ "114063400": "Venda de espaço para propaganda em mídia exterior, exceto sob comissão",
+ "114063900":
+ "Venda de espaço ou tempo para propaganda, exceto sob comissão não classificados em subposições anteriores",
+ "114070000": "Pesquisas de mercado e serviços de pesquisa de opinião pública",
+ "114081100": "Serviços fotográficos de retratos",
+ "114081200": "Serviços fotográficos e videográficos para propaganda",
+ "114081300": "Serviços fotográficos e videográficos de eventos",
+ "114081400": "Serviços fotográficos especiais",
+ "114081500": "Serviços de restauração e retoque de fotografias",
+ "114081900": "Serviços fotográficos e videográficos não classificados em subposições anteriores",
+ "114082000": "Serviços de processamento de fotografias",
+ "114091100": "Serviços de design de interiores para espaços comerciais e públicos",
+ "114091200": "Serviços de design de interiores para espaços residenciais",
+ "114092100":
+ "Serviços de desenho industrial de embalagens, expositores de loja e objetos promocionais para comunicação e vendas",
+ "114092200":
+ "Serviços de desenho industrial de produtos, utensílios, equipamentos, vestuário, calçados, ornamentos, joias e objetos pessoais",
+ "114092300":
+ "Serviços de desenho industrial de máquinas, equipamentos, acessórios e objetos de uso industrial de qualquer natureza",
+ "114092400": "Serviços de desenho industrial de mobiliários e itens de decoração",
+ "114092500":
+ "Serviços de desenho industrial de utensílios e equipamentos eletrodomésticos e eletroeletrônicos",
+ "114092900": "Serviços de desenho industrial não classificados em subposições anteriores",
+ "114093000": "Serviços de design de marcas, imagens, objetos gráficos e digitais",
+ "114099000": "Serviços especializados de design não classificados em subposições anteriores",
+ "114101000": "Serviços de consultoria ambiental",
+ "114109000":
+ "Serviços de consultoria técnica e científica não classificados em subposições anteriores",
+ "114110000": "Serviços de tradução e de intérpretes",
+ "114120000":
+ "Serviços para registros de marcas comerciais e de franquias empresariais, exceto as licenças de uso de direito",
+ "114130000": "Serviços de prospecção de clientes",
+ "114140000": "Serviços de prospecção de fornecedores",
+ "114150000":
+ "Serviços profissionais, técnicos e gerenciais não classificados em posições anteriores",
+ "115011000": "Serviços de consultoria em tecnologia da informação (TI)",
+ "115012000": "Serviços de segurança em tecnologia da informação (TI)",
+ "115013000": "Serviços de suporte em tecnologia da informação (TI)",
+ "115021000":
+ "Serviços de projeto, desenvolvimento e instalação de aplicativos e programas não personalizados (não customizados)",
+ "115022000":
+ "Serviços de projeto e desenvolvimento, adaptação e instalação de aplicativos e programas personalizados (customizados)",
+ "115023000":
+ "Serviços de projeto e desenvolvimento de estruturas e conteúdo de páginas eletrônicas",
+ "115024000": "Serviços de projeto e desenvolvimento de estruturas e conteúdo de bancos de dados",
+ "115025000": "Serviços de integração de sistemas em tecnologia da informação (TI)",
+ "115029000":
+ "Serviços de projeto e desenvolvimento de aplicativos e programas em tecnologia da informação (TI) não classificados em subposições anteriores",
+ "115030000": "Serviços de projeto e desenvolvimento de redes em tecnologia da informação (TI)",
+ "115040000": "Serviços de projeto e desenvolvimento de topografias de circuitos integrados",
+ "115050000": "Serviços de projeto de circuitos integrados",
+ "115061000": "Serviços de hospedagem de sítios eletrônicos na rede mundial de computadores",
+ "115062100": "Serviços de hospedagem de aplicativos e programas software como serviço (SaaS)",
+ "115062200": "Serviços de fornecimento de infraestrutura como serviço (IaaS)",
+ "115062300": "Serviços de fornecimento de plataformas como serviço (PaaS)",
+ "115062900":
+ "Serviços de hospedagem de aplicativos e programas não classificados em subposições anteriores",
+ "115069000":
+ "Serviços de hospedagem e de disponibilização de infraestrutura em tecnologia da informação (TI) não classificados em subposições anteriores",
+ "115071000": "Serviços de gerenciamento de redes em tecnologia da informação (TI)",
+ "115072000": "Serviços de gerenciamento de sistemas computacionais",
+ "115079000":
+ "Serviços de gerenciamento de infraestrutura em tecnologia da informação (TI) não classificados em subposições anteriores",
+ "115080000": "Serviços de manutenção de aplicativos e programas",
+ "115090000": "Serviços de processamento de dados",
+ "115100000":
+ "Serviços de tecnologia da informação (TI) não classificados em subposições anteriores",
+ "117011100": "Serviços de interconexão pelo uso da rede fixa",
+ "117011200": "Serviços de interconexão pelo uso de rede móvel",
+ "117011900": "Serviços de operadoras não classificados em subposições anteriores",
+ "117012100": "Serviços de chamadas de telecomunicações fixas comutadas",
+ "117012900": "Serviços de telecomunicações fixas não classificados em subposições anteriores",
+ "117013100": "Serviços de voz nas telecomunicações móveis",
+ "117013200": "Serviços de texto nas telecomunicações móveis",
+ "117013300":
+ "Serviços de transmissão de dados nas telecomunicações móveis, exceto serviços de texto",
+ "117013400": "Serviços de telecomunicações móveis para usuários visitantes (roaming)",
+ "117014000": "Serviços de redes privadas",
+ "117015100": "Serviços de transmissão de dados local, nacional ou internacional",
+ "117015200": "Serviços de exploração de linha dedicada local, nacional ou internacional",
+ "117019000":
+ "Serviços de telecomunicações, exceto pela rede mundial de computadores, não classificados em subposições anteriores",
+ "117021000":
+ "Serviços de fornecimento de infraestrutura de acesso (backbone) à rede mundial de computadores",
+ "117022100": "Serviços de acesso à rede mundial de computadores por banda estreita",
+ "117022200": "Serviços de acesso à rede mundial de computadores por banda larga",
+ "117029000":
+ "Serviços de telecomunicações pela rede mundial de computadores não classificados em subposições anteriores",
+ "117031000":
+ "Serviços de oferta de livros, jornais, periódicos, diretórios e malas diretas de acesso imediato (on-line)",
+ "117032100": "Serviços de oferta de áudio para download",
+ "117032200": "Serviços de oferta de áudio de conteúdo contínuo (streaming)",
+ "117033100": "Serviços de oferta de arquivos contendo filmes e vídeos para download",
+ "117033200": "Serviços de oferta de filmes e vídeos de conteúdo contínuo (streaming)",
+ "117039100": "Serviços de oferta de jogos de acesso imediato (on-line)",
+ "117039200": "Serviços de oferta de conteúdo de portais de busca na rede mundial de computadores",
+ "117039900":
+ "Serviços de oferta de outros conteúdos de acesso imediato (on-line) não classificados em subposições anteriores",
+ "117041000": "Serviços de agências de notícias para jornais e periódicos",
+ "117042000": "Serviços de agências de notícias para mídia audiovisual",
+ "117051000": "Serviços de biblioteca",
+ "117052000": "Serviços de arquivo",
+ "117061100": "Serviços de programação dos canais de rádio",
+ "117061200": "Serviços de programação dos canais de televisão",
+ "117062100":
+ "Serviços de transmissão de sinais, sons e imagens de rádio e televisão, aberta ou por assinatura",
+ "117062200":
+ "Serviços de distribuição de pacotes básicos de programação de televisão por assinatura",
+ "117062300":
+ "Serviços de distribuição de pacotes de programação adicional de televisão por assinatura",
+ "117062400":
+ '"Serviços de distribuição de programas de televisão por assinatura, na modalidade ""pague por exibição"" (pay-per-view)"',
+ "117069000":
+ "Serviços de difusão, programação e distribuição de programas de rádio e televisão não classificados em subposições anteriores",
+ "118011100": "Serviço de recrutamento e seleção de profissionais executivos",
+ "118011200": "Serviço de recrutamento e seleção de profissionais, exceto executivos",
+ "118012100": "Serviços de fornecimento de mão de obra terceirizada, exceto temporária",
+ "118012200": "Serviços de fornecimento de mão de obra temporária",
+ "118012900":
+ "Serviços de fornecimento de mão de obra não classificados em subposições anteriores",
+ "118021000": "Serviços de investigação",
+ "118022000": "Serviços de consultoria em segurança",
+ "118023000": "Serviços de sistemas de segurança",
+ "118024000": "Serviços de carro-forte",
+ "118025000": "Serviços de guarda e escolta armada",
+ "118029000": "Serviços de segurança não classificados em subposições anteriores",
+ "118031000": "Serviços gerais de limpeza",
+ "118032100": "Serviços de desinfecção e extermínio de pragas",
+ "118032200": "Serviços de limpeza de janelas",
+ "118032900": "Serviços especializados de limpeza não classificados em subposições anteriores",
+ "118040000": "Serviços de acondicionamento e empacotamento",
+ "118051100": "Serviços de reservas para transporte aéreo de passageiros",
+ "118051200": "Serviços de reservas para transporte ferroviário de passageiros",
+ "118051300": "Serviços de reservas para transporte rodoviário de passageiros",
+ "118051400": "Serviços de reservas de carros de aluguel",
+ "118051900":
+ "Serviços de reservas para transporte de passageiros não classificados em subposições anteriores",
+ "118052100": "Serviços de reservas de hospedagem, exceto em unidades compartilhadas",
+ "118052200": "Serviços de reservas e intercâmbio de unidades compartilhadas (time-share)",
+ "118052300": "Serviços de reservas em cruzeiros",
+ "118052400": "Serviços de reservas de pacotes turísticos",
+ "118053100": "Serviços de reservas para centros de convenções, auditórios e salas de exposições",
+ "118053200": "Serviços de reservas de ingressos para eventos de entretenimento e recreativos",
+ "118053900": "Serviços de reservas não classificados em subposições anteriores",
+ "118054000": "Serviços de operadoras de turismo",
+ "118055000": "Serviços de guias turísticos",
+ "118056100": "Serviços de promoção turística",
+ "118056200": "Serviços de informação a visitantes",
+ "118061000": "Serviços de informação cadastral e análise de crédito",
+ "118062000": "Serviços de cobrança",
+ "118063100": "Serviços de call center",
+ "118063900":
+ "Serviços de apoio às atividades empresariais por meio de telefone não classificados em subposições anteriores",
+ "118064000": "Serviços combinados de escritório e apoio administrativo",
+ "118065100": "Serviços de fotocópias e outros serviços de reprodução de documentos",
+ "118065200": "Serviços de execução e envio de mala direta e de elaboração de listas de endereços",
+ "118065300": "Serviços de preparação de documentos",
+ "118065900":
+ "Serviços especializados de apoio a escritório não classificados em subposições anteriores",
+ "118066100": "Serviços de assistência e organização de convenções",
+ "118066200": "Serviços de assistência e organização de feiras de negócios",
+ "118066300": "Serviços de assistência e organização de exposições e outros eventos",
+ "118067000": "Serviços de jardinagem",
+ "118068100": "Serviços de agenciamento de modelos",
+ "118068200": "Serviços de agenciamento de artistas",
+ "118068300": "Serviços de agenciamento de atletas",
+ "118069000": "Serviços de apoio não classificados em subposições anteriores",
+ "119011000": "Serviços de apoio à agricultura",
+ "119012000": "Serviços de apoio à pecuária",
+ "119013000": "Serviços de apoio à produção florestal (silvicultura)",
+ "119014000": "Serviços de apoio à pesca",
+ "119015000": "Serviços de apoio à aquicultura",
+ "119021000": "Serviços de apoio à extração de petróleo e gás",
+ "119029000": "Serviços de apoio à mineração não classificados em subposições anteriores",
+ "119031100": "Serviços de apoio à transmissão de eletricidade",
+ "119031200": "Serviços de apoio à distribuição de eletricidade",
+ "119032000": "Serviços de apoio à distribuição de gás por meio de tubulações",
+ "119033000":
+ "Serviços de apoio à distribuição de água por meio de tubulações, exceto vapor de água e água quente",
+ "119034000":
+ "Serviços de apoio à distribuição de vapor de água, água quente e ar condicionado por meio de tubulações",
+ "119035000": "Serviços de apoio à distribuição de água, exceto por meio de tubulações",
+ "120011000":
+ "Serviços de manutenção e reparação de produtos metálicos, exceto maquinários e equipamentos",
+ "120012000":
+ "Serviços de manutenção e reparação de computadores e seus periféricos e de maquinário para escritório",
+ "120013110": "Serviços de manutenção e reparação de veículos rodoviários motorizados",
+ "120013120": "Serviços de manutenção e reparação de veículos rodoviários não motorizados",
+ "120013200": "Serviços de manutenção e reparação de veículos sobre trilhos",
+ "120013300": "Serviços de manutenção e reparação de veículos aquaviários",
+ "120013410":
+ "Serviços de manutenção e reparação de aeronaves, exceto de motores, turborreatores e turbopropulsores aeronáuticos",
+ "120013420":
+ "Serviços de manutenção e reparação de motores, turborreatores e turbopropulsores aeronáuticos",
+ "120013430": "Serviços de manutenção e reparação de foguetes e equipamentos aeroespaciais",
+ "120013500": "Serviços de manutenção e reparação de veículos militares",
+ "120013900":
+ "Serviços de manutenção e reparação de maquinários e equipamentos de transporte não classificados em subposições anteriores",
+ "120014000":
+ "Serviços de manutenção e reparação de plataformas, inclusive navios-plataforma, para extração de petróleo e gás",
+ "120015000": "Serviços de manutenção e reparação de maquinários e equipamentos de uso industrial",
+ "120016000": "Serviços de manutenção e reparação de maquinários e equipamentos de uso comercial",
+ "120017000": "Serviços de manutenção e reparação de equipamentos e aparelhos de telecomunicações",
+ "120018100": "Serviços de manutenção e reparação de aparelhos eletroeletrônicos domésticos",
+ "120018200":
+ "Serviços de manutenção e reparação de instrumentos e equipamentos médico-hospitalares, odontológicos, óticos e de precisão",
+ "120018300": "Serviços de manutenção e reparação de equipamentos militares",
+ "120018900":
+ "Serviços de manutenção e reparação de outros maquinários e equipamentos não classificados em subposições anteriores",
+ "120021000": "Serviços de manutenção e reparação de produtos de couro, calçados, malas e bolsas",
+ "120022000": "Serviços de manutenção e reparação de relógios e joias",
+ "120023000": "Serviços de manutenção e reparação de móveis",
+ "120024000": "Serviços de manutenção de roupas e outros produtos têxteis",
+ "120029000":
+ "Serviços de manutenção e reparação de outros bens de consumo não classificados em subposições anteriores",
+ "120031000": "Serviços de instalação de produtos metálicos, exceto maquinário e equipamentos",
+ "120032110": "Serviços de montagem sob encomenda de turbinas industriais",
+ "120032190":
+ "Serviços de instalação de maquinários, aparelhos e equipamentos industriais não classificados em itens anteriores",
+ "120032200":
+ "Serviços de instalação de computadores e seus periféricos e maquinário de escritório",
+ "120032300":
+ "Serviços de instalação de equipamentos e aparelhos de comunicação, incluindo de rádio e de televisão",
+ "120032400":
+ "Serviços de instalação de maquinários, equipamentos, instrumentos e aparelhos médico-hospitalares, óticos e de precisão",
+ "120032510": "Serviços de instalação de sensores e sistemas de armas",
+ "120032520": "Serviços de instalação de maquinários, aparelhos e equipamentos de emprego militar",
+ "120032610":
+ "Serviços de montagem sob encomenda de motores, turborreatores e turbopropulsores aeronáuticos",
+ "120032690":
+ "Serviços de instalação de maquinários e equipamentos de transporte não classificados em itens anteriores",
+ "120032900":
+ "Serviços de instalação de maquinários, aparelhos e equipamentos não classificados em subposições anteriores",
+ "121011000": "Serviços de editoração",
+ "121012100": "Serviços de impressão",
+ "121012200": "Serviços relacionados à impressão",
+ "121012300": "Serviços de reprodução de mídia gravada",
+ "121013000": "Serviços de publicação",
+ "122011100": "Serviços de creche ou entidade equivalente",
+ "122011200": "Serviços de pré-escola",
+ "122011900": "Serviços de educação infantil não classificados em subposições anteriores",
+ "122012000": "Serviços de ensino fundamental",
+ "122013000": "Serviços de ensino médio",
+ "122020000": "Serviços de educação técnica de nível médio",
+ "122031000": "Serviços de ensino fundamental de jovens e adultos",
+ "122032000": "Serviços de ensino médio de jovens e adultos",
+ "122041000": "Serviços educacionais de graduação",
+ "122042000": "Serviços educacionais de pós-graduação",
+ "122043000": "Serviços educacionais de extensão",
+ "122044000": "Serviços educacionais de cursos sequenciais",
+ "122051100": "Serviços de educação com enfoque cultural",
+ "122051200": "Serviços de educação desportiva e recreacional",
+ "122051300": "Serviços de educação em línguas estrangeiras e de sinais",
+ "122051400": "Serviços de palestras e conferências",
+ "122051900":
+ "Serviços de educação, inclusive treinamento não classificados em subposições anteriores",
+ "122052000": "Serviços de apoio aos serviços educacionais",
+ "123011100": "Serviços cirúrgicos",
+ "123011200": "Serviços ginecológicos e obstétricos",
+ "123011300": "Serviços psiquiátricos",
+ "123011400": "Serviços prestados em Unidades de Terapia Intensiva",
+ "123011500": "Serviços de atendimento de urgência",
+ "123011900": "Serviços hospitalares não classificados em subposições anteriores",
+ "123012100": "Serviços de clínica médica",
+ "123012200": "Serviços médicos especializados",
+ "123012300": "Serviços odontológicos",
+ "123019100": "Serviços de enfermagem",
+ "123019200": "Serviços de fisioterapia",
+ "123019300": "Serviços laboratoriais",
+ "123019400": "Serviços de diagnóstico por imagem",
+ "123019500": "Serviços de bancos de material biológico humano",
+ "123019600": "Serviços de ambulância",
+ "123019700": "Serviços de assistência ao parto e pós-parto",
+ "123019800": "Serviços de psicologia",
+ "123019900": "Outros serviços de saúde humana não classificados em subposições anteriores",
+ "123021000": "Serviços de cuidado em saúde em unidades de acolhimento",
+ "123022100": "Serviços de assistência a idosos em unidades de acolhimento",
+ "123022200":
+ "Serviços de assistência a crianças e adolescentes com deficiência em unidades de acolhimento",
+ "123022300": "Serviços de assistência a adultos com deficiência em unidades de acolhimento",
+ "123030000": "Serviços de assistência social com acomodação",
+ "123041100": "Serviços de reabilitação vocacional para pessoas com deficiência",
+ "123041200": "Serviços de reabilitação vocacional para desempregados",
+ "123041900": "Serviço de reabilitação vocacional não classificados nas subposições anteriores",
+ "123042000": "Serviços de orientação e aconselhamento relacionados a crianças e adolescentes",
+ "123049000":
+ "Serviços de assistência social sem acomodação não classificados em subposições anteriores",
+ "124010000": "Serviços de tratamento de água",
+ "124021000": "Serviços de esgoto e tratamento de esgotos",
+ "124022000": "Serviços de esvaziamento e limpeza de fossas sépticas",
+ "124031100": "Serviços de coleta de resíduos de serviços de saúde e outros resíduos biológicos",
+ "124031200":
+ "Serviços de coleta de resíduos perigosos industriais, exceto resíduos de serviços de saúde e outros resíduos biológicos",
+ "124031900":
+ "Serviços de coleta de outros resíduos perigosos não classificados em subposições anteriores",
+ "124032100": "Serviços de coleta de resíduos recicláveis, não perigosos, de origem doméstica",
+ "124032200":
+ "Serviços de coleta de resíduos recicláveis, não perigosos, exceto de origem doméstica",
+ "124033100": "Serviços de coleta de resíduos gerais de origem doméstica",
+ "124033200": "Serviços de coleta de resíduos gerais, exceto de origem doméstica",
+ "124041100": "Serviços de triagem, preparação, consolidação e estocagem de resíduos perigosos",
+ "124041200": "Serviços de demolição e desmantelamento de embarcações, veículos e outros bens",
+ "124041300":
+ "Serviços de triagem, preparação, consolidação e estocagem de resíduos recicláveis não perigosos",
+ "124041900":
+ "Serviços de triagem, preparação, consolidação e estocagem de resíduos não perigosos, exceto os recicláveis",
+ "124042100": "Serviços de tratamento de resíduos perigosos",
+ "124042200": "Serviços de eliminação de resíduos perigosos",
+ "124043100":
+ "Serviços de tratamento e eliminação de resíduos não perigosos em aterros sanitários",
+ "124043200":
+ "Serviços de tratamento e eliminação de resíduos não perigosos em aterros, exceto os sanitários",
+ "124043300": "Serviços de incineração de resíduos não perigosos",
+ "124043900":
+ "Serviços de tratamento e eliminação de resíduos não perigosos não classificados em subposições anteriores",
+ "124051100": "Serviços de remediação e limpeza do ar",
+ "124051200": "Serviços de remediação e limpeza de águas de superfície",
+ "124051300": "Serviços de remediação e limpeza do solo e de águas subterrâneas",
+ "124051400": "Serviços de remediação em edificações",
+ "124052000": "Serviços de contenção, controle e monitoramento de áreas contaminadas",
+ "124059000": "Serviços de remediação não classificados em subposições anteriores",
+ "124061000": "Serviços de varrição de vias e áreas públicas",
+ "124069000": "Serviços de limpeza urbana e similares não classificados em subposições anteriores",
+ "124070000": "Serviços de proteção ambiental não classificados em posições anteriores",
+ "125011100": "Serviços de gravação de som em estúdio",
+ "125011200": "Serviços de gravação de som ao vivo",
+ "125012100": "Serviços de produção de programas de televisão, videoteipes e filmes",
+ "125012200": "Serviços de produção de programas de rádio",
+ "125013100": "Serviços de edição de obras audiovisuais",
+ "125013200": "Serviços de duplicação e transferência de obras audiovisuais",
+ "125013300": "Serviços de correção de cor e restauração digital de obras audiovisuais",
+ "125013400": "Serviços de efeitos visuais em obras audiovisuais",
+ "125013500": "Serviços de animação",
+ "125013600": "Serviços de legendas, títulos e dublagem em obras audiovisuais",
+ "125013700": "Serviços de projeto e edição de som em obras audiovisuais",
+ "125013900":
+ "Serviços de pós-produção de obras audiovisuais não classificados em subposições anteriores",
+ "125014000": "Serviços de agenciamento para a comercialização de obras audiovisuais",
+ "125015000": "Serviços de projeção de filmes",
+ "125019000":
+ "Serviços de produção audiovisual, de apoio e relacionados não classificados em subposições anteriores",
+ "125021000": "Serviços de organização e promoção de atuações artísticas ao vivo",
+ "125022000": "Serviços de produção e apresentação de atuações artísticas ao vivo",
+ "125023000": "Serviços de apoio para atuações artísticas ao vivo",
+ "125029000":
+ "Serviços de apresentação e promoção de atuações artísticas e outros serviços de entretenimento ao vivo não classificados em subposições anteriores",
+ "125031000": "Serviços de atuação artística",
+ "125032000":
+ "Serviços de autores, compositores, escultores, pintores e outros artistas, exceto os de atuação artística",
+ "125041100": "Serviços de museus",
+ "125041200": "Serviços de preservação e operação de locais e construções históricas",
+ "125042100": "Serviços de jardins botânico e zoológico",
+ "125042200": "Serviços de reserva natural, incluindo preservação de vida selvagem",
+ "125051000":
+ "Serviços de organização e promoção de eventos desportivos e recreacionais desportivos",
+ "125052000": "Serviços de clubes desportivos",
+ "125059000":
+ "Serviços desportivos e recreacionais desportivos não classificados em subposições anteriores",
+ "125060000":
+ "Serviços fornecidos por atletas e desportistas, por conta própria, e serviços de apoio relacionados com desportes e recreação desportiva",
+ "125071000": "Serviços de parques temáticos de diversão",
+ "125079000":
+ "Serviços de parques de diversão e atrações similares não classificados em subposições anteriores",
+ "125080000":
+ "Serviços recreativos, culturais e desportivos não classificados em posições anteriores",
+ "126011000": "Serviços de limpeza de têxteis, exceto quando realizados a seco",
+ "126012000": "Serviços de limpeza a seco",
+ "126013000": "Serviços de tinturaria",
+ "126014000": "Serviços de passadoria de roupas e outros artigos têxteis",
+ "126019000": "Serviços de lavanderia não classificados em subposições anteriores",
+ "126021000": "Serviços de cabeleireiros e barbeiros",
+ "126022000": "Serviços de manicure, pedicure e tratamento cosmético",
+ "126023000": "Serviços de bem-estar físico",
+ "126029000":
+ "Serviços de tratamento de beleza e bem-estar físico não classificados em subposições anteriores",
+ "126030000": "Serviços funerários, de cremação e de embalsamamento",
+ "126040000": "Serviços de confecção de roupas e outros artigos têxteis",
+ "126050000": "Serviços domésticos",
+ "126060000": "Serviços pessoais não classificados em posições anteriores",
+};
+
+/**
+ * Shape an NBS code has to be written in: the 9 digits, optionally split into the printed
+ * groups of 1, 4, 2 and 2 by a single whitespace or mask character.
+ */
+export const NBS_FORMAT_REGEX = /^\d[\s.\-/]?\d{4}[\s.\-/]?\d{2}[\s.\-/]?\d{2}$/;
diff --git a/src/_internals/constants/service-items.ts b/src/_internals/constants/service-items.ts
new file mode 100644
index 000000000..b5563ee59
--- /dev/null
+++ b/src/_internals/constants/service-items.ts
@@ -0,0 +1,315 @@
+/**
+ * The subitems in force of the service list annexed to the Lei Complementar 116/2003, indexed by
+ * the raw 4 digits of the subitem (`0101` for `1.01`), the first four digits of the national
+ * `cTribNac` code.
+ *
+ * Built from the sheet `LISTA.SERV.NAC.` of the `ANEXO_B-NBS2-LISTA_SERVICO_NACIONAL` workbook
+ * of the Sistema Nacional NFS-e, which prints the list in force, vetoed subitems left out and
+ * the amendments of later leis complementares applied. The item headings, the national codes a
+ * subitem is split into and item 99, an addition of the national list, are not part of the law
+ * and are left out.
+ *
+ * Generated by `node ./scripts/service-items.ts` from `anexo_b-nbs2-lista_servico_nacional-snnfse-v1-01-20260122.xlsx`.
+ * Do not edit by hand.
+ *
+ * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp116.htm
+ * Lei Complementar 116/2003, "Lista de serviços anexa".
+ * @see Official: https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual
+ * Sistema Nacional NFS-e, current technical documentation, ANEXO B.
+ */
+export const SERVICE_ITEM_DESCRIPTIONS: Record = {
+ "1001":
+ "Agenciamento, corretagem ou intermediação de câmbio, de seguros, de cartões de crédito, de planos de saúde e de planos de previdência privada.",
+ "1002":
+ "Agenciamento, corretagem ou intermediação de títulos em geral, valores mobiliários e contratos quaisquer.",
+ "1003":
+ "Agenciamento, corretagem ou intermediação de direitos de propriedade industrial, artística ou literária.",
+ "1004":
+ "Agenciamento, corretagem ou intermediação de contratos de arrendamento mercantil (leasing), de franquia (franchising) e de faturização (factoring).",
+ "1005":
+ "Agenciamento, corretagem ou intermediação de bens móveis ou imóveis, não abrangidos em outros itens ou subitens, inclusive aqueles realizados no âmbito de Bolsas de Mercadorias e Futuros, por quaisquer meios.",
+ "1006": "Agenciamento marítimo.",
+ "1007": "Agenciamento de notícias.",
+ "1008":
+ "Agenciamento de publicidade e propaganda, inclusive o agenciamento de veiculação por quaisquer meios.",
+ "1009": "Representação de qualquer natureza, inclusive comercial.",
+ "1010": "Distribuição de bens de terceiros.",
+ "1101":
+ "Guarda e estacionamento de veículos terrestres automotores, de aeronaves e de embarcações.",
+ "1102": "Vigilância, segurança ou monitoramento de bens, pessoas e semoventes.",
+ "1103": "Escolta, inclusive de veículos e cargas.",
+ "1104":
+ "Armazenamento, depósito, carga, descarga, arrumação e guarda de bens de qualquer espécie.",
+ "1105":
+ "Serviços relacionados ao monitoramento e rastreamento a distância, em qualquer via ou local, de veículos, cargas, pessoas e semoventes em circulação ou movimento, realizados por meio de telefonia móvel, transmissão de satélites, rádio ou qualquer outro meio, inclusive pelas empresas de Tecnologia da Informação Veicular, independentemente de o prestador de serviços ser proprietário ou não da infraestrutura de telecomunicações que utiliza.",
+ "1201": "Espetáculos teatrais.",
+ "1202": "Exibições cinematográficas.",
+ "1203": "Espetáculos circenses.",
+ "1204": "Programas de auditório.",
+ "1205": "Parques de diversões, centros de lazer e congêneres.",
+ "1206": "Boates, taxi-dancing e congêneres.",
+ "1207":
+ "Shows, ballet, danças, desfiles, bailes, óperas, concertos, recitais, festivais e congêneres.",
+ "1208": "Feiras, exposições, congressos e congêneres.",
+ "1209": "Bilhares, boliches e diversões eletrônicas ou não.",
+ "1210": "Corridas e competições de animais.",
+ "1211":
+ "Competições esportivas ou de destreza física ou intelectual, com ou sem a participação do espectador.",
+ "1212": "Execução de música.",
+ "1213":
+ "Produção, mediante ou sem encomenda prévia, de eventos, espetáculos, entrevistas, shows, ballet, danças, desfiles, bailes, teatros, óperas, concertos, recitais, festivais e congêneres.",
+ "1214":
+ "Fornecimento de música para ambientes fechados ou não, mediante transmissão por qualquer processo.",
+ "1215": "Desfiles de blocos carnavalescos ou folclóricos, trios elétricos e congêneres.",
+ "1216":
+ "Exibição de filmes, entrevistas, musicais, espetáculos, shows, concertos, desfiles, óperas, competições esportivas, de destreza intelectual ou congêneres.",
+ "1217": "Recreação e animação, inclusive em festas e eventos de qualquer natureza.",
+ "1302": "Fonografia ou gravação de sons, inclusive trucagem, dublagem, mixagem e congêneres.",
+ "1303":
+ "Fotografia e cinematografia, inclusive revelação, ampliação, cópia, reprodução, trucagem e congêneres.",
+ "1304": "Reprografia, microfilmagem e digitalização.",
+ "1305":
+ "Composição gráfica, inclusive confecção de impressos gráficos, fotocomposição, clicheria, zincografia, litografia e fotolitografia, exceto se destinados a posterior operação de comercialização ou industrialização, ainda que incorporados, de qualquer forma, a outra mercadoria que deva ser objeto de posterior circulação, tais como bulas, rótulos, etiquetas, caixas, cartuchos, embalagens e manuais técnicos e de instrução, quando ficarão sujeitos ao ICMS.",
+ "1401":
+ "Lubrificação, limpeza, lustração, revisão, carga e recarga, conserto, restauração, blindagem, manutenção e conservação de máquinas, veículos, aparelhos, equipamentos, motores, elevadores ou de qualquer objeto (exceto peças e partes empregadas, que ficam sujeitas ao ICMS).",
+ "1402": "Assistência técnica.",
+ "1403":
+ "Recondicionamento de motores (exceto peças e partes empregadas, que ficam sujeitas ao ICMS).",
+ "1404": "Recauchutagem ou regeneração de pneus.",
+ "1405":
+ "Restauração, recondicionamento, acondicionamento, pintura, beneficiamento, lavagem, secagem, tingimento, galvanoplastia, anodização, corte, recorte, plastificação, costura, acabamento, polimento e congêneres de objetos quaisquer.",
+ "1406":
+ "Instalação e montagem de aparelhos, máquinas e equipamentos, inclusive montagem industrial, prestados ao usuário final, exclusivamente com material por ele fornecido.",
+ "1407": "Colocação de molduras e congêneres.",
+ "1408": "Encadernação, gravação e douração de livros, revistas e congêneres.",
+ "1409":
+ "Alfaiataria e costura, quando o material for fornecido pelo usuário final, exceto aviamento.",
+ "1410": "Tinturaria e lavanderia.",
+ "1411": "Tapeçaria e reforma de estofamentos em geral.",
+ "1412": "Funilaria e lanternagem.",
+ "1413": "Carpintaria e serralheria.",
+ "1414": "Guincho intramunicipal, guindaste e içamento.",
+ "1501":
+ "Administração de fundos quaisquer, de consórcio, de cartão de crédito ou débito e congêneres, de carteira de clientes, de cheques pré-datados e congêneres.",
+ "1502":
+ "Abertura de contas em geral, inclusive conta-corrente, conta de investimentos e aplicação e caderneta de poupança, no País e no exterior, bem como a manutenção das referidas contas ativas e inativas.",
+ "1503":
+ "Locação e manutenção de cofres particulares, de terminais eletrônicos, de terminais de atendimento e de bens e equipamentos em geral.",
+ "1504":
+ "Fornecimento ou emissão de atestados em geral, inclusive atestado de idoneidade, atestado de capacidade financeira e congêneres.",
+ "1505":
+ "Cadastro, elaboração de ficha cadastral, renovação cadastral e congêneres, inclusão ou exclusão no Cadastro de Emitentes de Cheques sem Fundos - CCF ou em quaisquer outros bancos cadastrais.",
+ "1506":
+ "Emissão, reemissão e fornecimento de avisos, comprovantes e documentos em geral; abono de firmas; coleta e entrega de documentos, bens e valores; comunicação com outra agência ou com a administração central; licenciamento eletrônico de veículos; transferência de veículos; agenciamento fiduciário ou depositário; devolução de bens em custódia.",
+ "1507":
+ "Acesso, movimentação, atendimento e consulta a contas em geral, por qualquer meio ou processo, inclusive por telefone, fac-símile, internet e telex, acesso a terminais de atendimento, inclusive vinte e quatro horas; acesso a outro banco e à rede compartilhada; fornecimento de saldo, extrato e demais informações relativas a contas em geral, por qualquer meio ou processo.",
+ "1508":
+ "Emissão, reemissão, alteração, cessão, substituição, cancelamento e registro de contrato de crédito; estudo, análise e avaliação de operações de crédito; emissão, concessão, alteração ou contratação de aval, fiança, anuência e congêneres; serviços relativos à abertura de crédito, para quaisquer fins.",
+ "1509":
+ "Arrendamento mercantil (leasing) de quaisquer bens, inclusive cessão de direitos e obrigações, substituição de garantia, alteração, cancelamento e registro de contrato, e demais serviços relacionados ao arrendamento mercantil (leasing).",
+ "1510":
+ "Serviços relacionados a cobranças, recebimentos ou pagamentos em geral, de títulos quaisquer, de contas ou carnês, de câmbio, de tributos e por conta de terceiros, inclusive os efetuados por meio eletrônico, automático ou por máquinas de atendimento; fornecimento de posição de cobrança, recebimento ou pagamento; emissão de carnês, fichas de compensação, impressos e documentos em geral.",
+ "1511":
+ "Devolução de títulos, protesto de títulos, sustação de protesto, manutenção de títulos, reapresentação de títulos, e demais serviços a eles relacionados.",
+ "1512": "Custódia em geral, inclusive de títulos e valores mobiliários.",
+ "1513":
+ "Serviços relacionados a operações de câmbio em geral, edição, alteração, prorrogação, cancelamento e baixa de contrato de câmbio; emissão de registro de exportação ou de crédito; cobrança ou depósito no exterior; emissão, fornecimento e cancelamento de cheques de viagem; fornecimento, transferência, cancelamento e demais serviços relativos a carta de crédito de importação, exportação e garantias recebidas; envio e recebimento de mensagens em geral relacionadas a operações de câmbio.",
+ "1514":
+ "Fornecimento, emissão, reemissão, renovação e manutenção de cartão magnético, cartão de crédito, cartão de débito, cartão salário e congêneres.",
+ "1515":
+ "Compensação de cheques e títulos quaisquer; serviços relacionados a depósito, inclusive depósito identificado, a saque de contas quaisquer, por qualquer meio ou processo, inclusive em terminais eletrônicos e de atendimento.",
+ "1516":
+ "Emissão, reemissão, liquidação, alteração, cancelamento e baixa de ordens de pagamento, ordens de crédito e similares, por qualquer meio ou processo; serviços relacionados à transferência de valores, dados, fundos, pagamentos e similares, inclusive entre contas em geral.",
+ "1517":
+ "Emissão, fornecimento, devolução, sustação, cancelamento e oposição de cheques quaisquer, avulso ou por talão.",
+ "1518":
+ "Serviços relacionados a crédito imobiliário, avaliação e vistoria de imóvel ou obra, análise técnica e jurídica, emissão, reemissão, alteração, transferência e renegociação de contrato, emissão e reemissão do termo de quitação e demais serviços relacionados a crédito imobiliário.",
+ "1601":
+ "Serviços de transporte coletivo municipal rodoviário, metroviário, ferroviário e aquaviário de passageiros.",
+ "1602": "Outros serviços de transporte de natureza municipal.",
+ "1701":
+ "Assessoria ou consultoria de qualquer natureza, não contida em outros itens desta lista; análise, exame, pesquisa, coleta, compilação e fornecimento de dados e informações de qualquer natureza, inclusive cadastro e similares.",
+ "1702":
+ "Datilografia, digitação, estenografia, expediente, secretaria em geral, resposta audível, redação, edição, interpretação, revisão, tradução, apoio e infra-estrutura administrativa e congêneres.",
+ "1703":
+ "Planejamento, coordenação, programação ou organização técnica, financeira ou administrativa.",
+ "1704": "Recrutamento, agenciamento, seleção e colocação de mão-de-obra.",
+ "1705":
+ "Fornecimento de mão-de-obra, mesmo em caráter temporário, inclusive de empregados ou trabalhadores, avulsos ou temporários, contratados pelo prestador de serviço.",
+ "1706":
+ "Propaganda e publicidade, inclusive promoção de vendas, planejamento de campanhas ou sistemas de publicidade, elaboração de desenhos, textos e demais materiais publicitários.",
+ "1708": "Franquia (franchising).",
+ "1709": "Perícias, laudos, exames técnicos e análises técnicas.",
+ "1710":
+ "Planejamento, organização e administração de feiras, exposições, congressos e congêneres.",
+ "1711":
+ "Organização de festas e recepções; bufê (exceto o fornecimento de alimentação e bebidas, que fica sujeito ao ICMS).",
+ "1712": "Administração em geral, inclusive de bens e negócios de terceiros.",
+ "1713": "Leilão e congêneres.",
+ "1714": "Advocacia",
+ "1715": "Arbitragem de qualquer espécie, inclusive jurídica.",
+ "1716": "Auditoria.",
+ "1717": "Análise de Organização e Métodos.",
+ "1718": "Atuária e cálculos técnicos de qualquer natureza.",
+ "1719": "Contabilidade, inclusive serviços técnicos e auxiliares.",
+ "1720": "Consultoria e assessoria econômica ou financeira.",
+ "1721": "Estatística.",
+ "1722": "Cobrança em geral.",
+ "1723":
+ "Assessoria, análise, avaliação, atendimento, consulta, cadastro, seleção, gerenciamento de informações, administração de contas a receber ou a pagar e em geral, relacionados a operações de faturização (factoring).",
+ "1724": "Apresentação de palestras, conferências, seminários e congêneres.",
+ "1725":
+ "Inserção de textos, desenhos e outros materiais de propaganda e publicidade, em qualquer meio (exceto em livros, jornais, periódicos e nas modalidades de serviços de radiodifusão sonora e de sons e imagens de recepção livre e gratuita).",
+ "1801":
+ "Serviços de regulação de sinistros vinculados a contratos de seguros; inspeção e avaliação de riscos para cobertura de contratos de seguros; prevenção e gerência de riscos seguráveis e congêneres.",
+ "1901":
+ "Serviços de distribuição e venda de bilhetes e demais produtos de loteria, bingos, cartões, pules ou cupons de apostas, sorteios, prêmios, inclusive os decorrentes de títulos de capitalização e congêneres.",
+ "2001":
+ "Serviços portuários, ferroportuários, utilização de porto, movimentação de passageiros, reboque de embarcações, rebocador escoteiro, atracação, desatracação, serviços de praticagem, capatazia, armazenagem de qualquer natureza, serviços acessórios, movimentação de mercadorias, serviços de apoio marítimo, de movimentação ao largo, serviços de armadores, estiva, conferência, logística e congêneres.",
+ "2002":
+ "Serviços aeroportuários, utilização de aeroporto, movimentação de passageiros, armazenagem de qualquer natureza, capatazia, movimentação de aeronaves, serviços de apoio aeroportuários, serviços acessórios, movimentação de mercadorias, logística e congêneres.",
+ "2003":
+ "Serviços de terminais rodoviários, ferroviários, metroviários, movimentação de passageiros, mercadorias, inclusive suas operações, logística e congêneres.",
+ "2101": "Serviços de registros públicos, cartorários e notariais.",
+ "2201":
+ "Serviços de exploração de rodovia mediante cobrança de preço ou pedágio dos usuários, envolvendo execução de serviços de conservação, manutenção, melhoramentos para adequação de capacidade e segurança de trânsito, operação, monitoração, assistência aos usuários e outros serviços definidos em contratos, atos de concessão ou de permissão ou em normas oficiais.",
+ "2301": "Serviços de programação e comunicação visual, desenho industrial e congêneres.",
+ "2401":
+ "Serviços de chaveiros, confecção de carimbos, placas, sinalização visual, banners, adesivos e congêneres.",
+ "2501":
+ "Funerais, inclusive fornecimento de caixão, urna ou esquifes; aluguel de capela; transporte do corpo cadavérico; fornecimento de flores, coroas e outros paramentos; desembaraço de certidão de óbito; fornecimento de véu, essa e outros adornos; embalsamento, embelezamento, conservação ou restauração de cadáveres.",
+ "2502": "Translado intramunicipal e cremação de corpos e partes de corpos cadavéricos.",
+ "2503": "Planos ou convênio funerários.",
+ "2504": "Manutenção e conservação de jazigos e cemitérios.",
+ "2505": "Cessão de uso de espaços em cemitérios para sepultamento.",
+ "2601":
+ "Serviços de coleta, remessa ou entrega de correspondências, documentos, objetos, bens ou valores, inclusive pelos correios e suas agências franqueadas; courrier e congêneres.",
+ "2701": "Serviços de assistência social.",
+ "2801": "Serviços de avaliação de bens e serviços de qualquer natureza.",
+ "2901": "Serviços de biblioteconomia.",
+ "3001": "Serviços de biologia, biotecnologia e química.",
+ "3101":
+ "Serviços técnicos em edificações, eletrônica, eletrotécnica, mecânica, telecomunicações e congêneres.",
+ "3201": "Serviços de desenhos técnicos.",
+ "3301": "Serviços de desembaraço aduaneiro, comissários, despachantes e congêneres.",
+ "3401": "Serviços de investigações particulares, detetives e congêneres.",
+ "3501": "Serviços de reportagem, assessoria de imprensa, jornalismo e relações públicas.",
+ "3601": "Serviços de meteorologia.",
+ "3701": "Serviços de artistas, atletas, modelos e manequins.",
+ "3801": "Serviços de museologia.",
+ "3901":
+ "Serviços de ourivesaria e lapidação (quando o material for fornecido pelo tomador do serviço).",
+ "4001": "Obras de arte sob encomenda.",
+ "0101": "Análise e desenvolvimento de sistemas.",
+ "0102": "Programação.",
+ "0103":
+ "Processamento, armazenamento ou hospedagem de dados, textos, imagens, vídeos, páginas eletrônicas, aplicativos e sistemas de informação, entre outros formatos, e congêneres.",
+ "0104":
+ "Elaboração de programas de computadores, inclusive de jogos eletrônicos, independentemente da arquitetura construtiva da máquina em que o programa será executado, incluindo tablets, smartphones e congêneres.",
+ "0105": "Licenciamento ou cessão de direito de uso de programas de computação.",
+ "0106": "Assessoria e consultoria em informática.",
+ "0107":
+ "Suporte técnico em informática, inclusive instalação, configuração e manutenção de programas de computação e bancos de dados.",
+ "0108": "Planejamento, confecção, manutenção e atualização de páginas eletrônicas.",
+ "0109":
+ "Disponibilização, sem cessão definitiva, de conteúdos de áudio, vídeo, imagem e texto por meio da internet, respeitada a imunidade de livros, jornais e periódicos (exceto a distribuição de conteúdos pelas prestadoras de Serviço de Acesso Condicionado, de que trata a Lei nº 12.485, de 12 de setembro de 2011, sujeita ao ICMS).",
+ "0201": "Serviços de pesquisas e desenvolvimento de qualquer natureza.",
+ "0302": "Cessão de direito de uso de marcas e de sinais de propaganda.",
+ "0303":
+ "Exploração de salões de festas, centro de convenções, escritórios virtuais, stands, quadras esportivas, estádios, ginásios, auditórios, casas de espetáculos, parques de diversões, canchas e congêneres, para realização de eventos ou negócios de qualquer natureza.",
+ "0304":
+ "Locação, sublocação, arrendamento, direito de passagem ou permissão de uso, compartilhado ou não, de ferrovia, rodovia, postes, cabos, dutos e condutos de qualquer natureza.",
+ "0305": "Cessão de andaimes, palcos, coberturas e outras estruturas de uso temporário.",
+ "0401": "Medicina e biomedicina.",
+ "0402":
+ "Análises clínicas, patologia, eletricidade médica, radioterapia, quimioterapia, ultra-sonografia, ressonância magnética, radiologia, tomografia e congêneres.",
+ "0403":
+ "Hospitais, clínicas, laboratórios, sanatórios, manicômios, casas de saúde, prontos-socorros, ambulatórios e congêneres.",
+ "0404": "Instrumentação cirúrgica.",
+ "0405": "Acupuntura.",
+ "0406": "Enfermagem, inclusive serviços auxiliares.",
+ "0407": "Serviços farmacêuticos.",
+ "0408": "Terapia ocupacional, fisioterapia e fonoaudiologia.",
+ "0409": "Terapias de qualquer espécie destinadas ao tratamento físico, orgânico e mental.",
+ "0410": "Nutrição.",
+ "0411": "Obstetrícia.",
+ "0412": "Odontologia.",
+ "0413": "Ortóptica.",
+ "0414": "Próteses sob encomenda.",
+ "0415": "Psicanálise.",
+ "0416": "Psicologia.",
+ "0417": "Casas de repouso e de recuperação, creches, asilos e congêneres.",
+ "0418": "Inseminação artificial, fertilização in vitro e congêneres.",
+ "0419": "Bancos de sangue, leite, pele, olhos, óvulos, sêmen e congêneres.",
+ "0420":
+ "Coleta de sangue, leite, tecidos, sêmen, órgãos e materiais biológicos de qualquer espécie.",
+ "0421": "Unidade de atendimento, assistência ou tratamento móvel e congêneres.",
+ "0422":
+ "Planos de medicina de grupo ou individual e convênios para prestação de assistência médica, hospitalar, odontológica e congêneres.",
+ "0423":
+ "Outros planos de saúde que se cumpram através de serviços de terceiros contratados, credenciados, cooperados ou apenas pagos pelo operador do plano mediante indicação do beneficiário.",
+ "0501": "Medicina veterinária e zootecnia.",
+ "0502": "Hospitais, clínicas, ambulatórios, prontos-socorros e congêneres, na área veterinária.",
+ "0503": "Laboratórios de análise na área veterinária.",
+ "0504": "Inseminação artificial, fertilização in vitro e congêneres.",
+ "0505": "Bancos de sangue e de órgãos e congêneres.",
+ "0506":
+ "Coleta de sangue, leite, tecidos, sêmen, órgãos e materiais biológicos de qualquer espécie.",
+ "0507": "Unidade de atendimento, assistência ou tratamento móvel e congêneres.",
+ "0508": "Guarda, tratamento, amestramento, embelezamento, alojamento e congêneres.",
+ "0509": "Planos de atendimento e assistência médico-veterinária.",
+ "0601": "Barbearia, cabeleireiros, manicuros, pedicuros e congêneres.",
+ "0602": "Esteticistas, tratamento de pele, depilação e congêneres.",
+ "0603": "Banhos, duchas, sauna, massagens e congêneres.",
+ "0604": "Ginástica, dança, esportes, natação, artes marciais e demais atividades físicas.",
+ "0605": "Centros de emagrecimento, spa e congêneres.",
+ "0606": "Aplicação de tatuagens, piercings e congêneres.",
+ "0701":
+ "Engenharia, agronomia, agrimensura, arquitetura, geologia, urbanismo, paisagismo e congêneres.",
+ "0702":
+ "Execução, por administração, empreitada ou subempreitada, de obras de construção civil, hidráulica ou elétrica e de outras obras semelhantes, inclusive sondagem, perfuração de poços, escavação, drenagem e irrigação, terraplanagem, pavimentação, concretagem e a instalação e montagem de produtos, peças e equipamentos (exceto o fornecimento de mercadorias produzidas pelo prestador de serviços fora do local da prestação dos serviços, que fica sujeito ao ICMS).",
+ "0703":
+ "Elaboração de planos diretores, estudos de viabilidade, estudos organizacionais e outros, relacionados com obras e serviços de engenharia; elaboração de anteprojetos, projetos básicos e projetos executivos para trabalhos de engenharia.",
+ "0704": "Demolição.",
+ "0705":
+ "Reparação, conservação e reforma de edifícios, estradas, pontes, portos e congêneres (exceto o fornecimento de mercadorias produzidas pelo prestador dos serviços, fora do local da prestação dos serviços, que fica sujeito ao ICMS).",
+ "0706":
+ "Colocação e instalação de tapetes, carpetes, assoalhos, cortinas, revestimentos de parede, vidros, divisórias, placas de gesso e congêneres, com material fornecido pelo tomador do serviço.",
+ "0707": "Recuperação, raspagem, polimento e lustração de pisos e congêneres.",
+ "0708": "Calafetação.",
+ "0709":
+ "Varrição, coleta, remoção, incineração, tratamento, reciclagem, separação e destinação final de lixo, rejeitos e outros resíduos quaisquer.",
+ "0710":
+ "Limpeza, manutenção e conservação de vias e logradouros públicos, imóveis, chaminés, piscinas, parques, jardins e congêneres.",
+ "0711": "Decoração e jardinagem, inclusive corte e poda de árvores.",
+ "0712":
+ "Controle e tratamento de efluentes de qualquer natureza e de agentes físicos, químicos e biológicos.",
+ "0713":
+ "Dedetização, desinfecção, desinsetização, imunização, higienização, desratização, pulverização e congêneres.",
+ "0716":
+ "Florestamento, reflorestamento, semeadura, adubação, reparação de solo, plantio, silagem, colheita, corte e descascamento de árvores, silvicultura, exploração florestal e dos serviços congêneres indissociáveis da formação, manutenção e colheita de florestas, para quaisquer fins e por quaisquer meios.",
+ "0717": "Escoramento, contenção de encostas e serviços congêneres.",
+ "0718":
+ "Limpeza e dragagem de rios, portos, canais, baías, lagos, lagoas, represas, açudes e congêneres.",
+ "0719":
+ "Acompanhamento e fiscalização da execução de obras de engenharia, arquitetura e urbanismo.",
+ "0720":
+ "Aerofotogrametria (inclusive interpretação), cartografia, mapeamento, levantamentos topográficos, batimétricos, geográficos, geodésicos, geológicos, geofísicos e congêneres.",
+ "0721":
+ "Pesquisa, perfuração, cimentação, mergulho, perfilagem, concretação, testemunhagem, pescaria, estimulação e outros serviços relacionados com a exploração e explotação de petróleo, gás natural e de outros recursos minerais.",
+ "0722": "Nucleação e bombardeamento de nuvens e congêneres.",
+ "0801": "Ensino regular pré-escolar, fundamental, médio e superior.",
+ "0802":
+ "Instrução, treinamento, orientação pedagógica e educacional, avaliação de conhecimentos de qualquer natureza.",
+ "0901":
+ "Hospedagem de qualquer natureza em hotéis, apart-service condominiais, flat, apart-hotéis, hotéis residência, residence-service, suite service, hotelaria marítima, motéis, pensões e congêneres; ocupação por temporada com fornecimento de serviço (o valor da alimentação e gorjeta, quando incluído no preço da diária, fica sujeito ao Imposto Sobre Serviços).",
+ "0902":
+ "Agenciamento, organização, promoção, intermediação e execução de programas de turismo, passeios, viagens, excursões, hospedagens e congêneres.",
+ "0903": "Guias de turismo.",
+};
+
+/**
+ * Shape a subitem has to be written in: one or two digits of the item and the two of the
+ * subitem, with the dot the law prints between them or without it.
+ */
+export const SERVICE_ITEM_FORMAT_REGEX = /^\d{1,2}\.?\d{2}$/;
diff --git a/src/format-nbs/format-nbs.test.ts b/src/format-nbs/format-nbs.test.ts
new file mode 100644
index 000000000..20dcf0cf7
--- /dev/null
+++ b/src/format-nbs/format-nbs.test.ts
@@ -0,0 +1,80 @@
+import { anyGarbage, anyValue, digits } from "../_internals/test/arbitraries";
+import {
+ expectAlwaysReturnsType,
+ expectIdempotent,
+ expectMatchesPattern,
+ expectNeverThrows,
+} from "../_internals/test/properties";
+import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime";
+import { formatNbs } from "./format-nbs";
+
+describe("formatNbs", () => {
+ it("should format an NBS code given as digits", () => {
+ expect(formatNbs("101011100")).toBe("1.0101.11.00");
+ });
+
+ it("should format an NBS code given as a number", () => {
+ expect(formatNbs(126_050_000)).toBe("1.2605.00.00");
+ });
+
+ it("should keep a code that already has the mask", () => {
+ expect(formatNbs("1.0101.11.00")).toBe("1.0101.11.00");
+ });
+
+ it("should not validate whether the code exists in the official table", () => {
+ expect(formatNbs("999999999")).toBe("9.9999.99.99");
+ });
+
+ it("should mask a partial value progressively", () => {
+ expect(formatNbs("1")).toBe("1");
+ expect(formatNbs("10")).toBe("1.0");
+ expect(formatNbs("10101")).toBe("1.0101");
+ expect(formatNbs("101011")).toBe("1.0101.1");
+ expect(formatNbs("1010111")).toBe("1.0101.11");
+ expect(formatNbs("10101110")).toBe("1.0101.11.0");
+ });
+
+ it("should drop the characters outside the mask and the digits after the ninth", () => {
+ expect(formatNbs("abc101011100")).toBe("1.0101.11.00");
+ expect(formatNbs("1010111009")).toBe("1.0101.11.00");
+ });
+
+ it("should return an empty string when there is no digit", () => {
+ expect(formatNbs("")).toBe("");
+ expect(formatNbs("abc")).toBe("");
+ });
+
+ it("should return an empty string for null and undefined", () => {
+ // @ts-expect-error not a string or number
+ expect(formatNbs(null)).toBe("");
+ // @ts-expect-error not a string or number
+ expect(formatNbs()).toBe("");
+ });
+
+ describe("properties", () => {
+ const nineDigits = digits(9);
+
+ test("should never throw, regardless of the input", () => {
+ expectNeverThrows(formatNbs, anyGarbage);
+ });
+
+ test("should always return a string", () => {
+ expectAlwaysReturnsType(formatNbs, "string", anyValue);
+ });
+
+ test("should format every 9 digit value in the N.NNNN.NN.NN pattern", () => {
+ expectMatchesPattern(formatNbs, /^\d\.\d{4}\.\d{2}\.\d{2}$/, nineDigits);
+ });
+
+ test("should be idempotent on a full 9 digit code", () => {
+ expectIdempotent(formatNbs, nineDigits);
+ });
+ });
+});
+
+describe("formatNbs types", () => {
+ test("should take a string or number value and return a string", () => {
+ expectTypeOf(formatNbs).parameters.toEqualTypeOf<[value: string | number]>();
+ expectTypeOf(formatNbs).returns.toEqualTypeOf();
+ });
+});
diff --git a/src/format-nbs/format-nbs.ts b/src/format-nbs/format-nbs.ts
new file mode 100644
index 000000000..075b037f6
--- /dev/null
+++ b/src/format-nbs/format-nbs.ts
@@ -0,0 +1,31 @@
+import { format } from "../_internals/format/format";
+import { isNullish } from "../_internals/is-nullish/is-nullish";
+import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits";
+
+/**
+ * Formats an NBS (Nomenclatura Brasileira de Serviços, Intangíveis e Outras Operações que
+ * Produzam Variações no Patrimônio) code into the `N.NNNN.NN.NN` mask the nomenclature prints.
+ *
+ * This is a purely structural transformation, it does not check the code against the official
+ * table, use `isValidNbs` for that. Like every formatter of this package, the value is read for
+ * its digits and masked as far as they go, so a partial code still being typed is masked
+ * progressively, characters outside the mask are dropped and anything without a digit gives
+ * `""` instead of throwing. Every NBS code starts with 1, so there is no zero padding to offer.
+ *
+ * @param {string|number} value - The NBS code to be formatted.
+ * @returns {string} The formatted code in the `N.NNNN.NN.NN` pattern, or an empty string when
+ * there is nothing to format.
+ *
+ * @example
+ * ```typescript
+ * formatNbs("101011100"); // "1.0101.11.00"
+ * formatNbs(101011100); // "1.0101.11.00"
+ * formatNbs("10101"); // "1.0101" (partial values are masked as far as they go)
+ * ```
+ *
+ * @see Official: https://www.gov.br/mdic/pt-br/images/REPOSITORIO/scs/decos/NBS/Anexoa_Ia_NBSa_2.0a_coma_alteraa_esa_6.12.18.pdf
+ * Anexo I of the Portaria Conjunta RFB/SCS 2.000/2018, NBS 2.0: "Formação do código da NBS" and
+ * the `N.NNNN.NN.NN` form every code of the nomenclature is printed in.
+ */
+export const formatNbs = (value: string | number): string =>
+ isNullish(value) ? "" : format({ value: sanitizeToDigits(value), pattern: "0.0000.00.00" });
diff --git a/src/get-nbs/get-nbs.test.ts b/src/get-nbs/get-nbs.test.ts
new file mode 100644
index 000000000..84f626cc1
--- /dev/null
+++ b/src/get-nbs/get-nbs.test.ts
@@ -0,0 +1,148 @@
+import * as fc from "fast-check";
+
+import { NBS_DESCRIPTIONS } from "../_internals/constants/nbs";
+import { anyGarbage, PROTOTYPE_KEYS } from "../_internals/test/arbitraries";
+import { expectNeverThrows } from "../_internals/test/properties";
+import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime";
+import { formatNbs } from "../format-nbs/format-nbs";
+import { isValidNbs } from "../is-valid-nbs/is-valid-nbs";
+import { getNbs, type Nbs } from "./get-nbs";
+
+const RESIDENTIAL = {
+ code: "101011100",
+ description: "Serviços de construção de edificações residenciais de um e dois pavimentos",
+};
+
+describe("getNbs", () => {
+ it("should return the code for the 9 digits without a mask", () => {
+ expect(getNbs("101011100")).toEqual(RESIDENTIAL);
+ });
+
+ it("should return the code for the N.NNNN.NN.NN mask", () => {
+ expect(getNbs("1.0101.11.00")).toEqual(RESIDENTIAL);
+ });
+
+ it("should return the code for a number", () => {
+ expect(getNbs(101_011_100)).toEqual(RESIDENTIAL);
+ expect(getNbs(126_050_000)).toEqual({ code: "126050000", description: "Serviços domésticos" });
+ });
+
+ it("should ignore surrounding whitespace", () => {
+ expect(getNbs(" 1.0101.11.00\n")).toEqual(RESIDENTIAL);
+ });
+
+ it("should accept any single mask character between the groups", () => {
+ expect(getNbs("1 0101 11 00")).toEqual(RESIDENTIAL);
+ expect(getNbs("1-0101-11-00")).toEqual(RESIDENTIAL);
+ expect(getNbs("1/0101/11/00")).toEqual(RESIDENTIAL);
+ expect(getNbs("1.010111.00")).toEqual(RESIDENTIAL);
+ });
+
+ it("should resolve the three codes of the NBS 2.0 the ANEXO B of the NFS-e leaves out", () => {
+ expect(getNbs("1.0904.40.00")).toEqual({
+ code: "109044000",
+ description: "Serviços de retrocessão",
+ });
+ expect(getNbs("1.0402.29.00")?.code).toBe("104022900");
+ expect(getNbs("1.0403.29.00")?.code).toBe("104032900");
+ });
+
+ it("should resolve the codes the Portaria 2.000/2018 renumbered and not the ones it replaced", () => {
+ expect(getNbs("1.0402.11.10")?.code).toBe("104021110");
+ expect(getNbs("1.0402.11.90")?.code).toBe("104021190");
+ expect(getNbs("1.0402.11.11")).toBeNull();
+ expect(getNbs("1.0402.11.19")).toBeNull();
+ });
+
+ it("should return a fresh object that does not leak the internal table", () => {
+ expect(getNbs("101011100")).not.toBe(getNbs("101011100"));
+ });
+
+ it("should return null for the chapter, position and subposition headings", () => {
+ expect(getNbs("1.01")).toBeNull();
+ expect(getNbs("1.0101")).toBeNull();
+ expect(getNbs("1.0101.1")).toBeNull();
+ expect(getNbs("10101")).toBeNull();
+ });
+
+ it("should return null for a 9 digit code the table does not carry", () => {
+ expect(getNbs("1.9999.99.99")).toBeNull();
+ expect(getNbs("9.9999.99.99")).toBeNull();
+ expect(getNbs("000000000")).toBeNull();
+ });
+
+ it("should return null for a group boundary written with more than one separator", () => {
+ expect(getNbs("1..0101.11.00")).toBeNull();
+ expect(getNbs("1.0101..11.00")).toBeNull();
+ expect(getNbs("1.0101.11..00")).toBeNull();
+ });
+
+ it("should return null for a string that is not written in a documented form", () => {
+ expect(getNbs("1.0101abc11.00")).toBeNull();
+ expect(getNbs("10.101.11.00")).toBeNull();
+ expect(getNbs("x101011100")).toBeNull();
+ expect(getNbs("101011100x")).toBeNull();
+ expect(getNbs("1010111000")).toBeNull();
+ expect(getNbs("1,0101,11,00")).toBeNull();
+ });
+
+ it("should return null for an empty string and for whitespace only", () => {
+ expect(getNbs("")).toBeNull();
+ expect(getNbs(" ")).toBeNull();
+ });
+
+ it("should return null for null and undefined", () => {
+ // @ts-expect-error not a string or number
+ expect(getNbs(null)).toBeNull();
+ // @ts-expect-error not a string or number
+ expect(getNbs()).toBeNull();
+ });
+
+ it("should return null for a number that is not a non-negative safe integer", () => {
+ expect(getNbs(-101_011_100)).toBeNull();
+ expect(getNbs(101_011_100.5)).toBeNull();
+ expect(getNbs(2 ** 53)).toBeNull();
+ });
+
+ it("should return null for the keys of Object.prototype", () => {
+ for (const key of PROTOTYPE_KEYS) expect(getNbs(key)).toBeNull();
+ });
+
+ describe("properties", () => {
+ const codeArbitrary = fc.constantFrom(...Object.keys(NBS_DESCRIPTIONS));
+
+ test("should never throw, regardless of the input", () => {
+ expectNeverThrows(getNbs, anyGarbage);
+ });
+
+ test("should resolve every known code, bare, masked or as a number, and agree with isValidNbs", () => {
+ fc.assert(
+ fc.property(codeArbitrary, (code) => {
+ const expected = { code, description: NBS_DESCRIPTIONS[code] };
+
+ expect(getNbs(code)).toEqual(expected);
+ expect(getNbs(Number(code))).toEqual(expected);
+ expect(getNbs(formatNbs(code))).toEqual(expected);
+ expect(isValidNbs(code)).toBe(true);
+ }),
+ );
+ });
+
+ test("should only carry 9 digit codes starting with 1 and non-empty descriptions", () => {
+ fc.assert(
+ fc.property(codeArbitrary, (code) => {
+ expect(code).toMatch(/^1\d{8}$/);
+ expect(NBS_DESCRIPTIONS[code].trim()).not.toBe("");
+ }),
+ );
+ });
+ });
+});
+
+describe("getNbs types", () => {
+ test("should take a string or number and return an Nbs or null", () => {
+ expectTypeOf(getNbs).parameter(0).toEqualTypeOf();
+ expectTypeOf(getNbs).returns.toEqualTypeOf();
+ expectTypeOf().toEqualTypeOf<{ code: string; description: string }>();
+ });
+});
diff --git a/src/get-nbs/get-nbs.ts b/src/get-nbs/get-nbs.ts
new file mode 100644
index 000000000..a2327a5e3
--- /dev/null
+++ b/src/get-nbs/get-nbs.ts
@@ -0,0 +1,67 @@
+import { NBS_DESCRIPTIONS, NBS_FORMAT_REGEX } from "../_internals/constants/nbs";
+import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code";
+import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits";
+
+/**
+ * A code of the NBS (Nomenclatura Brasileira de Serviços, Intangíveis e Outras Operações que
+ * Produzam Variações no Patrimônio).
+ */
+export type Nbs = {
+ /** The 9 digit code, without the `N.NNNN.NN.NN` mask. */
+ code: string;
+ /** The official description, as the NBS 2.0 table prints it. */
+ description: string;
+};
+
+/**
+ * Looks an NBS (Nomenclatura Brasileira de Serviços, Intangíveis e Outras Operações que
+ * Produzam Variações no Patrimônio) code up in the official NBS 2.0 table, the code the national
+ * NFS-e carries in `cNBS`.
+ *
+ * An NBS code has 9 digits: the digit 1, two of the chapter, two of the position, the two
+ * subposition levels, the item and the subitem, printed as `N.NNNN.NN.NN`. Only complete codes
+ * are in the table; the chapter (`1.01`), position (`1.0101`) and subposition (`1.0101.1`)
+ * headings classify nothing by themselves and give `null`.
+ *
+ * A string is only read as a code when it is written in one of the documented forms: the 9
+ * digits, or the `N.NNNN.NN.NN` mask, with a single separator between the groups and optional
+ * surrounding whitespace. Anything else (`"1.0101abc11.00"`) 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.
+ * Every code starts with 1, so nothing is padded.
+ *
+ * @param {string|number} value - The NBS code to look up, with or without the mask, e.g.
+ * `"1.0101.11.00"`, `"101011100"` or `101011100`.
+ * @returns {Nbs|null} The matching code, or null when it is unknown or invalid.
+ *
+ * @example
+ * ```typescript
+ * getNbs("1.0101.11.00");
+ * // { code: "101011100", description: "Serviços de construção de edificações residenciais de um e dois pavimentos" }
+ * getNbs(126050000); // { code: "126050000", description: "Serviços domésticos" }
+ * getNbs("1.0101"); // null (a position heading, not a complete code)
+ * getNbs("1.9999.99.99"); // null
+ * ```
+ *
+ * @see Official: https://www.gov.br/mdic/pt-br/assuntos/sdic/comercio-e-servicos/nbs-nomenclatura-brasileira-de-servicos
+ * The NBS page of the MDIC: NBS 2.0, approved by the Portaria Conjunta RFB/SCS 1.429/2018 and
+ * amended by the Portaria Conjunta RFB/SCS 2.000/2018, whose Anexo I states how the code is
+ * formed ("Formação do código da NBS").
+ * @see Official: https://www.gov.br/mdic/pt-br/images/REPOSITORIO/scs/decos/NBS/NBSa_2-0.csv
+ * The NBS 2.0 table in CSV, the file `NBS_DESCRIPTIONS` is generated from.
+ * @see Official: https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual
+ * Sistema Nacional NFS-e, `tiposSimples_v1.01.xsd`: `TSCodNBS` is `[0-9]{9}`.
+ */
+export const getNbs = (value: string | number): Nbs | null => {
+ if (!isLookupCode(value)) return null;
+
+ const code = String(value).trim();
+
+ if (!NBS_FORMAT_REGEX.test(code)) return null;
+
+ const digits = sanitizeToDigits(code);
+ const description = NBS_DESCRIPTIONS[digits];
+
+ if (description === undefined) return null;
+
+ return { code: digits, description };
+};
diff --git a/src/get-service-item/get-service-item.test.ts b/src/get-service-item/get-service-item.test.ts
new file mode 100644
index 000000000..a9dc459d0
--- /dev/null
+++ b/src/get-service-item/get-service-item.test.ts
@@ -0,0 +1,150 @@
+import * as fc from "fast-check";
+
+import { SERVICE_ITEM_DESCRIPTIONS } from "../_internals/constants/service-items";
+import { anyGarbage, PROTOTYPE_KEYS } from "../_internals/test/arbitraries";
+import { expectNeverThrows } from "../_internals/test/properties";
+import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime";
+import { isValidServiceItem } from "../is-valid-service-item/is-valid-service-item";
+import { getServiceItem, type ServiceItem } from "./get-service-item";
+
+const SYSTEMS = { code: "1.01", description: "Análise e desenvolvimento de sistemas." };
+
+describe("getServiceItem", () => {
+ it("should return the subitem for the form the law prints", () => {
+ expect(getServiceItem("1.01")).toEqual(SYSTEMS);
+ expect(getServiceItem("1.02")).toEqual({ code: "1.02", description: "Programação." });
+ });
+
+ it("should return the subitem for a zero padded item", () => {
+ expect(getServiceItem("01.01")).toEqual(SYSTEMS);
+ });
+
+ it("should return the subitem for the bare digits, the first four of a cTribNac", () => {
+ expect(getServiceItem("0101")).toEqual(SYSTEMS);
+ expect(getServiceItem("101")).toEqual(SYSTEMS);
+ expect(getServiceItem(101)).toEqual(SYSTEMS);
+ });
+
+ it("should return a two digit item without a leading zero in the code", () => {
+ expect(getServiceItem("40.01")).toEqual({
+ code: "40.01",
+ description: "Obras de arte sob encomenda.",
+ });
+ expect(getServiceItem(4001)).toEqual({
+ code: "40.01",
+ description: "Obras de arte sob encomenda.",
+ });
+ expect(getServiceItem("10.04")?.code).toBe("10.04");
+ });
+
+ it("should return the subitem 11.05, added by the Lei Complementar 183/2021", () => {
+ expect(getServiceItem("11.05")?.code).toBe("11.05");
+ });
+
+ it("should ignore surrounding whitespace", () => {
+ expect(getServiceItem(" 1.01\n")).toEqual(SYSTEMS);
+ });
+
+ it("should return a fresh object that does not leak the internal table", () => {
+ expect(getServiceItem("1.01")).not.toBe(getServiceItem("1.01"));
+ });
+
+ it("should return null for the vetoed subitems", () => {
+ expect(getServiceItem("3.01")).toBeNull();
+ expect(getServiceItem("7.14")).toBeNull();
+ expect(getServiceItem("7.15")).toBeNull();
+ expect(getServiceItem("13.01")).toBeNull();
+ expect(getServiceItem("17.07")).toBeNull();
+ });
+
+ it("should return null for item 99 of the national list, which is not part of the law", () => {
+ expect(getServiceItem("99.01")).toBeNull();
+ });
+
+ it("should return null for an item heading and for a 6 digit national code", () => {
+ expect(getServiceItem("1")).toBeNull();
+ expect(getServiceItem("01")).toBeNull();
+ expect(getServiceItem("010101")).toBeNull();
+ expect(getServiceItem("1.01.01")).toBeNull();
+ });
+
+ it("should return null for a subitem the list does not carry", () => {
+ expect(getServiceItem("1.10")).toBeNull();
+ expect(getServiceItem("41.01")).toBeNull();
+ expect(getServiceItem("0.01")).toBeNull();
+ });
+
+ it("should return null for a string that is not written in a documented form", () => {
+ expect(getServiceItem("1.1")).toBeNull();
+ expect(getServiceItem("1..01")).toBeNull();
+ expect(getServiceItem("1-01")).toBeNull();
+ expect(getServiceItem("1,01")).toBeNull();
+ expect(getServiceItem("x1.01")).toBeNull();
+ expect(getServiceItem("1.01x")).toBeNull();
+ expect(getServiceItem("001.01")).toBeNull();
+ });
+
+ it("should return null for an empty string and for whitespace only", () => {
+ expect(getServiceItem("")).toBeNull();
+ expect(getServiceItem(" ")).toBeNull();
+ });
+
+ it("should return null for null and undefined", () => {
+ // @ts-expect-error not a string or number
+ expect(getServiceItem(null)).toBeNull();
+ // @ts-expect-error not a string or number
+ expect(getServiceItem()).toBeNull();
+ });
+
+ it("should return null for a number that is not a non-negative safe integer", () => {
+ expect(getServiceItem(1.01)).toBeNull();
+ expect(getServiceItem(-101)).toBeNull();
+ expect(getServiceItem(2 ** 53)).toBeNull();
+ });
+
+ it("should return null for the keys of Object.prototype", () => {
+ for (const key of PROTOTYPE_KEYS) expect(getServiceItem(key)).toBeNull();
+ });
+
+ describe("properties", () => {
+ const keyArbitrary = fc.constantFrom(...Object.keys(SERVICE_ITEM_DESCRIPTIONS));
+
+ test("should never throw, regardless of the input", () => {
+ expectNeverThrows(getServiceItem, anyGarbage);
+ });
+
+ test("should resolve every subitem of the table in every documented form and agree with isValidServiceItem", () => {
+ fc.assert(
+ fc.property(keyArbitrary, (key) => {
+ const description = SERVICE_ITEM_DESCRIPTIONS[key];
+ const padded = `${key.slice(0, 2)}.${key.slice(2)}`;
+ const found = getServiceItem(key);
+
+ expect(found?.description).toBe(description);
+ expect(found?.code).toMatch(/^[1-9]\d?\.\d{2}$/);
+ expect(getServiceItem(padded)).toEqual(found);
+ expect(getServiceItem(Number(key))).toEqual(found);
+ expect(getServiceItem(found?.code ?? "")).toEqual(found);
+ expect(isValidServiceItem(key)).toBe(true);
+ }),
+ );
+ });
+
+ test("should only carry items 01 to 40 and non-empty descriptions", () => {
+ fc.assert(
+ fc.property(keyArbitrary, (key) => {
+ expect(key).toMatch(/^(?:0[1-9]|[1-3]\d|40)\d{2}$/);
+ expect(SERVICE_ITEM_DESCRIPTIONS[key].trim()).not.toBe("");
+ }),
+ );
+ });
+ });
+});
+
+describe("getServiceItem types", () => {
+ test("should take a string or number and return a ServiceItem or null", () => {
+ expectTypeOf(getServiceItem).parameter(0).toEqualTypeOf();
+ expectTypeOf(getServiceItem).returns.toEqualTypeOf();
+ expectTypeOf().toEqualTypeOf<{ code: string; description: string }>();
+ });
+});
diff --git a/src/get-service-item/get-service-item.ts b/src/get-service-item/get-service-item.ts
new file mode 100644
index 000000000..13efdbc12
--- /dev/null
+++ b/src/get-service-item/get-service-item.ts
@@ -0,0 +1,72 @@
+import {
+ SERVICE_ITEM_DESCRIPTIONS,
+ SERVICE_ITEM_FORMAT_REGEX,
+} from "../_internals/constants/service-items";
+import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code";
+import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits";
+
+const SUBITEM_LENGTH = 4;
+
+const ITEM_LENGTH = 2;
+
+/** A subitem of the service list annexed to the Lei Complementar 116/2003. */
+export type ServiceItem = {
+ /** The subitem as the law numbers it, e.g. `"1.01"` or `"17.25"`. */
+ code: string;
+ /** The description of the subitem, as the list in force prints it. */
+ description: string;
+};
+
+/**
+ * Looks a subitem up in the service list annexed to the Lei Complementar 116/2003, the list of
+ * the services the ISS is levied on.
+ *
+ * The law numbers a subitem as the item, a dot and two digits, `1.01` to `40.01`. It is accepted
+ * in that form, with a zero padded item (`"01.01"`) or as the bare digits (`"0101"`, `"101"`),
+ * which are the first four digits of the `cTribNac` code of the national NFS-e, and optional
+ * surrounding whitespace. A number is only read when it is a non-negative safe integer, so
+ * `101` is `1.01` while the float `1.01` gives `null`: write the dotted form as a string.
+ *
+ * Only the subitems in force are in the table. The vetoed ones (`3.01`, `7.14`, `7.15`, `13.01`
+ * and `17.07`) give `null`, and so do the item headings (`"1"`), the national codes a subitem is
+ * split into (`"010101"`) and item 99 of the national list, which is not part of the law.
+ * Municipal service codes are out of scope.
+ *
+ * @param {string|number} value - The subitem to look up, e.g. `"1.01"`, `"01.01"`, `"0101"` or
+ * `101`.
+ * @returns {ServiceItem|null} The matching subitem, or null when it is unknown or invalid.
+ *
+ * @example
+ * ```typescript
+ * getServiceItem("1.01"); // { code: "1.01", description: "Análise e desenvolvimento de sistemas." }
+ * getServiceItem("0101"); // { code: "1.01", description: "Análise e desenvolvimento de sistemas." }
+ * getServiceItem("40.01"); // { code: "40.01", description: "Obras de arte sob encomenda." }
+ * getServiceItem("3.01"); // null (vetoed)
+ * getServiceItem(1.01); // null (not a non-negative safe integer)
+ * ```
+ *
+ * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp116.htm
+ * Lei Complementar 116/2003, "Lista de serviços anexa".
+ * @see Official: https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual
+ * Sistema Nacional NFS-e, `ANEXO_B-NBS2-LISTA_SERVICO_NACIONAL`, sheet `LISTA.SERV.NAC.`: the
+ * list in force in machine readable form, which `SERVICE_ITEM_DESCRIPTIONS` is generated from,
+ * and `TSCodTribNac` of `tiposSimples_v1.01.xsd`, "2 para Item (LC 116/2003), 2 para Subitem (LC
+ * 116/2003) e 2 para Desdobro Nacional".
+ */
+export const getServiceItem = (value: string | number): ServiceItem | null => {
+ if (!isLookupCode(value)) return null;
+
+ const written = String(value).trim();
+
+ if (!SERVICE_ITEM_FORMAT_REGEX.test(written)) return null;
+
+ const digits = sanitizeToDigits(written).padStart(SUBITEM_LENGTH, "0");
+ const description = SERVICE_ITEM_DESCRIPTIONS[digits];
+
+ if (description === undefined) return null;
+
+ return {
+ code: `${Number(digits.slice(0, ITEM_LENGTH))}.${digits.slice(ITEM_LENGTH)}`,
+ description,
+ };
+};
diff --git a/src/index.test.ts b/src/index.test.ts
index 2bfdffa30..665e08d90 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -82,6 +82,7 @@ import {
type LegalNatureCategory,
type LicensePlateFormat,
type Municipality,
+ type Nbs,
type NfeKeyInfo,
type NfeKeyModel,
type NfseKeyGeneratorEnvironment,
@@ -98,6 +99,7 @@ import {
type PixPayloadInfo,
type PixPointOfInitiation,
type RegistroProfissionalCouncil,
+ type ServiceItem,
type State,
type StateCode,
type StandardSchemaV1,
@@ -147,6 +149,7 @@ const PUBLIC = [
"formatIban",
"formatLegalNature",
"formatLicensePlate",
+ "formatNbs",
"formatNcm",
"formatNfeKey",
"formatPassport",
@@ -197,10 +200,12 @@ const PUBLIC = [
"getMunicipalities",
"getMunicipality",
"getMunicipalityByCode",
+ "getNbs",
"getNfeKeyInfo",
"getNfseKeyInfo",
"getPixKeyInfo",
"getPixPayloadInfo",
+ "getServiceItem",
"getStateByCep",
"getStateByIbgeCode",
"getStateCodeByName",
@@ -238,6 +243,7 @@ const PUBLIC = [
"isValidLegalNature",
"isValidLicensePlate",
"isValidMobilePhone",
+ "isValidNbs",
"isValidNcm",
"isValidNfeKey",
"isValidNfseKey",
@@ -250,6 +256,7 @@ const PUBLIC = [
"isValidProcessoJuridico",
"isValidRegistroProfissional",
"isValidRenavam",
+ "isValidServiceItem",
"isValidServicePhone",
"isValidSuframa",
"isValidVin",
@@ -394,6 +401,7 @@ describe("Public API", () => {
LegalNatureCategory: LegalNatureCategory;
LicensePlateFormat: LicensePlateFormat;
Municipality: Municipality;
+ Nbs: Nbs;
NfeKeyInfo: NfeKeyInfo;
NfeKeyModel: NfeKeyModel;
NfseKeyGeneratorEnvironment: NfseKeyGeneratorEnvironment;
@@ -410,6 +418,7 @@ describe("Public API", () => {
PixPayloadInfo: PixPayloadInfo;
PixPointOfInitiation: PixPointOfInitiation;
RegistroProfissionalCouncil: RegistroProfissionalCouncil;
+ ServiceItem: ServiceItem;
State: State;
StateCode: StateCode;
StandardSchemaV1: StandardSchemaV1;
diff --git a/src/index.ts b/src/index.ts
index dfd10232d..17da55681 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -44,6 +44,7 @@ export {
formatLegalNature,
} from "./format-legal-nature/format-legal-nature";
export { formatLicensePlate } from "./format-license-plate/format-license-plate";
+export { formatNbs } from "./format-nbs/format-nbs";
export { type FormatNcmOptions, formatNcm } from "./format-ncm/format-ncm";
export { type FormatNfeKeyOptions, formatNfeKey } from "./format-nfe-key/format-nfe-key";
export { formatPassport } from "./format-passport/format-passport";
@@ -153,6 +154,7 @@ export {
getMunicipality,
} from "./get-municipality/get-municipality";
export { getMunicipalityByCode } from "./get-municipality-by-code/get-municipality-by-code";
+export { type Nbs, getNbs } from "./get-nbs/get-nbs";
export {
type NfeKeyInfo,
type NfeKeyModel,
@@ -174,6 +176,7 @@ export {
type PixPointOfInitiation,
getPixPayloadInfo,
} from "./get-pix-payload-info/get-pix-payload-info";
+export { type ServiceItem, getServiceItem } from "./get-service-item/get-service-item";
export { getStateByCep } from "./get-state-by-cep/get-state-by-cep";
export { getStateByIbgeCode } from "./get-state-by-ibge-code/get-state-by-ibge-code";
export { getStateCodeByName } from "./get-state-code-by-name/get-state-code-by-name";
@@ -216,6 +219,7 @@ export {
type IsValidMobilePhoneOptions,
isValidMobilePhone,
} from "./is-valid-mobile-phone/is-valid-mobile-phone";
+export { isValidNbs } from "./is-valid-nbs/is-valid-nbs";
export { isValidNcm } from "./is-valid-ncm/is-valid-ncm";
export { isValidNfeKey } from "./is-valid-nfe-key/is-valid-nfe-key";
export { isValidNfseKey } from "./is-valid-nfse-key/is-valid-nfse-key";
@@ -236,6 +240,7 @@ export {
isValidRegistroProfissional,
} from "./is-valid-registro-profissional/is-valid-registro-profissional";
export { isValidRenavam } from "./is-valid-renavam/is-valid-renavam";
+export { isValidServiceItem } from "./is-valid-service-item/is-valid-service-item";
export { isValidServicePhone } from "./is-valid-service-phone/is-valid-service-phone";
export { isValidSuframa } from "./is-valid-suframa/is-valid-suframa";
export { isValidVin } from "./is-valid-vin/is-valid-vin";
diff --git a/src/is-valid-nbs/is-valid-nbs.test.ts b/src/is-valid-nbs/is-valid-nbs.test.ts
new file mode 100644
index 000000000..90cf28d87
--- /dev/null
+++ b/src/is-valid-nbs/is-valid-nbs.test.ts
@@ -0,0 +1,63 @@
+import { anyGarbage, anyValue } from "../_internals/test/arbitraries";
+import { expectAlwaysReturnsType, expectNeverThrows } from "../_internals/test/properties";
+import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime";
+import { isValidNbs } from "./is-valid-nbs";
+
+describe("isValidNbs", () => {
+ it("should validate an NBS code without a mask", () => {
+ expect(isValidNbs("101011100")).toBe(true);
+ });
+
+ it("should validate an NBS code with the N.NNNN.NN.NN mask", () => {
+ expect(isValidNbs("1.0101.11.00")).toBe(true);
+ expect(isValidNbs("1.2606.00.00")).toBe(true);
+ });
+
+ it("should validate an NBS code given as a number", () => {
+ expect(isValidNbs(101_011_100)).toBe(true);
+ });
+
+ it("should reject the headings of the nomenclature", () => {
+ expect(isValidNbs("1.01")).toBe(false);
+ expect(isValidNbs("1.0101")).toBe(false);
+ expect(isValidNbs("1.0101.1")).toBe(false);
+ });
+
+ it("should reject a well formed code the table does not carry", () => {
+ expect(isValidNbs("1.9999.99.99")).toBe(false);
+ expect(isValidNbs("1.0101.11.01")).toBe(false);
+ });
+
+ it("should reject a malformed value", () => {
+ expect(isValidNbs("1.0101abc11.00")).toBe(false);
+ expect(isValidNbs("1..0101.11.00")).toBe(false);
+ expect(isValidNbs("")).toBe(false);
+ });
+
+ it("should reject null, undefined and non lookup values", () => {
+ // @ts-expect-error not a string or number
+ expect(isValidNbs(null)).toBe(false);
+ // @ts-expect-error not a string or number
+ expect(isValidNbs()).toBe(false);
+ // @ts-expect-error not a string or number
+ expect(isValidNbs(["101011100"])).toBe(false);
+ expect(isValidNbs(-101_011_100)).toBe(false);
+ });
+
+ describe("properties", () => {
+ test("should never throw, regardless of the input", () => {
+ expectNeverThrows(isValidNbs, anyGarbage);
+ });
+
+ test("should always return a boolean", () => {
+ expectAlwaysReturnsType(isValidNbs, "boolean", anyValue);
+ });
+ });
+});
+
+describe("isValidNbs types", () => {
+ test("should take a string or number and return a boolean", () => {
+ expectTypeOf(isValidNbs).parameter(0).toEqualTypeOf();
+ expectTypeOf(isValidNbs).returns.toEqualTypeOf();
+ });
+});
diff --git a/src/is-valid-nbs/is-valid-nbs.ts b/src/is-valid-nbs/is-valid-nbs.ts
new file mode 100644
index 000000000..d8f8f8286
--- /dev/null
+++ b/src/is-valid-nbs/is-valid-nbs.ts
@@ -0,0 +1,35 @@
+import { getNbs } from "../get-nbs/get-nbs";
+
+/**
+ * Checks if a value is a code of the official NBS 2.0 table (Nomenclatura Brasileira de
+ * Serviços, Intangíveis e Outras Operações que Produzam Variações no Patrimônio), the code the
+ * national NFS-e carries in `cNBS`.
+ *
+ * Accepts what `getNbs` accepts: the 9 digits or the `N.NNNN.NN.NN` mask, with a single
+ * separator between the groups and optional surrounding whitespace, or a non-negative safe
+ * integer. Only complete codes are valid; the chapter, position and subposition headings of the
+ * nomenclature are not.
+ *
+ * The table is the NBS 2.0 the MDIC publishes. The ANEXO B of the Sistema Nacional NFS-e lists
+ * the same codes except three (`1.0402.29.00`, `1.0403.29.00` and `1.0904.40.00`), so a code
+ * valid here can still be refused by the NFS-e.
+ *
+ * @param {string|number} value - The NBS code to be validated.
+ * @returns {boolean} True if the code is in the table, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * isValidNbs("1.0101.11.00"); // true
+ * isValidNbs("101011100"); // true
+ * isValidNbs(101011100); // true
+ * isValidNbs("1.0101"); // false (a position heading, not a complete code)
+ * isValidNbs("1.9999.99.99"); // false
+ * ```
+ *
+ * @see Official: https://www.gov.br/mdic/pt-br/assuntos/sdic/comercio-e-servicos/nbs-nomenclatura-brasileira-de-servicos
+ * The NBS page of the MDIC: NBS 2.0, approved by the Portaria Conjunta RFB/SCS 1.429/2018 and
+ * amended by the Portaria Conjunta RFB/SCS 2.000/2018.
+ * @see Official: https://www.gov.br/mdic/pt-br/images/REPOSITORIO/scs/decos/NBS/NBSa_2-0.csv
+ * The NBS 2.0 table in CSV.
+ */
+export const isValidNbs = (value: string | number): boolean => getNbs(value) !== null;
diff --git a/src/is-valid-service-item/is-valid-service-item.test.ts b/src/is-valid-service-item/is-valid-service-item.test.ts
new file mode 100644
index 000000000..1405eb690
--- /dev/null
+++ b/src/is-valid-service-item/is-valid-service-item.test.ts
@@ -0,0 +1,61 @@
+import { anyGarbage, anyValue } from "../_internals/test/arbitraries";
+import { expectAlwaysReturnsType, expectNeverThrows } from "../_internals/test/properties";
+import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime";
+import { isValidServiceItem } from "./is-valid-service-item";
+
+describe("isValidServiceItem", () => {
+ it("should validate a subitem in the form the law prints", () => {
+ expect(isValidServiceItem("1.01")).toBe(true);
+ expect(isValidServiceItem("17.25")).toBe(true);
+ expect(isValidServiceItem("40.01")).toBe(true);
+ });
+
+ it("should validate a zero padded item and the bare digits", () => {
+ expect(isValidServiceItem("01.01")).toBe(true);
+ expect(isValidServiceItem("0101")).toBe(true);
+ expect(isValidServiceItem(101)).toBe(true);
+ });
+
+ it("should reject the vetoed subitems", () => {
+ expect(isValidServiceItem("3.01")).toBe(false);
+ expect(isValidServiceItem("17.07")).toBe(false);
+ });
+
+ it("should reject item 99 of the national list and the 6 digit national codes", () => {
+ expect(isValidServiceItem("99.01")).toBe(false);
+ expect(isValidServiceItem("010101")).toBe(false);
+ });
+
+ it("should reject a malformed value", () => {
+ expect(isValidServiceItem("1.1")).toBe(false);
+ expect(isValidServiceItem("1-01")).toBe(false);
+ expect(isValidServiceItem("")).toBe(false);
+ });
+
+ it("should reject null, undefined and non lookup values", () => {
+ // @ts-expect-error not a string or number
+ expect(isValidServiceItem(null)).toBe(false);
+ // @ts-expect-error not a string or number
+ expect(isValidServiceItem()).toBe(false);
+ // @ts-expect-error not a string or number
+ expect(isValidServiceItem(["1.01"])).toBe(false);
+ expect(isValidServiceItem(1.01)).toBe(false);
+ });
+
+ describe("properties", () => {
+ test("should never throw, regardless of the input", () => {
+ expectNeverThrows(isValidServiceItem, anyGarbage);
+ });
+
+ test("should always return a boolean", () => {
+ expectAlwaysReturnsType(isValidServiceItem, "boolean", anyValue);
+ });
+ });
+});
+
+describe("isValidServiceItem types", () => {
+ test("should take a string or number and return a boolean", () => {
+ expectTypeOf(isValidServiceItem).parameter(0).toEqualTypeOf();
+ expectTypeOf(isValidServiceItem).returns.toEqualTypeOf();
+ });
+});
diff --git a/src/is-valid-service-item/is-valid-service-item.ts b/src/is-valid-service-item/is-valid-service-item.ts
new file mode 100644
index 000000000..689148280
--- /dev/null
+++ b/src/is-valid-service-item/is-valid-service-item.ts
@@ -0,0 +1,31 @@
+import { getServiceItem } from "../get-service-item/get-service-item";
+
+/**
+ * Checks if a value is a subitem in force of the service list annexed to the Lei Complementar
+ * 116/2003, the list of the services the ISS is levied on.
+ *
+ * Accepts what `getServiceItem` accepts: the form the law prints (`"1.01"`), a zero padded item
+ * (`"01.01"`), the bare digits (`"0101"`, `"101"` or the integer `101`), with optional
+ * surrounding whitespace. Vetoed subitems, item headings, the 6 digit national codes of the
+ * NFS-e and item 99 of the national list are not valid.
+ *
+ * @param {string|number} value - The subitem to be validated.
+ * @returns {boolean} True if the subitem is in the list in force, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * isValidServiceItem("1.01"); // true
+ * isValidServiceItem("0101"); // true
+ * isValidServiceItem("17.25"); // true
+ * isValidServiceItem("3.01"); // false (vetoed)
+ * isValidServiceItem("99.01"); // false (national list only, not the law)
+ * ```
+ *
+ * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp116.htm
+ * Lei Complementar 116/2003, "Lista de serviços anexa".
+ * @see Official: https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual
+ * Sistema Nacional NFS-e, `ANEXO_B-NBS2-LISTA_SERVICO_NACIONAL`, sheet `LISTA.SERV.NAC.`: the
+ * list in force in machine readable form.
+ */
+export const isValidServiceItem = (value: string | number): boolean =>
+ getServiceItem(value) !== null;
From 6ade0164a3e47965ff8e9ce5597d3f209c32fbd6 Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Sat, 19 Sep 2026 11:46:20 -0300
Subject: [PATCH 02/12] fix(nbs): read the quoted description of the official
CSV
One row of the MDIC NBSa_2-0.csv, the code 1.1706.24.00, is written as a
quoted field because its description carries quotation marks. The parser
split the row at the first semicolon and took the rest of the line as it was,
so the outer quotes and the doubled inner ones shipped in the table, while
the Anexo I of the Portaria prints the plain wording.
The reader now follows RFC 4180: a line break inside a quoted field no longer
ends a record, a quoted field loses its outer quotes and every doubled quote
inside it becomes one, and a field that opens a quote it never closes fails
the run instead of shipping.
---
scripts/nbs.ts | 53 +++++++++++++++++++++++++++++----
src/_internals/constants/nbs.ts | 2 +-
src/get-nbs/get-nbs.test.ts | 17 +++++++++++
3 files changed, 65 insertions(+), 7 deletions(-)
diff --git a/scripts/nbs.ts b/scripts/nbs.ts
index 018423361..f08773939 100644
--- a/scripts/nbs.ts
+++ b/scripts/nbs.ts
@@ -4,6 +4,7 @@ import { writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fetchSortedRecord } from "./fetch-sorted-record.ts";
+import { serializeRecord } from "./serialize-record.ts";
const scriptsDir = import.meta.dirname;
@@ -25,13 +26,56 @@ const NBS_CODE_REGEX = /^\d\.\d{4}\.\d{2}\.\d{2}$/;
*/
const MINIMUM_CODES = 900;
+/**
+ * Splits a CSV into records. A line break inside a quoted field does not end a record, so a
+ * description written over two lines stays in one piece.
+ * @param {string} csv - The decoded CSV.
+ * @returns {string[]} One string per record, the line breaks between records removed.
+ */
+const splitRecords = (csv: string): string[] => {
+ const records: string[] = [];
+ let record = "";
+ let quoted = false;
+
+ for (const character of csv) {
+ if (character === '"') quoted = !quoted;
+
+ if (character === "\n" && !quoted) {
+ records.push(record);
+ record = "";
+ } else if (character !== "\r" || quoted) {
+ record += character;
+ }
+ }
+
+ return [...records, record];
+};
+
+/**
+ * Reads a field the way RFC 4180 writes it: a quoted field loses its outer quotes and every
+ * doubled quote inside it becomes one.
+ * @param {string} field - The raw field, quotes included.
+ * @returns {string} The text of the field, whitespace collapsed.
+ */
+const readField = (field: string): string => {
+ const trimmed = field.trim();
+
+ if (!trimmed.startsWith('"')) return trimmed.replaceAll(/\s+/g, " ");
+
+ if (trimmed.length < 2 || !trimmed.endsWith('"')) {
+ throw new Error(`NBS CSV field ${trimmed} opens a quote it never closes`);
+ }
+
+ return trimmed.slice(1, -1).replaceAll('""', '"').replaceAll(/\s+/g, " ");
+};
+
/**
* Reads the `NBS 2.0;DESCRIÇÃO` rows of the official NBS 2.0 CSV.
* @param {string} csv - The decoded CSV.
* @returns {Record} One entry per complete code, keyed by its 9 digits.
*/
const parseCsv = (csv: string): Record => {
- const [header, ...rows] = csv.split(/\r?\n/);
+ const [header, ...rows] = splitRecords(csv);
if (header?.trim() !== NBS_CSV_HEADER) {
throw new Error(`NBS CSV header is not "${NBS_CSV_HEADER}"`);
@@ -42,10 +86,7 @@ const parseCsv = (csv: string): Record => {
for (const row of rows) {
const separator = row.indexOf(";");
const code = row.slice(0, Math.max(separator, 0)).trim();
- const description = row
- .slice(separator + 1)
- .trim()
- .replaceAll(/\s+/g, " ");
+ const description = readField(row.slice(separator + 1));
if (!NBS_CODE_REGEX.test(code) || description === "") continue;
@@ -88,7 +129,7 @@ const main = async (): Promise => {
* @see Official: https://www.gov.br/mdic/pt-br/images/REPOSITORIO/scs/decos/NBS/NBSa_2-0.csv
* The NBS 2.0 table in CSV.
*/
-export const NBS_DESCRIPTIONS: Record = ${JSON.stringify(sorted)};
+export const NBS_DESCRIPTIONS: Record = ${serializeRecord(sorted)};
/**
* Shape an NBS code has to be written in: the 9 digits, optionally split into the printed
diff --git a/src/_internals/constants/nbs.ts b/src/_internals/constants/nbs.ts
index 030011475..7390645fd 100644
--- a/src/_internals/constants/nbs.ts
+++ b/src/_internals/constants/nbs.ts
@@ -930,7 +930,7 @@ export const NBS_DESCRIPTIONS: Record = {
"117062300":
"Serviços de distribuição de pacotes de programação adicional de televisão por assinatura",
"117062400":
- '"Serviços de distribuição de programas de televisão por assinatura, na modalidade ""pague por exibição"" (pay-per-view)"',
+ 'Serviços de distribuição de programas de televisão por assinatura, na modalidade "pague por exibição" (pay-per-view)',
"117069000":
"Serviços de difusão, programação e distribuição de programas de rádio e televisão não classificados em subposições anteriores",
"118011100": "Serviço de recrutamento e seleção de profissionais executivos",
diff --git a/src/get-nbs/get-nbs.test.ts b/src/get-nbs/get-nbs.test.ts
index 84f626cc1..9a48f9dc8 100644
--- a/src/get-nbs/get-nbs.test.ts
+++ b/src/get-nbs/get-nbs.test.ts
@@ -54,6 +54,14 @@ describe("getNbs", () => {
expect(getNbs("1.0402.11.19")).toBeNull();
});
+ it("should return the only description the official CSV quotes, without its CSV quoting", () => {
+ expect(getNbs("1.1706.24.00")).toEqual({
+ code: "117062400",
+ description:
+ 'Serviços de distribuição de programas de televisão por assinatura, na modalidade "pague por exibição" (pay-per-view)',
+ });
+ });
+
it("should return a fresh object that does not leak the internal table", () => {
expect(getNbs("101011100")).not.toBe(getNbs("101011100"));
});
@@ -136,6 +144,15 @@ describe("getNbs", () => {
}),
);
});
+
+ test("should carry no description with the quoting of the CSV left on it", () => {
+ fc.assert(
+ fc.property(codeArbitrary, (code) => {
+ expect(NBS_DESCRIPTIONS[code].startsWith('"')).toBe(false);
+ expect(NBS_DESCRIPTIONS[code]).not.toContain('""');
+ }),
+ );
+ });
});
});
From cd6b85c2ab46c0c6b01cecfc215c7e3411d71d4c Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Sat, 19 Sep 2026 11:46:28 -0300
Subject: [PATCH 03/12] refactor(scripts): harden the xlsx reader and emit the
tables sorted
The reader was written for the one workbook it reads today, which left a few
ways for a future publication to corrupt a table quietly. It now checks the
central directory signature of every entry instead of trusting the offset,
inflates with a size cap so a corrupt archive cannot exhaust the memory,
inflates only the files a caller asks for, decodes the numeric character
references a writer other than Excel emits, places a row by its r attribute
so a skipped row leaves a gap instead of shifting the table, reads a cell
whatever the order of its attributes and an inline string as text, resolves
an absolute or upwards relationship target, and fails when the rows it read
do not add up to the dimension the sheet declares.
serializeRecord replaces the sort-into-a-fresh-record loop the generator
copied from fetchSortedRecord. JavaScript hoists the keys that are canonical
array indices, so the service list was emitted with 10.01 to 40.01 ahead of
1.01 to 9.03 although the record was built from sorted keys. Writing the
entries out in key order makes the generated file read the way it is meant
to. The data is unchanged, and the reader gives the same table back.
---
scripts/read-xlsx-sheet.ts | 149 ++++++++++++---
scripts/serialize-record.ts | 15 ++
scripts/service-items.ts | 8 +-
src/_internals/constants/service-items.ts | 212 +++++++++++-----------
4 files changed, 248 insertions(+), 136 deletions(-)
create mode 100644 scripts/serialize-record.ts
diff --git a/scripts/read-xlsx-sheet.ts b/scripts/read-xlsx-sheet.ts
index 741bf4b30..01db599f4 100644
--- a/scripts/read-xlsx-sheet.ts
+++ b/scripts/read-xlsx-sheet.ts
@@ -2,12 +2,17 @@ import { inflateRawSync } from "node:zlib";
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06_05_4b_50;
+const CENTRAL_DIRECTORY_SIGNATURE = 0x02_01_4b_50;
+
const CENTRAL_DIRECTORY_HEADER_SIZE = 46;
const LOCAL_HEADER_SIZE = 30;
const DEFLATE = 8;
+/** Largest file the reader inflates, so a corrupt archive cannot exhaust the memory. */
+const MAXIMUM_FILE_SIZE = 64 * 1024 * 1024;
+
const ALPHABET_LENGTH = 26;
const CODE_BEFORE_A = 64;
@@ -20,25 +25,56 @@ const XML_ENTITIES: Record = {
"'": "'",
};
+const MAXIMUM_CODE_POINT = 0x10_ff_ff;
+
+/**
+ * Decodes the five entities of XML and the numeric character references a writer other than
+ * Excel emits, `
` and `é` alike. Anything else is left as it is.
+ * @param {string} text - The text as the XML carries it.
+ * @returns {string} The decoded text.
+ */
const decodeXml = (text: string): string =>
- text.replaceAll(/&(?:amp|lt|gt|quot|apos);/g, (entity) => XML_ENTITIES[entity] ?? entity);
+ text.replaceAll(/&(?:amp|lt|gt|quot|apos|#\d+|#x[\da-fA-F]+);/g, (entity) => {
+ if (!entity.startsWith("")) return XML_ENTITIES[entity] ?? entity;
+
+ const reference = entity.slice(2, -1);
+ const code = reference.startsWith("x")
+ ? Number.parseInt(reference.slice(1), 16)
+ : Number(reference);
+
+ return code <= MAXIMUM_CODE_POINT ? String.fromCodePoint(code) : entity;
+ });
+
+/** One file of a zip archive, still compressed. */
+type ZipEntry = {
+ /** The compression method of the entry, 8 for deflate and 0 for stored. */
+ method: number;
+ /** The bytes of the entry as the archive carries them. */
+ data: Buffer;
+};
/**
- * Reads the files of a zip archive out of its central directory, inflating the deflated ones.
+ * Reads the entries of a zip archive out of its central directory. ZIP64 is not supported, so
+ * an archive above 4 GB or with more than 65535 entries is out of reach, which no table the
+ * government publishes comes close to.
* @param {Buffer} archive - The whole archive.
- * @returns {Map} The UTF-8 content of every file, keyed by its path.
+ * @returns {Map} Every entry, keyed by its path, left compressed.
*/
-const unzip = (archive: Buffer): Map => {
+const unzip = (archive: Buffer): Map => {
let end = archive.length - 22;
while (end >= 0 && archive.readUInt32LE(end) !== END_OF_CENTRAL_DIRECTORY_SIGNATURE) end--;
if (end < 0) throw new Error("not a zip archive");
- const files = new Map();
+ const files = new Map();
let offset = archive.readUInt32LE(end + 16);
for (let index = archive.readUInt16LE(end + 10); index > 0; index--) {
+ if (archive.readUInt32LE(offset) !== CENTRAL_DIRECTORY_SIGNATURE) {
+ throw new Error("the zip central directory is broken");
+ }
+
const method = archive.readUInt16LE(offset + 10);
const size = archive.readUInt32LE(offset + 20);
const nameLength = archive.readUInt16LE(offset + 28);
@@ -51,9 +87,8 @@ const unzip = (archive: Buffer): Map => {
LOCAL_HEADER_SIZE +
archive.readUInt16LE(local + 26) +
archive.readUInt16LE(local + 28);
- const data = archive.subarray(start, start + size);
- files.set(name, (method === DEFLATE ? inflateRawSync(data) : data).toString("utf8"));
+ files.set(name, { method, data: archive.subarray(start, start + size) });
offset = nameStart + nameLength + skipped;
}
@@ -75,17 +110,82 @@ const columnIndex = (letters: string): number => {
return index - 1;
};
-const readFile = (files: Map, path: string): string => {
- const content = files.get(path);
+/**
+ * Inflates one file of the archive and reads it as UTF-8. Only the files a caller asks for are
+ * inflated, which leaves the themes, the styles and the custom XML of a workbook untouched.
+ * @param {Map} files - The entries of the archive.
+ * @param {string} path - The path of the file inside the archive.
+ * @returns {string} The content of the file.
+ */
+const readFile = (files: Map, path: string): string => {
+ const entry = files.get(path);
- if (content === undefined) throw new Error(`the workbook has no ${path}`);
+ if (entry === undefined) throw new Error(`the workbook has no ${path}`);
- return content;
+ return (
+ entry.method === DEFLATE
+ ? inflateRawSync(entry.data, { maxOutputLength: MAXIMUM_FILE_SIZE })
+ : entry.data
+ ).toString("utf8");
+};
+
+/**
+ * Joins the text runs of a shared string or of an inline string. A cell whose text is styled in
+ * pieces carries one `` per piece.
+ * @param {string} xml - The content of the `` or `` element.
+ * @returns {string} The text of the element.
+ */
+const joinRuns = (xml: string): string =>
+ [...xml.matchAll(/]*>([^<]*)<\/t>/g)].map((run) => decodeXml(run[1])).join("");
+
+/**
+ * Resolves the target of a workbook relationship into a path inside the archive. The OPC spec
+ * allows an absolute target (`/xl/worksheets/sheet1.xml`) and one that climbs out of the part
+ * folder (`../worksheets/sheet1.xml`) as well as the plain relative form.
+ * @param {string} target - The `Target` of the relationship.
+ * @returns {string} The path of the part inside the archive.
+ */
+const resolveTarget = (target: string): string =>
+ decodeURIComponent(new URL(target, "file:///xl/").pathname).slice(1);
+
+/**
+ * Reads the rows of a worksheet, placed by the `r` attribute of the row rather than by the
+ * order they are written in, so a skipped row leaves a gap instead of shifting the table.
+ * @param {string} sheet - The worksheet XML.
+ * @param {string[]} strings - The shared strings of the workbook.
+ * @returns {string[][]} The rows of the sheet.
+ */
+const readRows = (sheet: string, strings: string[]): string[][] => {
+ const rows: string[][] = [];
+
+ for (const row of sheet.matchAll(/]*?)(?:\/>|>(.*?)<\/row>)/gs)) {
+ const cells: string[] = [];
+
+ for (const cell of (row[2] ?? "").matchAll(/]*?)(?:\/>|>(.*?)<\/c>)/gs)) {
+ const reference = /\br="([A-Z]+)\d+"/.exec(cell[1])?.[1];
+ const type = /\bt="([^"]*)"/.exec(cell[1])?.[1];
+ const body = cell[2] ?? "";
+ const value = /([^<]*)<\/v>/.exec(body)?.[1] ?? "";
+
+ if (reference === undefined) continue;
+
+ if (type === "inlineStr") cells[columnIndex(reference)] = joinRuns(body);
+ else if (type === "s") cells[columnIndex(reference)] = strings[Number(value)] ?? "";
+ else cells[columnIndex(reference)] = decodeXml(value);
+ }
+
+ const index = Number(/\br="(\d+)"/.exec(row[1])?.[1] ?? rows.length + 1) - 1;
+
+ rows[index] = Array.from(cells, (cell) => cell ?? "");
+ }
+
+ return Array.from(rows, (row) => row ?? []);
};
/**
* Reads one sheet of an `.xlsx` workbook, enough of the format for the plain tables the
- * government publishes: shared strings (rich text runs joined), numbers and empty cells.
+ * government publishes: shared strings (rich text runs joined), inline strings, numbers and
+ * empty cells.
* @param {Buffer} workbook - The `.xlsx` file.
* @param {string} sheetName - The name of the sheet, as its tab shows it.
* @returns {string[][]} The rows of the sheet, every cell as text and an absent cell as `""`.
@@ -101,21 +201,20 @@ export const readXlsxSheet = (workbook: Buffer, sheetName: string): string[][] =
if (target === undefined) throw new Error(`the workbook has no sheet named ${sheetName}`);
- const strings = [...readFile(files, "xl/sharedStrings.xml").matchAll(/(.*?)<\/si>/gs)].map(
- (item) =>
- [...item[1].matchAll(/]*>([^<]*)<\/t>/g)].map((run) => decodeXml(run[1])).join(""),
+ const items = readFile(files, "xl/sharedStrings.xml").matchAll(
+ /]*?(?:\/>|>(.*?)<\/si>)/gs,
);
+ const strings = [...items].map((item) => joinRuns(item[1] ?? ""));
- return [...readFile(files, `xl/${target}`).matchAll(/]*>(.*?)<\/row>/gs)].map((row) => {
- const cells: string[] = [];
+ const content = readFile(files, resolveTarget(target));
+ const rows = readRows(content, strings);
+ const dimension = /]*?)(?:\/>|>(.*?)<\/c>)/gs)) {
- const column = columnIndex(cell[1]);
- const value = /([^<]*)<\/v>/.exec(cell[3] ?? "")?.[1] ?? "";
-
- cells[column] = cell[2].includes('t="s"') ? strings[Number(value)] : value;
- }
+ if (dimension !== undefined && rows.length !== Number(dimension)) {
+ throw new Error(
+ `the sheet ${sheetName} holds ${rows.length} rows, not the ${dimension} it declares`,
+ );
+ }
- return Array.from(cells, (cell) => cell ?? "");
- });
+ return rows;
};
diff --git a/scripts/serialize-record.ts b/scripts/serialize-record.ts
new file mode 100644
index 000000000..55fdb4049
--- /dev/null
+++ b/scripts/serialize-record.ts
@@ -0,0 +1,15 @@
+/**
+ * Serializes a record as an object literal whose entries are written in ascending key order.
+ * `JSON.stringify` follows the insertion order of the object, and JavaScript hoists the keys
+ * that are canonical array indices (`"1001"`) ahead of the others (`"0101"`), so a record built
+ * 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.
+ * @returns {string} The object literal, sorted by key.
+ */
+export const serializeRecord = (data: Record): string =>
+ `{${Object.keys(data)
+ .sort()
+ .map((key) => `${JSON.stringify(key)}:${JSON.stringify(data[key])}`)
+ .join(",")}}`;
diff --git a/scripts/service-items.ts b/scripts/service-items.ts
index cbc23f7fe..63650f482 100644
--- a/scripts/service-items.ts
+++ b/scripts/service-items.ts
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts";
import { readXlsxSheet } from "./read-xlsx-sheet.ts";
+import { serializeRecord } from "./serialize-record.ts";
const scriptsDir = import.meta.dirname;
@@ -81,11 +82,8 @@ const main = async (): Promise => {
const workbookResponse = await fetchOk(link, "NFS-e ANEXO B");
const workbook = await workbookResponse.arrayBuffer();
const data = parseSheet(readXlsxSheet(Buffer.from(workbook), SHEET_NAME));
- const sorted: Record = {};
- for (const key of Object.keys(data).sort()) sorted[key] = data[key];
-
- if (Object.keys(sorted).length < MINIMUM_SUBITEMS) {
+ if (Object.keys(data).length < MINIMUM_SUBITEMS) {
throw new Error(`the national service list holds fewer than ${MINIMUM_SUBITEMS} subitems`);
}
@@ -110,7 +108,7 @@ const main = async (): Promise => {
* @see Official: ${DOCUMENTATION_URL}
* Sistema Nacional NFS-e, current technical documentation, ANEXO B.
*/
-export const SERVICE_ITEM_DESCRIPTIONS: Record = ${JSON.stringify(sorted)};
+export const SERVICE_ITEM_DESCRIPTIONS: Record = ${serializeRecord(data)};
/**
* Shape a subitem has to be written in: one or two digits of the item and the two of the
diff --git a/src/_internals/constants/service-items.ts b/src/_internals/constants/service-items.ts
index b5563ee59..d4c0d861a 100644
--- a/src/_internals/constants/service-items.ts
+++ b/src/_internals/constants/service-items.ts
@@ -18,6 +18,112 @@
* Sistema Nacional NFS-e, current technical documentation, ANEXO B.
*/
export const SERVICE_ITEM_DESCRIPTIONS: Record = {
+ "0101": "Análise e desenvolvimento de sistemas.",
+ "0102": "Programação.",
+ "0103":
+ "Processamento, armazenamento ou hospedagem de dados, textos, imagens, vídeos, páginas eletrônicas, aplicativos e sistemas de informação, entre outros formatos, e congêneres.",
+ "0104":
+ "Elaboração de programas de computadores, inclusive de jogos eletrônicos, independentemente da arquitetura construtiva da máquina em que o programa será executado, incluindo tablets, smartphones e congêneres.",
+ "0105": "Licenciamento ou cessão de direito de uso de programas de computação.",
+ "0106": "Assessoria e consultoria em informática.",
+ "0107":
+ "Suporte técnico em informática, inclusive instalação, configuração e manutenção de programas de computação e bancos de dados.",
+ "0108": "Planejamento, confecção, manutenção e atualização de páginas eletrônicas.",
+ "0109":
+ "Disponibilização, sem cessão definitiva, de conteúdos de áudio, vídeo, imagem e texto por meio da internet, respeitada a imunidade de livros, jornais e periódicos (exceto a distribuição de conteúdos pelas prestadoras de Serviço de Acesso Condicionado, de que trata a Lei nº 12.485, de 12 de setembro de 2011, sujeita ao ICMS).",
+ "0201": "Serviços de pesquisas e desenvolvimento de qualquer natureza.",
+ "0302": "Cessão de direito de uso de marcas e de sinais de propaganda.",
+ "0303":
+ "Exploração de salões de festas, centro de convenções, escritórios virtuais, stands, quadras esportivas, estádios, ginásios, auditórios, casas de espetáculos, parques de diversões, canchas e congêneres, para realização de eventos ou negócios de qualquer natureza.",
+ "0304":
+ "Locação, sublocação, arrendamento, direito de passagem ou permissão de uso, compartilhado ou não, de ferrovia, rodovia, postes, cabos, dutos e condutos de qualquer natureza.",
+ "0305": "Cessão de andaimes, palcos, coberturas e outras estruturas de uso temporário.",
+ "0401": "Medicina e biomedicina.",
+ "0402":
+ "Análises clínicas, patologia, eletricidade médica, radioterapia, quimioterapia, ultra-sonografia, ressonância magnética, radiologia, tomografia e congêneres.",
+ "0403":
+ "Hospitais, clínicas, laboratórios, sanatórios, manicômios, casas de saúde, prontos-socorros, ambulatórios e congêneres.",
+ "0404": "Instrumentação cirúrgica.",
+ "0405": "Acupuntura.",
+ "0406": "Enfermagem, inclusive serviços auxiliares.",
+ "0407": "Serviços farmacêuticos.",
+ "0408": "Terapia ocupacional, fisioterapia e fonoaudiologia.",
+ "0409": "Terapias de qualquer espécie destinadas ao tratamento físico, orgânico e mental.",
+ "0410": "Nutrição.",
+ "0411": "Obstetrícia.",
+ "0412": "Odontologia.",
+ "0413": "Ortóptica.",
+ "0414": "Próteses sob encomenda.",
+ "0415": "Psicanálise.",
+ "0416": "Psicologia.",
+ "0417": "Casas de repouso e de recuperação, creches, asilos e congêneres.",
+ "0418": "Inseminação artificial, fertilização in vitro e congêneres.",
+ "0419": "Bancos de sangue, leite, pele, olhos, óvulos, sêmen e congêneres.",
+ "0420":
+ "Coleta de sangue, leite, tecidos, sêmen, órgãos e materiais biológicos de qualquer espécie.",
+ "0421": "Unidade de atendimento, assistência ou tratamento móvel e congêneres.",
+ "0422":
+ "Planos de medicina de grupo ou individual e convênios para prestação de assistência médica, hospitalar, odontológica e congêneres.",
+ "0423":
+ "Outros planos de saúde que se cumpram através de serviços de terceiros contratados, credenciados, cooperados ou apenas pagos pelo operador do plano mediante indicação do beneficiário.",
+ "0501": "Medicina veterinária e zootecnia.",
+ "0502": "Hospitais, clínicas, ambulatórios, prontos-socorros e congêneres, na área veterinária.",
+ "0503": "Laboratórios de análise na área veterinária.",
+ "0504": "Inseminação artificial, fertilização in vitro e congêneres.",
+ "0505": "Bancos de sangue e de órgãos e congêneres.",
+ "0506":
+ "Coleta de sangue, leite, tecidos, sêmen, órgãos e materiais biológicos de qualquer espécie.",
+ "0507": "Unidade de atendimento, assistência ou tratamento móvel e congêneres.",
+ "0508": "Guarda, tratamento, amestramento, embelezamento, alojamento e congêneres.",
+ "0509": "Planos de atendimento e assistência médico-veterinária.",
+ "0601": "Barbearia, cabeleireiros, manicuros, pedicuros e congêneres.",
+ "0602": "Esteticistas, tratamento de pele, depilação e congêneres.",
+ "0603": "Banhos, duchas, sauna, massagens e congêneres.",
+ "0604": "Ginástica, dança, esportes, natação, artes marciais e demais atividades físicas.",
+ "0605": "Centros de emagrecimento, spa e congêneres.",
+ "0606": "Aplicação de tatuagens, piercings e congêneres.",
+ "0701":
+ "Engenharia, agronomia, agrimensura, arquitetura, geologia, urbanismo, paisagismo e congêneres.",
+ "0702":
+ "Execução, por administração, empreitada ou subempreitada, de obras de construção civil, hidráulica ou elétrica e de outras obras semelhantes, inclusive sondagem, perfuração de poços, escavação, drenagem e irrigação, terraplanagem, pavimentação, concretagem e a instalação e montagem de produtos, peças e equipamentos (exceto o fornecimento de mercadorias produzidas pelo prestador de serviços fora do local da prestação dos serviços, que fica sujeito ao ICMS).",
+ "0703":
+ "Elaboração de planos diretores, estudos de viabilidade, estudos organizacionais e outros, relacionados com obras e serviços de engenharia; elaboração de anteprojetos, projetos básicos e projetos executivos para trabalhos de engenharia.",
+ "0704": "Demolição.",
+ "0705":
+ "Reparação, conservação e reforma de edifícios, estradas, pontes, portos e congêneres (exceto o fornecimento de mercadorias produzidas pelo prestador dos serviços, fora do local da prestação dos serviços, que fica sujeito ao ICMS).",
+ "0706":
+ "Colocação e instalação de tapetes, carpetes, assoalhos, cortinas, revestimentos de parede, vidros, divisórias, placas de gesso e congêneres, com material fornecido pelo tomador do serviço.",
+ "0707": "Recuperação, raspagem, polimento e lustração de pisos e congêneres.",
+ "0708": "Calafetação.",
+ "0709":
+ "Varrição, coleta, remoção, incineração, tratamento, reciclagem, separação e destinação final de lixo, rejeitos e outros resíduos quaisquer.",
+ "0710":
+ "Limpeza, manutenção e conservação de vias e logradouros públicos, imóveis, chaminés, piscinas, parques, jardins e congêneres.",
+ "0711": "Decoração e jardinagem, inclusive corte e poda de árvores.",
+ "0712":
+ "Controle e tratamento de efluentes de qualquer natureza e de agentes físicos, químicos e biológicos.",
+ "0713":
+ "Dedetização, desinfecção, desinsetização, imunização, higienização, desratização, pulverização e congêneres.",
+ "0716":
+ "Florestamento, reflorestamento, semeadura, adubação, reparação de solo, plantio, silagem, colheita, corte e descascamento de árvores, silvicultura, exploração florestal e dos serviços congêneres indissociáveis da formação, manutenção e colheita de florestas, para quaisquer fins e por quaisquer meios.",
+ "0717": "Escoramento, contenção de encostas e serviços congêneres.",
+ "0718":
+ "Limpeza e dragagem de rios, portos, canais, baías, lagos, lagoas, represas, açudes e congêneres.",
+ "0719":
+ "Acompanhamento e fiscalização da execução de obras de engenharia, arquitetura e urbanismo.",
+ "0720":
+ "Aerofotogrametria (inclusive interpretação), cartografia, mapeamento, levantamentos topográficos, batimétricos, geográficos, geodésicos, geológicos, geofísicos e congêneres.",
+ "0721":
+ "Pesquisa, perfuração, cimentação, mergulho, perfilagem, concretação, testemunhagem, pescaria, estimulação e outros serviços relacionados com a exploração e explotação de petróleo, gás natural e de outros recursos minerais.",
+ "0722": "Nucleação e bombardeamento de nuvens e congêneres.",
+ "0801": "Ensino regular pré-escolar, fundamental, médio e superior.",
+ "0802":
+ "Instrução, treinamento, orientação pedagógica e educacional, avaliação de conhecimentos de qualquer natureza.",
+ "0901":
+ "Hospedagem de qualquer natureza em hotéis, apart-service condominiais, flat, apart-hotéis, hotéis residência, residence-service, suite service, hotelaria marítima, motéis, pensões e congêneres; ocupação por temporada com fornecimento de serviço (o valor da alimentação e gorjeta, quando incluído no preço da diária, fica sujeito ao Imposto Sobre Serviços).",
+ "0902":
+ "Agenciamento, organização, promoção, intermediação e execução de programas de turismo, passeios, viagens, excursões, hospedagens e congêneres.",
+ "0903": "Guias de turismo.",
"1001":
"Agenciamento, corretagem ou intermediação de câmbio, de seguros, de cartões de crédito, de planos de saúde e de planos de previdência privada.",
"1002":
@@ -200,112 +306,6 @@ export const SERVICE_ITEM_DESCRIPTIONS: Record = {
"3901":
"Serviços de ourivesaria e lapidação (quando o material for fornecido pelo tomador do serviço).",
"4001": "Obras de arte sob encomenda.",
- "0101": "Análise e desenvolvimento de sistemas.",
- "0102": "Programação.",
- "0103":
- "Processamento, armazenamento ou hospedagem de dados, textos, imagens, vídeos, páginas eletrônicas, aplicativos e sistemas de informação, entre outros formatos, e congêneres.",
- "0104":
- "Elaboração de programas de computadores, inclusive de jogos eletrônicos, independentemente da arquitetura construtiva da máquina em que o programa será executado, incluindo tablets, smartphones e congêneres.",
- "0105": "Licenciamento ou cessão de direito de uso de programas de computação.",
- "0106": "Assessoria e consultoria em informática.",
- "0107":
- "Suporte técnico em informática, inclusive instalação, configuração e manutenção de programas de computação e bancos de dados.",
- "0108": "Planejamento, confecção, manutenção e atualização de páginas eletrônicas.",
- "0109":
- "Disponibilização, sem cessão definitiva, de conteúdos de áudio, vídeo, imagem e texto por meio da internet, respeitada a imunidade de livros, jornais e periódicos (exceto a distribuição de conteúdos pelas prestadoras de Serviço de Acesso Condicionado, de que trata a Lei nº 12.485, de 12 de setembro de 2011, sujeita ao ICMS).",
- "0201": "Serviços de pesquisas e desenvolvimento de qualquer natureza.",
- "0302": "Cessão de direito de uso de marcas e de sinais de propaganda.",
- "0303":
- "Exploração de salões de festas, centro de convenções, escritórios virtuais, stands, quadras esportivas, estádios, ginásios, auditórios, casas de espetáculos, parques de diversões, canchas e congêneres, para realização de eventos ou negócios de qualquer natureza.",
- "0304":
- "Locação, sublocação, arrendamento, direito de passagem ou permissão de uso, compartilhado ou não, de ferrovia, rodovia, postes, cabos, dutos e condutos de qualquer natureza.",
- "0305": "Cessão de andaimes, palcos, coberturas e outras estruturas de uso temporário.",
- "0401": "Medicina e biomedicina.",
- "0402":
- "Análises clínicas, patologia, eletricidade médica, radioterapia, quimioterapia, ultra-sonografia, ressonância magnética, radiologia, tomografia e congêneres.",
- "0403":
- "Hospitais, clínicas, laboratórios, sanatórios, manicômios, casas de saúde, prontos-socorros, ambulatórios e congêneres.",
- "0404": "Instrumentação cirúrgica.",
- "0405": "Acupuntura.",
- "0406": "Enfermagem, inclusive serviços auxiliares.",
- "0407": "Serviços farmacêuticos.",
- "0408": "Terapia ocupacional, fisioterapia e fonoaudiologia.",
- "0409": "Terapias de qualquer espécie destinadas ao tratamento físico, orgânico e mental.",
- "0410": "Nutrição.",
- "0411": "Obstetrícia.",
- "0412": "Odontologia.",
- "0413": "Ortóptica.",
- "0414": "Próteses sob encomenda.",
- "0415": "Psicanálise.",
- "0416": "Psicologia.",
- "0417": "Casas de repouso e de recuperação, creches, asilos e congêneres.",
- "0418": "Inseminação artificial, fertilização in vitro e congêneres.",
- "0419": "Bancos de sangue, leite, pele, olhos, óvulos, sêmen e congêneres.",
- "0420":
- "Coleta de sangue, leite, tecidos, sêmen, órgãos e materiais biológicos de qualquer espécie.",
- "0421": "Unidade de atendimento, assistência ou tratamento móvel e congêneres.",
- "0422":
- "Planos de medicina de grupo ou individual e convênios para prestação de assistência médica, hospitalar, odontológica e congêneres.",
- "0423":
- "Outros planos de saúde que se cumpram através de serviços de terceiros contratados, credenciados, cooperados ou apenas pagos pelo operador do plano mediante indicação do beneficiário.",
- "0501": "Medicina veterinária e zootecnia.",
- "0502": "Hospitais, clínicas, ambulatórios, prontos-socorros e congêneres, na área veterinária.",
- "0503": "Laboratórios de análise na área veterinária.",
- "0504": "Inseminação artificial, fertilização in vitro e congêneres.",
- "0505": "Bancos de sangue e de órgãos e congêneres.",
- "0506":
- "Coleta de sangue, leite, tecidos, sêmen, órgãos e materiais biológicos de qualquer espécie.",
- "0507": "Unidade de atendimento, assistência ou tratamento móvel e congêneres.",
- "0508": "Guarda, tratamento, amestramento, embelezamento, alojamento e congêneres.",
- "0509": "Planos de atendimento e assistência médico-veterinária.",
- "0601": "Barbearia, cabeleireiros, manicuros, pedicuros e congêneres.",
- "0602": "Esteticistas, tratamento de pele, depilação e congêneres.",
- "0603": "Banhos, duchas, sauna, massagens e congêneres.",
- "0604": "Ginástica, dança, esportes, natação, artes marciais e demais atividades físicas.",
- "0605": "Centros de emagrecimento, spa e congêneres.",
- "0606": "Aplicação de tatuagens, piercings e congêneres.",
- "0701":
- "Engenharia, agronomia, agrimensura, arquitetura, geologia, urbanismo, paisagismo e congêneres.",
- "0702":
- "Execução, por administração, empreitada ou subempreitada, de obras de construção civil, hidráulica ou elétrica e de outras obras semelhantes, inclusive sondagem, perfuração de poços, escavação, drenagem e irrigação, terraplanagem, pavimentação, concretagem e a instalação e montagem de produtos, peças e equipamentos (exceto o fornecimento de mercadorias produzidas pelo prestador de serviços fora do local da prestação dos serviços, que fica sujeito ao ICMS).",
- "0703":
- "Elaboração de planos diretores, estudos de viabilidade, estudos organizacionais e outros, relacionados com obras e serviços de engenharia; elaboração de anteprojetos, projetos básicos e projetos executivos para trabalhos de engenharia.",
- "0704": "Demolição.",
- "0705":
- "Reparação, conservação e reforma de edifícios, estradas, pontes, portos e congêneres (exceto o fornecimento de mercadorias produzidas pelo prestador dos serviços, fora do local da prestação dos serviços, que fica sujeito ao ICMS).",
- "0706":
- "Colocação e instalação de tapetes, carpetes, assoalhos, cortinas, revestimentos de parede, vidros, divisórias, placas de gesso e congêneres, com material fornecido pelo tomador do serviço.",
- "0707": "Recuperação, raspagem, polimento e lustração de pisos e congêneres.",
- "0708": "Calafetação.",
- "0709":
- "Varrição, coleta, remoção, incineração, tratamento, reciclagem, separação e destinação final de lixo, rejeitos e outros resíduos quaisquer.",
- "0710":
- "Limpeza, manutenção e conservação de vias e logradouros públicos, imóveis, chaminés, piscinas, parques, jardins e congêneres.",
- "0711": "Decoração e jardinagem, inclusive corte e poda de árvores.",
- "0712":
- "Controle e tratamento de efluentes de qualquer natureza e de agentes físicos, químicos e biológicos.",
- "0713":
- "Dedetização, desinfecção, desinsetização, imunização, higienização, desratização, pulverização e congêneres.",
- "0716":
- "Florestamento, reflorestamento, semeadura, adubação, reparação de solo, plantio, silagem, colheita, corte e descascamento de árvores, silvicultura, exploração florestal e dos serviços congêneres indissociáveis da formação, manutenção e colheita de florestas, para quaisquer fins e por quaisquer meios.",
- "0717": "Escoramento, contenção de encostas e serviços congêneres.",
- "0718":
- "Limpeza e dragagem de rios, portos, canais, baías, lagos, lagoas, represas, açudes e congêneres.",
- "0719":
- "Acompanhamento e fiscalização da execução de obras de engenharia, arquitetura e urbanismo.",
- "0720":
- "Aerofotogrametria (inclusive interpretação), cartografia, mapeamento, levantamentos topográficos, batimétricos, geográficos, geodésicos, geológicos, geofísicos e congêneres.",
- "0721":
- "Pesquisa, perfuração, cimentação, mergulho, perfilagem, concretação, testemunhagem, pescaria, estimulação e outros serviços relacionados com a exploração e explotação de petróleo, gás natural e de outros recursos minerais.",
- "0722": "Nucleação e bombardeamento de nuvens e congêneres.",
- "0801": "Ensino regular pré-escolar, fundamental, médio e superior.",
- "0802":
- "Instrução, treinamento, orientação pedagógica e educacional, avaliação de conhecimentos de qualquer natureza.",
- "0901":
- "Hospedagem de qualquer natureza em hotéis, apart-service condominiais, flat, apart-hotéis, hotéis residência, residence-service, suite service, hotelaria marítima, motéis, pensões e congêneres; ocupação por temporada com fornecimento de serviço (o valor da alimentação e gorjeta, quando incluído no preço da diária, fica sujeito ao Imposto Sobre Serviços).",
- "0902":
- "Agenciamento, organização, promoção, intermediação e execução de programas de turismo, passeios, viagens, excursões, hospedagens e congêneres.",
- "0903": "Guias de turismo.",
};
/**
From deec023fc1cfcc7968c050436fb6404726a1133c Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Sat, 19 Sep 2026 11:46:34 -0300
Subject: [PATCH 04/12] docs(nfse): say why a service subitem only takes the
dot
Every other lookup of a masked code accepts the usual mask characters, so the
docs state that as a general rule. A service subitem does not: the law only
ever prints the dot between the item and the subitem, the way a CSOSN has no
printed grouping at all, and the asymmetry was neither explained nor called
out. The JSDoc and both docs pages now say so, and a test pins the subitem
17.14, the one description the official sheet prints without a final period.
context7.json also missed the four new dataset backed utils in the rule that
tells an agent to lazy load them by subpath.
---
context7.json | 2 +-
docs/pt-br/utilities.md | 1 +
docs/utilities.md | 1 +
src/get-service-item/get-service-item.test.ts | 4 ++++
src/get-service-item/get-service-item.ts | 7 +++++--
src/is-valid-service-item/is-valid-service-item.ts | 5 +++--
6 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/context7.json b/context7.json
index 919ebd833..e92877c25 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): each one embeds a large official table.",
+ "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.",
"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/pt-br/utilities.md b/docs/pt-br/utilities.md
index 7ac470f91..fa0cb8770 100644
--- a/docs/pt-br/utilities.md
+++ b/docs/pt-br/utilities.md
@@ -2852,6 +2852,7 @@ Verifica se um valor é um subitem em vigor da lista de serviços anexa à Lei C
- A lei numera o subitem como o item, um ponto e dois dígitos, de `1.01` a `40.01`.
- Aceita essa forma, o item preenchido com zero (`'01.01'`) ou os dígitos puros (`'0101'`, `'101'` ou o inteiro `101`), que são os quatro primeiros dígitos do código `cTribNac` da NFS-e nacional, com espaços opcionais nas extremidades.
+- O ponto é o único separador que a lei imprime entre o item e o subitem, então, ao contrário dos códigos com máscara de agrupamento impressa (`isValidCfop`, `isValidNbs`), nada mais é aceito no lugar dele e `'1-01'` é rejeitado.
- Um número só é lido quando é um inteiro seguro não negativo, então o decimal `1.01` é rejeitado: escreva a forma com ponto como string.
- Os subitens vetados (`3.01`, `7.14`, `7.15`, `13.01` e `17.07`), os títulos de item, os códigos nacionais de 6 dígitos em que um subitem se desdobra e o item 99 da lista nacional, que não faz parte da lei, não são válidos. Códigos municipais de serviço estão fora do escopo.
diff --git a/docs/utilities.md b/docs/utilities.md
index 68fcb0884..6b391747a 100644
--- a/docs/utilities.md
+++ b/docs/utilities.md
@@ -2852,6 +2852,7 @@ Check if a value is a subitem in force of the service list annexed to the Lei Co
- The law numbers a subitem as the item, a dot and two digits, `1.01` to `40.01`.
- Accepts that form, a zero padded item (`'01.01'`) or the bare digits (`'0101'`, `'101'` or the integer `101`), which are the first four digits of the `cTribNac` code of the national NFS-e, with optional surrounding whitespace.
+- The dot is the only separator the law ever prints between the item and the subitem, so, unlike the codes with a printed grouping mask (`isValidCfop`, `isValidNbs`), nothing else is accepted in its place and `'1-01'` is rejected.
- A number is only read when it is a non-negative safe integer, so the float `1.01` is rejected: write the dotted form as a string.
- The vetoed subitems (`3.01`, `7.14`, `7.15`, `13.01` and `17.07`), the item headings, the 6 digit national codes a subitem is split into and item 99 of the national list, which is not part of the law, are not valid. Municipal service codes are out of scope.
diff --git a/src/get-service-item/get-service-item.test.ts b/src/get-service-item/get-service-item.test.ts
index a9dc459d0..e7328effb 100644
--- a/src/get-service-item/get-service-item.test.ts
+++ b/src/get-service-item/get-service-item.test.ts
@@ -45,6 +45,10 @@ describe("getServiceItem", () => {
expect(getServiceItem(" 1.01\n")).toEqual(SYSTEMS);
});
+ it("should return the subitem 17.14 as the official sheet prints it, without a final period", () => {
+ expect(getServiceItem("17.14")).toEqual({ code: "17.14", description: "Advocacia" });
+ });
+
it("should return a fresh object that does not leak the internal table", () => {
expect(getServiceItem("1.01")).not.toBe(getServiceItem("1.01"));
});
diff --git a/src/get-service-item/get-service-item.ts b/src/get-service-item/get-service-item.ts
index 13efdbc12..6a3d86ed7 100644
--- a/src/get-service-item/get-service-item.ts
+++ b/src/get-service-item/get-service-item.ts
@@ -24,8 +24,11 @@ export type ServiceItem = {
* The law numbers a subitem as the item, a dot and two digits, `1.01` to `40.01`. It is accepted
* in that form, with a zero padded item (`"01.01"`) or as the bare digits (`"0101"`, `"101"`),
* which are the first four digits of the `cTribNac` code of the national NFS-e, and optional
- * surrounding whitespace. A number is only read when it is a non-negative safe integer, so
- * `101` is `1.01` while the float `1.01` gives `null`: write the dotted form as a string.
+ * surrounding whitespace. The dot is the only separator the law ever prints between the item and
+ * the subitem, so unlike the codes with a printed grouping mask (`getCfop`, `getNbs`) nothing
+ * else is accepted in its place and `"1-01"` gives `null`. A number is only read when it is a
+ * non-negative safe integer, so `101` is `1.01` while the float `1.01` gives `null`: write the
+ * dotted form as a string.
*
* Only the subitems in force are in the table. The vetoed ones (`3.01`, `7.14`, `7.15`, `13.01`
* and `17.07`) give `null`, and so do the item headings (`"1"`), the national codes a subitem is
diff --git a/src/is-valid-service-item/is-valid-service-item.ts b/src/is-valid-service-item/is-valid-service-item.ts
index 689148280..6b6c8917e 100644
--- a/src/is-valid-service-item/is-valid-service-item.ts
+++ b/src/is-valid-service-item/is-valid-service-item.ts
@@ -6,8 +6,9 @@ import { getServiceItem } from "../get-service-item/get-service-item";
*
* Accepts what `getServiceItem` accepts: the form the law prints (`"1.01"`), a zero padded item
* (`"01.01"`), the bare digits (`"0101"`, `"101"` or the integer `101`), with optional
- * surrounding whitespace. Vetoed subitems, item headings, the 6 digit national codes of the
- * NFS-e and item 99 of the national list are not valid.
+ * surrounding whitespace. The dot is the only separator the law ever prints, so `"1-01"` is not
+ * valid. Vetoed subitems, item headings, the 6 digit national codes of the NFS-e and item 99 of
+ * the national list are not valid.
*
* @param {string|number} value - The subitem to be validated.
* @returns {boolean} True if the subitem is in the list in force, false otherwise.
From f41cdc4c4a5215fc8cb57f8c98634fb61638fe07 Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Sat, 19 Sep 2026 12:17:26 -0300
Subject: [PATCH 05/12] refactor(scripts): bound the coordinates the xlsx
reader accepts
A worksheet holds at most 1048576 rows and 16384 columns, so a reference past
either is a corrupt sheet, not one to read. The reader rejected it only after
placing the cell, by which point the row array had already grown to that
index. It now fails on the reference itself.
A workbook whose cells are all inline strings carries no shared string part,
which is valid, so the reader no longer insists on one.
---
scripts/read-xlsx-sheet.ts | 68 +++++++++++++++++++++++++++-----------
1 file changed, 49 insertions(+), 19 deletions(-)
diff --git a/scripts/read-xlsx-sheet.ts b/scripts/read-xlsx-sheet.ts
index 01db599f4..5d3cf15f0 100644
--- a/scripts/read-xlsx-sheet.ts
+++ b/scripts/read-xlsx-sheet.ts
@@ -13,6 +13,18 @@ const DEFLATE = 8;
/** Largest file the reader inflates, so a corrupt archive cannot exhaust the memory. */
const MAXIMUM_FILE_SIZE = 64 * 1024 * 1024;
+/**
+ * The part that holds the text of the cells. A workbook whose cells are all inline strings does
+ * not carry it, which is valid, so it is read only when the archive has it.
+ */
+const SHARED_STRINGS = "xl/sharedStrings.xml";
+
+/** Rows a worksheet can hold, `1048576`. A reference past it is not a sheet this reader can read. */
+const MAXIMUM_ROWS = 1_048_576;
+
+/** Columns a worksheet can hold, `A` to `XFD`. */
+const MAXIMUM_COLUMNS = 16_384;
+
const ALPHABET_LENGTH = 26;
const CODE_BEFORE_A = 64;
@@ -148,6 +160,38 @@ const joinRuns = (xml: string): string =>
const resolveTarget = (target: string): string =>
decodeURIComponent(new URL(target, "file:///xl/").pathname).slice(1);
+/**
+ * Reads the cells of one row, placed by the column of their reference. A cell past the last
+ * column a worksheet can hold fails the run, so a corrupt reference cannot make the row array
+ * grow to its index.
+ * @param {string} row - The content of the `` element.
+ * @param {string[]} strings - The shared strings of the workbook.
+ * @returns {string[]} The cells of the row, an absent one as `""`.
+ */
+const readCells = (row: string, strings: string[]): string[] => {
+ const cells: string[] = [];
+
+ for (const cell of row.matchAll(/]*?)(?:\/>|>(.*?)<\/c>)/gs)) {
+ const reference = /\br="([A-Z]+)\d+"/.exec(cell[1])?.[1];
+
+ if (reference === undefined) continue;
+
+ const column = columnIndex(reference);
+
+ if (column >= MAXIMUM_COLUMNS) throw new Error(`the column ${reference} is past the sheet`);
+
+ const type = /\bt="([^"]*)"/.exec(cell[1])?.[1];
+ const body = cell[2] ?? "";
+ const value = /([^<]*)<\/v>/.exec(body)?.[1] ?? "";
+
+ if (type === "inlineStr") cells[column] = joinRuns(body);
+ else if (type === "s") cells[column] = strings[Number(value)] ?? "";
+ else cells[column] = decodeXml(value);
+ }
+
+ return Array.from(cells, (cell) => cell ?? "");
+};
+
/**
* Reads the rows of a worksheet, placed by the `r` attribute of the row rather than by the
* order they are written in, so a skipped row leaves a gap instead of shifting the table.
@@ -159,24 +203,11 @@ const readRows = (sheet: string, strings: string[]): string[][] => {
const rows: string[][] = [];
for (const row of sheet.matchAll(/]*?)(?:\/>|>(.*?)<\/row>)/gs)) {
- const cells: string[] = [];
-
- for (const cell of (row[2] ?? "").matchAll(/]*?)(?:\/>|>(.*?)<\/c>)/gs)) {
- const reference = /\br="([A-Z]+)\d+"/.exec(cell[1])?.[1];
- const type = /\bt="([^"]*)"/.exec(cell[1])?.[1];
- const body = cell[2] ?? "";
- const value = /([^<]*)<\/v>/.exec(body)?.[1] ?? "";
-
- if (reference === undefined) continue;
-
- if (type === "inlineStr") cells[columnIndex(reference)] = joinRuns(body);
- else if (type === "s") cells[columnIndex(reference)] = strings[Number(value)] ?? "";
- else cells[columnIndex(reference)] = decodeXml(value);
- }
-
const index = Number(/\br="(\d+)"/.exec(row[1])?.[1] ?? rows.length + 1) - 1;
- rows[index] = Array.from(cells, (cell) => cell ?? "");
+ if (index >= MAXIMUM_ROWS) throw new Error(`the row ${index + 1} is past the sheet`);
+
+ rows[index] = readCells(row[2] ?? "", strings);
}
return Array.from(rows, (row) => row ?? []);
@@ -201,9 +232,8 @@ export const readXlsxSheet = (workbook: Buffer, sheetName: string): string[][] =
if (target === undefined) throw new Error(`the workbook has no sheet named ${sheetName}`);
- const items = readFile(files, "xl/sharedStrings.xml").matchAll(
- /]*?(?:\/>|>(.*?)<\/si>)/gs,
- );
+ const shared = files.has(SHARED_STRINGS) ? readFile(files, SHARED_STRINGS) : "";
+ const items = shared.matchAll(/]*?(?:\/>|>(.*?)<\/si>)/gs);
const strings = [...items].map((item) => joinRuns(item[1] ?? ""));
const content = readFile(files, resolveTarget(target));
From eadaed8d2dbf5e737eb300ed50112a1fe0d2ba25 Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Sat, 19 Sep 2026 14:35:08 -0300
Subject: [PATCH 06/12] refactor(scripts): make the xlsx reader the one every
generator shares
#566 (`scripts/ibs-cbs.ts`) carries a second hand-written zip and xlsx reader, and jscpd, which
runs over `scripts/` with a threshold of 0, fails once both are on main. This reader becomes
the shared one, with what the other copy did that this one did not:
- `readXlsxSheets` reads every sheet of a workbook, by name, for a workbook whose sheet names
carry the date of the version (the cClassTrib table names its sheets "CST 2026-06-01 Pub"
and "cClass 2026-06-01 Pub"); `readXlsxSheet` keeps reading one sheet by its name
- the files read out of one workbook are bounded together as well as one by one, since reading
every sheet inflates as many parts as the central directory lists
- every central directory entry has to point at a local file header signature
- the `` phonetic readings of a shared string are dropped before its runs are joined
- `decodeXml` moves into its own module so a generator that reads the HTML listing of a
government portal can decode it the same way
Both sheets of the ANEXO B workbook come out identical to the previous reader, cell for cell,
and so do both sheets of the cClassTrib workbook.
---
scripts/decode-xml.ts | 29 ++++++
scripts/read-xlsx-sheet.ts | 190 ++++++++++++++++++++++++++-----------
2 files changed, 161 insertions(+), 58 deletions(-)
create mode 100644 scripts/decode-xml.ts
diff --git a/scripts/decode-xml.ts b/scripts/decode-xml.ts
new file mode 100644
index 000000000..46febeff7
--- /dev/null
+++ b/scripts/decode-xml.ts
@@ -0,0 +1,29 @@
+const XML_ENTITIES: Record = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ """: '"',
+ "'": "'",
+};
+
+const MAXIMUM_CODE_POINT = 0x10_ff_ff;
+
+/**
+ * Decodes the five entities of XML and the numeric character references a writer other than
+ * Excel emits, `
` and `é` alike. Anything else is left as it is. Shared by the xlsx
+ * reader and the generators that read a listing page of a government portal.
+ *
+ * @param {string} text - The text as the XML carries it.
+ * @returns {string} The decoded text.
+ */
+export const decodeXml = (text: string): string =>
+ text.replaceAll(/&(?:amp|lt|gt|quot|apos|#\d+|#x[\da-fA-F]+);/g, (entity) => {
+ if (!entity.startsWith("")) return XML_ENTITIES[entity] ?? entity;
+
+ const reference = entity.slice(2, -1);
+ const code = reference.startsWith("x")
+ ? Number.parseInt(reference.slice(1), 16)
+ : Number(reference);
+
+ return code <= MAXIMUM_CODE_POINT ? String.fromCodePoint(code) : entity;
+ });
diff --git a/scripts/read-xlsx-sheet.ts b/scripts/read-xlsx-sheet.ts
index 5d3cf15f0..baef265ca 100644
--- a/scripts/read-xlsx-sheet.ts
+++ b/scripts/read-xlsx-sheet.ts
@@ -1,9 +1,13 @@
import { inflateRawSync } from "node:zlib";
+import { decodeXml } from "./decode-xml.ts";
+
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06_05_4b_50;
const CENTRAL_DIRECTORY_SIGNATURE = 0x02_01_4b_50;
+const LOCAL_HEADER_SIGNATURE = 0x04_03_4b_50;
+
const CENTRAL_DIRECTORY_HEADER_SIZE = 46;
const LOCAL_HEADER_SIZE = 30;
@@ -13,6 +17,12 @@ const DEFLATE = 8;
/** Largest file the reader inflates, so a corrupt archive cannot exhaust the memory. */
const MAXIMUM_FILE_SIZE = 64 * 1024 * 1024;
+/**
+ * Largest amount the files read out of one workbook may hold together. A central directory can
+ * list thousands of sheets, so bounding each file alone does not bound the whole read.
+ */
+const MAXIMUM_WORKBOOK_SIZE = 128 * 1024 * 1024;
+
/**
* The part that holds the text of the cells. A workbook whose cells are all inline strings does
* not carry it, which is valid, so it is read only when the archive has it.
@@ -29,34 +39,6 @@ const ALPHABET_LENGTH = 26;
const CODE_BEFORE_A = 64;
-const XML_ENTITIES: Record = {
- "&": "&",
- "<": "<",
- ">": ">",
- """: '"',
- "'": "'",
-};
-
-const MAXIMUM_CODE_POINT = 0x10_ff_ff;
-
-/**
- * Decodes the five entities of XML and the numeric character references a writer other than
- * Excel emits, `
` and `é` alike. Anything else is left as it is.
- * @param {string} text - The text as the XML carries it.
- * @returns {string} The decoded text.
- */
-const decodeXml = (text: string): string =>
- text.replaceAll(/&(?:amp|lt|gt|quot|apos|#\d+|#x[\da-fA-F]+);/g, (entity) => {
- if (!entity.startsWith("")) return XML_ENTITIES[entity] ?? entity;
-
- const reference = entity.slice(2, -1);
- const code = reference.startsWith("x")
- ? Number.parseInt(reference.slice(1), 16)
- : Number(reference);
-
- return code <= MAXIMUM_CODE_POINT ? String.fromCodePoint(code) : entity;
- });
-
/** One file of a zip archive, still compressed. */
type ZipEntry = {
/** The compression method of the entry, 8 for deflate and 0 for stored. */
@@ -65,14 +47,40 @@ type ZipEntry = {
data: Buffer;
};
+/** The entries of an archive and how much has been inflated out of it so far. */
+type Archive = {
+ /** Every entry, keyed by its path, left compressed. */
+ files: Map;
+ /** Bytes read out of the archive so far, checked against `MAXIMUM_WORKBOOK_SIZE`. */
+ read: number;
+};
+
+/** One sheet of a workbook, as `xl/workbook.xml` lists it. */
+type SheetEntry = {
+ /** The name of the sheet, as its tab shows it. */
+ name: string;
+ /** The path of the worksheet part inside the archive. */
+ path: string;
+};
+
+/** A workbook opened far enough to read any of its sheets. */
+type Workbook = {
+ /** The archive of the workbook. */
+ archive: Archive;
+ /** The shared strings of the workbook. */
+ strings: string[];
+ /** The sheets, in the order of their tabs. */
+ sheets: SheetEntry[];
+};
+
/**
* Reads the entries of a zip archive out of its central directory. ZIP64 is not supported, so
* an archive above 4 GB or with more than 65535 entries is out of reach, which no table the
* government publishes comes close to.
* @param {Buffer} archive - The whole archive.
- * @returns {Map} Every entry, keyed by its path, left compressed.
+ * @returns {Archive} Every entry, keyed by its path, left compressed.
*/
-const unzip = (archive: Buffer): Map => {
+const unzip = (archive: Buffer): Archive => {
let end = archive.length - 22;
while (end >= 0 && archive.readUInt32LE(end) !== END_OF_CENTRAL_DIRECTORY_SIGNATURE) end--;
@@ -94,6 +102,11 @@ const unzip = (archive: Buffer): Map => {
const local = archive.readUInt32LE(offset + 42);
const nameStart = offset + CENTRAL_DIRECTORY_HEADER_SIZE;
const name = archive.toString("utf8", nameStart, nameStart + nameLength);
+
+ if (archive.readUInt32LE(local) !== LOCAL_HEADER_SIGNATURE) {
+ throw new Error(`the zip entry ${name} points at no local header`);
+ }
+
const start =
local +
LOCAL_HEADER_SIZE +
@@ -104,7 +117,7 @@ const unzip = (archive: Buffer): Map => {
offset = nameStart + nameLength + skipped;
}
- return files;
+ return { files, read: 0 };
};
/**
@@ -125,30 +138,40 @@ const columnIndex = (letters: string): number => {
/**
* Inflates one file of the archive and reads it as UTF-8. Only the files a caller asks for are
* inflated, which leaves the themes, the styles and the custom XML of a workbook untouched.
- * @param {Map} files - The entries of the archive.
+ * @param {Archive} archive - The archive.
* @param {string} path - The path of the file inside the archive.
* @returns {string} The content of the file.
*/
-const readFile = (files: Map, path: string): string => {
- const entry = files.get(path);
+const readFile = (archive: Archive, path: string): string => {
+ const entry = archive.files.get(path);
if (entry === undefined) throw new Error(`the workbook has no ${path}`);
- return (
+ const content =
entry.method === DEFLATE
? inflateRawSync(entry.data, { maxOutputLength: MAXIMUM_FILE_SIZE })
- : entry.data
- ).toString("utf8");
+ : entry.data;
+
+ archive.read += content.length;
+
+ if (archive.read > MAXIMUM_WORKBOOK_SIZE) {
+ throw new Error(`the workbook holds more than ${MAXIMUM_WORKBOOK_SIZE} bytes`);
+ }
+
+ return content.toString("utf8");
};
/**
* Joins the text runs of a shared string or of an inline string. A cell whose text is styled in
- * pieces carries one `` per piece.
+ * pieces carries one `` per piece. The `` elements hold the phonetic reading of the
+ * text, not the text, so they are dropped first.
* @param {string} xml - The content of the `` or `` element.
* @returns {string} The text of the element.
*/
const joinRuns = (xml: string): string =>
- [...xml.matchAll(/]*>([^<]*)<\/t>/g)].map((run) => decodeXml(run[1])).join("");
+ [...xml.replaceAll(//gs, "").matchAll(/]*>([^<]*)<\/t>/g)]
+ .map((run) => decodeXml(run[1]))
+ .join("");
/**
* Resolves the target of a workbook relationship into a path inside the archive. The OPC spec
@@ -214,37 +237,88 @@ const readRows = (sheet: string, strings: string[]): string[][] => {
};
/**
- * Reads one sheet of an `.xlsx` workbook, enough of the format for the plain tables the
- * government publishes: shared strings (rich text runs joined), inline strings, numbers and
- * empty cells.
- * @param {Buffer} workbook - The `.xlsx` file.
- * @param {string} sheetName - The name of the sheet, as its tab shows it.
- * @returns {string[][]} The rows of the sheet, every cell as text and an absent cell as `""`.
+ * Opens a workbook: its archive, its shared strings and the list of its sheets, each resolved
+ * through `xl/_rels/workbook.xml.rels` to the part that holds it.
+ * @param {Buffer} buffer - The `.xlsx` file.
+ * @returns {Workbook} The opened workbook.
*/
-export const readXlsxSheet = (workbook: Buffer, sheetName: string): string[][] => {
- const files = unzip(workbook);
- const sheets = readFile(files, "xl/workbook.xml").matchAll(/]*>/g);
- const sheet = [...sheets].find((tag) => decodeXml(tag[0]).includes(`name="${sheetName}"`));
- const relationId = /r:id="([^"]+)"/.exec(sheet?.[0] ?? "")?.[1];
- const relations = readFile(files, "xl/_rels/workbook.xml.rels").matchAll(/]*>/g);
- const relation = [...relations].find((tag) => tag[0].includes(`Id="${relationId}"`));
- const target = /Target="([^"]+)"/.exec(relation?.[0] ?? "")?.[1];
+const openWorkbook = (buffer: Buffer): Workbook => {
+ const archive = unzip(buffer);
+ const relations = readFile(archive, "xl/_rels/workbook.xml.rels").matchAll(
+ /]*>/g,
+ );
+ const targets = new Map();
+
+ for (const [tag] of relations) {
+ const id = /\bId="([^"]+)"/.exec(tag)?.[1];
+ const target = /\bTarget="([^"]+)"/.exec(tag)?.[1];
+
+ if (id !== undefined && target !== undefined) targets.set(id, decodeXml(target));
+ }
+
+ const sheets: SheetEntry[] = [];
- if (target === undefined) throw new Error(`the workbook has no sheet named ${sheetName}`);
+ for (const [tag] of readFile(archive, "xl/workbook.xml").matchAll(/]*>/g)) {
+ const name = /\bname="([^"]*)"/.exec(tag)?.[1];
+ const target = targets.get(/\br:id="([^"]+)"/.exec(tag)?.[1] ?? "");
- const shared = files.has(SHARED_STRINGS) ? readFile(files, SHARED_STRINGS) : "";
+ if (name !== undefined && target !== undefined) {
+ sheets.push({ name: decodeXml(name), path: resolveTarget(target) });
+ }
+ }
+
+ const shared = archive.files.has(SHARED_STRINGS) ? readFile(archive, SHARED_STRINGS) : "";
const items = shared.matchAll(/]*?(?:\/>|>(.*?)<\/si>)/gs);
const strings = [...items].map((item) => joinRuns(item[1] ?? ""));
- const content = readFile(files, resolveTarget(target));
- const rows = readRows(content, strings);
+ return { archive, strings, sheets };
+};
+
+/**
+ * Reads one sheet of an opened workbook and checks its rows against the dimension it declares.
+ * @param {Workbook} workbook - The opened workbook.
+ * @param {SheetEntry} sheet - The sheet to read.
+ * @returns {string[][]} The rows of the sheet.
+ */
+const readSheet = (workbook: Workbook, sheet: SheetEntry): string[][] => {
+ const content = readFile(workbook.archive, sheet.path);
+ const rows = readRows(content, workbook.strings);
const dimension = / {
+ const opened = openWorkbook(workbook);
+ const sheet = opened.sheets.find(({ name }) => name === sheetName);
+
+ if (sheet === undefined) throw new Error(`the workbook has no sheet named ${sheetName}`);
+
+ return readSheet(opened, sheet);
+};
+
+/**
+ * Reads every sheet of an `.xlsx` workbook the way `readXlsxSheet` reads one, for a workbook
+ * whose sheet names change from one version to the next.
+ * @param {Buffer} workbook - The `.xlsx` file.
+ * @returns {Map} The rows of each sheet, by sheet name, in the order of the tabs.
+ */
+export const readXlsxSheets = (workbook: Buffer): Map => {
+ const opened = openWorkbook(workbook);
+
+ return new Map(opened.sheets.map((sheet) => [sheet.name, readSheet(opened, sheet)]));
+};
From 12a4379034b695a980b4ce4624bb28458756cbd7 Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Sat, 19 Sep 2026 15:08:11 -0300
Subject: [PATCH 07/12] fix(scripts): read an empty shared string reference of
the xlsx reader as an empty cell
A cell of type `s` whose `` is empty or missing went through `Number("")`, which is 0, so it
came out as the first shared string of the workbook instead of as an empty cell. The index is
now read only when it is a decimal number, and anything else gives `""`.
Checked on a built workbook whose row holds ``, a bare ``, `1` and
`-1`: it reads `["", "", "Y", ""]`, where it used to read `["X", "X", "Y", ""]`. Both
sheets of the ANEXO B and of the cClassTrib workbooks read the same as before.
---
scripts/read-xlsx-sheet.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/scripts/read-xlsx-sheet.ts b/scripts/read-xlsx-sheet.ts
index baef265ca..c328f0c3b 100644
--- a/scripts/read-xlsx-sheet.ts
+++ b/scripts/read-xlsx-sheet.ts
@@ -208,7 +208,8 @@ const readCells = (row: string, strings: string[]): string[] => {
const value = /([^<]*)<\/v>/.exec(body)?.[1] ?? "";
if (type === "inlineStr") cells[column] = joinRuns(body);
- else if (type === "s") cells[column] = strings[Number(value)] ?? "";
+ else if (type === "s")
+ cells[column] = /^\d+$/.test(value) ? (strings[Number(value)] ?? "") : "";
else cells[column] = decodeXml(value);
}
From 847253d7a103be89b2665bb0665b17d5daef9be7 Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Tue, 22 Sep 2026 04:13:06 -0300
Subject: [PATCH 08/12] fix(scripts): resolve the ANEXO B link of the service
list generator
The link to the ANEXO B workbook is read from the documentation page, and the
raw href went straight to fetch. A relative href has no base to resolve against
and an XML entity such as & stays encoded, so either one turns the download
into a request the portal cannot answer and the dataset workflow fails on a page
change rather than on a data change.
The href is now decoded with decodeXml and resolved against the documentation
page, the way scripts/ibs-cbs.ts already reads the links of the Portal da NF-e.
---
scripts/service-items.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/scripts/service-items.ts b/scripts/service-items.ts
index 63650f482..57d283157 100644
--- a/scripts/service-items.ts
+++ b/scripts/service-items.ts
@@ -4,6 +4,7 @@ import { writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts";
+import { decodeXml } from "./decode-xml.ts";
import { readXlsxSheet } from "./read-xlsx-sheet.ts";
import { serializeRecord } from "./serialize-record.ts";
@@ -16,6 +17,8 @@ const DOCUMENTATION_URL =
* The ANEXO B is published under a file name that carries its version and date, e.g.
* `anexo_b-nbs2-lista_servico_nacional-snnfse-v1-01-20260122.xlsx`, so the link is read from the
* documentation page instead of being pinned here: a new version is picked up by the next run.
+ * The `href` is read as the page writes it, so it is decoded and resolved against the page before
+ * it is fetched, the way `scripts/ibs-cbs.ts` reads the links of the Portal da NF-e.
*/
const ANNEX_LINK_REGEX = /href="([^"]*anexo_b-nbs2-lista_servico_nacional[^"]*\.xlsx)"/i;
@@ -79,7 +82,8 @@ const main = async (): Promise => {
if (link === undefined) throw new Error("the NFS-e documentation page links no ANEXO B");
- const workbookResponse = await fetchOk(link, "NFS-e ANEXO B");
+ const annex = new URL(decodeXml(link), DOCUMENTATION_URL).href;
+ const workbookResponse = await fetchOk(annex, "NFS-e ANEXO B");
const workbook = await workbookResponse.arrayBuffer();
const data = parseSheet(readXlsxSheet(Buffer.from(workbook), SHEET_NAME));
From 3cbfdb489d5dfb2264d9a876d4e69b75ab1d5eee Mon Sep 17 00:00:00 2001
From: Claude
Date: Sat, 26 Sep 2026 18:02:56 +0000
Subject: [PATCH 09/12] refactor(nbs): getNbs builds on isValidNbs
isValidNbs answered through getNbs, so a yes or no about a code went through building the
object getNbs returns. The dependency now runs the other way, as it does for the boleto and
the IBAN: isValidNbs holds the checks (a lookup code, the documented forms and the code in the
NBS 2.0 table) and getNbs starts from it and only builds the result.
Nothing a caller sees changes: the same inputs are valid, the same ones give null, and neither
function throws. A property test pins that getNbs returns null exactly when isValidNbs is false,
over the codes of the table, near misses, the keys of Object.prototype and any value at all.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
---
src/get-nbs/get-nbs.test.ts | 20 ++++++++++++++++++++
src/get-nbs/get-nbs.ts | 19 ++++++-------------
src/is-valid-nbs/is-valid-nbs.test.ts | 10 +++++++++-
src/is-valid-nbs/is-valid-nbs.ts | 22 ++++++++++++++++------
4 files changed, 51 insertions(+), 20 deletions(-)
diff --git a/src/get-nbs/get-nbs.test.ts b/src/get-nbs/get-nbs.test.ts
index 9a48f9dc8..90d16cd24 100644
--- a/src/get-nbs/get-nbs.test.ts
+++ b/src/get-nbs/get-nbs.test.ts
@@ -136,6 +136,26 @@ describe("getNbs", () => {
);
});
+ test("should return null exactly when isValidNbs is false", () => {
+ const inputArbitrary = fc.oneof(
+ codeArbitrary,
+ codeArbitrary.map(Number),
+ codeArbitrary.map((code) => formatNbs(code)),
+ codeArbitrary.map((code) => ` ${code.slice(0, -1)}${(Number(code.at(-1)) + 1) % 10}\n`),
+ fc.stringMatching(/^\d[\s.\-/]?\d{4}[\s.\-/]?\d{2}[\s.\-/]?\d{2}$/),
+ fc.constantFrom(...PROTOTYPE_KEYS),
+ fc.anything(),
+ );
+
+ fc.assert(
+ fc.property(inputArbitrary, (value) => {
+ const input = value as string | number;
+
+ expect(getNbs(input) === null).toBe(!isValidNbs(input));
+ }),
+ );
+ });
+
test("should only carry 9 digit codes starting with 1 and non-empty descriptions", () => {
fc.assert(
fc.property(codeArbitrary, (code) => {
diff --git a/src/get-nbs/get-nbs.ts b/src/get-nbs/get-nbs.ts
index a2327a5e3..de28ebdf1 100644
--- a/src/get-nbs/get-nbs.ts
+++ b/src/get-nbs/get-nbs.ts
@@ -1,6 +1,6 @@
-import { NBS_DESCRIPTIONS, NBS_FORMAT_REGEX } from "../_internals/constants/nbs";
-import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code";
+import { NBS_DESCRIPTIONS } from "../_internals/constants/nbs";
import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits";
+import { isValidNbs } from "../is-valid-nbs/is-valid-nbs";
/**
* A code of the NBS (Nomenclatura Brasileira de Serviços, Intangíveis e Outras Operações que
@@ -31,7 +31,7 @@ export type Nbs = {
*
* @param {string|number} value - The NBS code to look up, with or without the mask, e.g.
* `"1.0101.11.00"`, `"101011100"` or `101011100`.
- * @returns {Nbs|null} The matching code, or null when it is unknown or invalid.
+ * @returns {Nbs|null} The matching code, or null exactly when `isValidNbs` is false.
*
* @example
* ```typescript
@@ -52,16 +52,9 @@ export type Nbs = {
* Sistema Nacional NFS-e, `tiposSimples_v1.01.xsd`: `TSCodNBS` is `[0-9]{9}`.
*/
export const getNbs = (value: string | number): Nbs | null => {
- if (!isLookupCode(value)) return null;
+ if (!isValidNbs(value)) return null;
- const code = String(value).trim();
+ const code = sanitizeToDigits(String(value));
- if (!NBS_FORMAT_REGEX.test(code)) return null;
-
- const digits = sanitizeToDigits(code);
- const description = NBS_DESCRIPTIONS[digits];
-
- if (description === undefined) return null;
-
- return { code: digits, description };
+ return { code, description: NBS_DESCRIPTIONS[code] };
};
diff --git a/src/is-valid-nbs/is-valid-nbs.test.ts b/src/is-valid-nbs/is-valid-nbs.test.ts
index 90cf28d87..b6f98b0a0 100644
--- a/src/is-valid-nbs/is-valid-nbs.test.ts
+++ b/src/is-valid-nbs/is-valid-nbs.test.ts
@@ -1,4 +1,4 @@
-import { anyGarbage, anyValue } from "../_internals/test/arbitraries";
+import { anyGarbage, anyValue, PROTOTYPE_KEYS } from "../_internals/test/arbitraries";
import { expectAlwaysReturnsType, expectNeverThrows } from "../_internals/test/properties";
import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime";
import { isValidNbs } from "./is-valid-nbs";
@@ -44,6 +44,14 @@ describe("isValidNbs", () => {
expect(isValidNbs(-101_011_100)).toBe(false);
});
+ it("should reject the keys of Object.prototype", () => {
+ for (const key of PROTOTYPE_KEYS) expect(isValidNbs(key)).toBe(false);
+ });
+
+ it("should ignore surrounding whitespace", () => {
+ expect(isValidNbs(" 1.0101.11.00\n")).toBe(true);
+ });
+
describe("properties", () => {
test("should never throw, regardless of the input", () => {
expectNeverThrows(isValidNbs, anyGarbage);
diff --git a/src/is-valid-nbs/is-valid-nbs.ts b/src/is-valid-nbs/is-valid-nbs.ts
index d8f8f8286..83eb47059 100644
--- a/src/is-valid-nbs/is-valid-nbs.ts
+++ b/src/is-valid-nbs/is-valid-nbs.ts
@@ -1,14 +1,16 @@
-import { getNbs } from "../get-nbs/get-nbs";
+import { NBS_DESCRIPTIONS, NBS_FORMAT_REGEX } from "../_internals/constants/nbs";
+import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code";
+import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits";
/**
* Checks if a value is a code of the official NBS 2.0 table (Nomenclatura Brasileira de
* Serviços, Intangíveis e Outras Operações que Produzam Variações no Patrimônio), the code the
* national NFS-e carries in `cNBS`.
*
- * Accepts what `getNbs` accepts: the 9 digits or the `N.NNNN.NN.NN` mask, with a single
- * separator between the groups and optional surrounding whitespace, or a non-negative safe
- * integer. Only complete codes are valid; the chapter, position and subposition headings of the
- * nomenclature are not.
+ * Accepts the 9 digits or the `N.NNNN.NN.NN` mask, with a single separator between the groups
+ * and optional surrounding whitespace, or a non-negative safe integer; any other string
+ * (`"1.0101abc11.00"`) is not valid. Only complete codes are valid; the chapter, position and
+ * subposition headings of the nomenclature are not.
*
* The table is the NBS 2.0 the MDIC publishes. The ANEXO B of the Sistema Nacional NFS-e lists
* the same codes except three (`1.0402.29.00`, `1.0403.29.00` and `1.0904.40.00`), so a code
@@ -32,4 +34,12 @@ import { getNbs } from "../get-nbs/get-nbs";
* @see Official: https://www.gov.br/mdic/pt-br/images/REPOSITORIO/scs/decos/NBS/NBSa_2-0.csv
* The NBS 2.0 table in CSV.
*/
-export const isValidNbs = (value: string | number): boolean => getNbs(value) !== null;
+export const isValidNbs = (value: string | number): boolean => {
+ if (!isLookupCode(value)) return false;
+
+ const code = String(value).trim();
+
+ if (!NBS_FORMAT_REGEX.test(code)) return false;
+
+ return sanitizeToDigits(code) in NBS_DESCRIPTIONS;
+};
From 4fff2cf3cb31ef67b7417a15d28d8df7251494b4 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sat, 26 Sep 2026 18:07:13 +0000
Subject: [PATCH 10/12] refactor(service-item): getServiceItem builds on
isValidServiceItem
isValidServiceItem answered through getServiceItem, so a yes or no about a subitem went through
formatting the code the law prints. The dependency now runs the other way, as it does for the
boleto and the IBAN: isValidServiceItem holds the checks (a lookup code, the forms the law
prints and the zero padded subitem in the table) and getServiceItem starts from it and only
builds the result, the `N.NN` code and its description.
Both need the width the table is keyed by, so it moves next to the table and its format as
`SERVICE_ITEM_LENGTH`, written by the generator like the rest of that file.
Nothing a caller sees changes: the same inputs are valid, the same ones give null, and neither
function throws. A property test pins that getServiceItem returns null exactly when
isValidServiceItem is false, over the subitems of the table, well formed misses, the keys of
Object.prototype and any value at all. isValidServiceItem alone no longer bundles the
formatting, so the bundle size table of the getting started guide gives the pair as a range.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
---
docs/getting-started.md | 2 +-
docs/pt-br/getting-started.md | 2 +-
scripts/service-items.ts | 3 +++
src/_internals/constants/service-items.ts | 3 +++
src/get-service-item/get-service-item.test.ts | 19 +++++++++++++++
src/get-service-item/get-service-item.ts | 22 ++++++-----------
.../is-valid-service-item.test.ts | 15 +++++++++++-
.../is-valid-service-item.ts | 24 ++++++++++++++-----
8 files changed, 66 insertions(+), 24 deletions(-)
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 853c578b5..8190b2008 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -76,7 +76,7 @@ A few utils embed an official dataset and weigh far more than everything else co
| `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 |
| `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 |
+| `isValidServiceItem` · `getServiceItem` | Service list of the Lei Complementar 116/2003 | 27.1 - 27.2 KB | 8.8 - 8.9 KB |
The root of the package is a single ESM module, so a bundler cannot split one of these datasets out of it: importing a heavy util from the root puts its whole dataset in your main bundle, and a dynamic `import()` of the root does not help. To lazy-load one, import it from its own subpath:
diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md
index a3543b4a7..beadb0f03 100644
--- a/docs/pt-br/getting-started.md
+++ b/docs/pt-br/getting-started.md
@@ -76,7 +76,7 @@ Alguns utilitários embutem uma base de dados oficial e pesam muito mais que tod
| `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 |
| `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 |
+| `isValidServiceItem` · `getServiceItem` | lista de serviços da Lei Complementar 116/2003 | 27,1 - 27,2 KB | 8,8 - 8,9 KB |
A raiz do pacote é um único módulo ESM, então o bundler não consegue separar uma dessas bases de dados dele: importar um utilitário pesado da raiz coloca a base inteira no seu bundle principal, e um `import()` dinâmico da raiz não ajuda. Para carregar sob demanda, importe do subpath próprio:
diff --git a/scripts/service-items.ts b/scripts/service-items.ts
index 57d283157..43ad6423a 100644
--- a/scripts/service-items.ts
+++ b/scripts/service-items.ts
@@ -119,6 +119,9 @@ export const SERVICE_ITEM_DESCRIPTIONS: Record = ${serializeReco
* subitem, with the dot the law prints between them or without it.
*/
export const SERVICE_ITEM_FORMAT_REGEX = /^\\d{1,2}\\.?\\d{2}$/;
+
+/** Digits of a subitem the table is keyed by: two of the item and two of the subitem. */
+export const SERVICE_ITEM_LENGTH = 4;
`,
);
};
diff --git a/src/_internals/constants/service-items.ts b/src/_internals/constants/service-items.ts
index d4c0d861a..b626ef81e 100644
--- a/src/_internals/constants/service-items.ts
+++ b/src/_internals/constants/service-items.ts
@@ -313,3 +313,6 @@ export const SERVICE_ITEM_DESCRIPTIONS: Record = {
* subitem, with the dot the law prints between them or without it.
*/
export const SERVICE_ITEM_FORMAT_REGEX = /^\d{1,2}\.?\d{2}$/;
+
+/** Digits of a subitem the table is keyed by: two of the item and two of the subitem. */
+export const SERVICE_ITEM_LENGTH = 4;
diff --git a/src/get-service-item/get-service-item.test.ts b/src/get-service-item/get-service-item.test.ts
index e7328effb..96d0e3d99 100644
--- a/src/get-service-item/get-service-item.test.ts
+++ b/src/get-service-item/get-service-item.test.ts
@@ -134,6 +134,25 @@ describe("getServiceItem", () => {
);
});
+ test("should return null exactly when isValidServiceItem is false", () => {
+ const inputArbitrary = fc.oneof(
+ keyArbitrary,
+ keyArbitrary.map(Number),
+ keyArbitrary.map((key) => ` ${Number(key.slice(0, 2))}.${key.slice(2)}\n`),
+ fc.stringMatching(/^\d{1,2}\.?\d{2}$/),
+ fc.constantFrom(...PROTOTYPE_KEYS),
+ fc.anything(),
+ );
+
+ fc.assert(
+ fc.property(inputArbitrary, (value) => {
+ const input = value as string | number;
+
+ expect(getServiceItem(input) === null).toBe(!isValidServiceItem(input));
+ }),
+ );
+ });
+
test("should only carry items 01 to 40 and non-empty descriptions", () => {
fc.assert(
fc.property(keyArbitrary, (key) => {
diff --git a/src/get-service-item/get-service-item.ts b/src/get-service-item/get-service-item.ts
index 6a3d86ed7..302fec387 100644
--- a/src/get-service-item/get-service-item.ts
+++ b/src/get-service-item/get-service-item.ts
@@ -1,11 +1,9 @@
import {
SERVICE_ITEM_DESCRIPTIONS,
- SERVICE_ITEM_FORMAT_REGEX,
+ SERVICE_ITEM_LENGTH,
} from "../_internals/constants/service-items";
-import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code";
import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits";
-
-const SUBITEM_LENGTH = 4;
+import { isValidServiceItem } from "../is-valid-service-item/is-valid-service-item";
const ITEM_LENGTH = 2;
@@ -37,7 +35,8 @@ export type ServiceItem = {
*
* @param {string|number} value - The subitem to look up, e.g. `"1.01"`, `"01.01"`, `"0101"` or
* `101`.
- * @returns {ServiceItem|null} The matching subitem, or null when it is unknown or invalid.
+ * @returns {ServiceItem|null} The matching subitem, or null exactly when `isValidServiceItem` is
+ * false.
*
* @example
* ```typescript
@@ -57,19 +56,12 @@ export type ServiceItem = {
* 116/2003) e 2 para Desdobro Nacional".
*/
export const getServiceItem = (value: string | number): ServiceItem | null => {
- if (!isLookupCode(value)) return null;
-
- const written = String(value).trim();
-
- if (!SERVICE_ITEM_FORMAT_REGEX.test(written)) return null;
-
- const digits = sanitizeToDigits(written).padStart(SUBITEM_LENGTH, "0");
- const description = SERVICE_ITEM_DESCRIPTIONS[digits];
+ if (!isValidServiceItem(value)) return null;
- if (description === undefined) return null;
+ const digits = sanitizeToDigits(String(value)).padStart(SERVICE_ITEM_LENGTH, "0");
return {
code: `${Number(digits.slice(0, ITEM_LENGTH))}.${digits.slice(ITEM_LENGTH)}`,
- description,
+ description: SERVICE_ITEM_DESCRIPTIONS[digits],
};
};
diff --git a/src/is-valid-service-item/is-valid-service-item.test.ts b/src/is-valid-service-item/is-valid-service-item.test.ts
index 1405eb690..aafb68189 100644
--- a/src/is-valid-service-item/is-valid-service-item.test.ts
+++ b/src/is-valid-service-item/is-valid-service-item.test.ts
@@ -1,4 +1,4 @@
-import { anyGarbage, anyValue } from "../_internals/test/arbitraries";
+import { anyGarbage, anyValue, PROTOTYPE_KEYS } from "../_internals/test/arbitraries";
import { expectAlwaysReturnsType, expectNeverThrows } from "../_internals/test/properties";
import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime";
import { isValidServiceItem } from "./is-valid-service-item";
@@ -42,6 +42,19 @@ describe("isValidServiceItem", () => {
expect(isValidServiceItem(1.01)).toBe(false);
});
+ it("should validate the bare digits of an item with one digit", () => {
+ expect(isValidServiceItem("101")).toBe(true);
+ expect(isValidServiceItem("1.01")).toBe(true);
+ });
+
+ it("should ignore surrounding whitespace", () => {
+ expect(isValidServiceItem(" 1.01\n")).toBe(true);
+ });
+
+ it("should reject the keys of Object.prototype", () => {
+ for (const key of PROTOTYPE_KEYS) expect(isValidServiceItem(key)).toBe(false);
+ });
+
describe("properties", () => {
test("should never throw, regardless of the input", () => {
expectNeverThrows(isValidServiceItem, anyGarbage);
diff --git a/src/is-valid-service-item/is-valid-service-item.ts b/src/is-valid-service-item/is-valid-service-item.ts
index 6b6c8917e..8ed1fa4ef 100644
--- a/src/is-valid-service-item/is-valid-service-item.ts
+++ b/src/is-valid-service-item/is-valid-service-item.ts
@@ -1,12 +1,17 @@
-import { getServiceItem } from "../get-service-item/get-service-item";
+import {
+ SERVICE_ITEM_DESCRIPTIONS,
+ SERVICE_ITEM_FORMAT_REGEX,
+ SERVICE_ITEM_LENGTH,
+} from "../_internals/constants/service-items";
+import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code";
+import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits";
/**
* Checks if a value is a subitem in force of the service list annexed to the Lei Complementar
* 116/2003, the list of the services the ISS is levied on.
*
- * Accepts what `getServiceItem` accepts: the form the law prints (`"1.01"`), a zero padded item
- * (`"01.01"`), the bare digits (`"0101"`, `"101"` or the integer `101`), with optional
- * surrounding whitespace. The dot is the only separator the law ever prints, so `"1-01"` is not
+ * Accepts the form the law prints (`"1.01"`), a zero padded item (`"01.01"`), the bare digits
+ * (`"0101"`, `"101"` or the integer `101`), with optional surrounding whitespace. The dot is the only separator the law ever prints, so `"1-01"` is not
* valid. Vetoed subitems, item headings, the 6 digit national codes of the NFS-e and item 99 of
* the national list are not valid.
*
@@ -28,5 +33,12 @@ import { getServiceItem } from "../get-service-item/get-service-item";
* Sistema Nacional NFS-e, `ANEXO_B-NBS2-LISTA_SERVICO_NACIONAL`, sheet `LISTA.SERV.NAC.`: the
* list in force in machine readable form.
*/
-export const isValidServiceItem = (value: string | number): boolean =>
- getServiceItem(value) !== null;
+export const isValidServiceItem = (value: string | number): boolean => {
+ if (!isLookupCode(value)) return false;
+
+ const written = String(value).trim();
+
+ if (!SERVICE_ITEM_FORMAT_REGEX.test(written)) return false;
+
+ return sanitizeToDigits(written).padStart(SERVICE_ITEM_LENGTH, "0") in SERVICE_ITEM_DESCRIPTIONS;
+};
From a13cde81f405a6d483fa0b169f1f886f93d79538 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sat, 26 Sep 2026 18:49:46 +0000
Subject: [PATCH 11/12] fix(scripts): summarize the NBS and service list tables
in the dataset refresh
scripts/data.ts regenerates nbs.ts and service-items.ts every week, but
scripts/data-summary.ts did not list them, so an Update datasets pull request
that changed either one said nothing about it, or "No dataset file changed."
when nothing else did.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
---
scripts/data-summary.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/scripts/data-summary.ts b/scripts/data-summary.ts
index b23514099..d8f7750e6 100644
--- a/scripts/data-summary.ts
+++ b/scripts/data-summary.ts
@@ -21,6 +21,9 @@ const DATASETS: Record = {
"src/_internals/constants/cfop.ts": "CFOP codes (CONFAZ, Convênio SINIEF s/nº 1970)",
"src/_internals/constants/cnae.ts": "CNAE subclasses (IBGE/CONCLA)",
"src/_internals/constants/municipalities.ts": "Municipalities (IBGE)",
+ "src/_internals/constants/nbs.ts": "NBS 2.0 descriptions (MDIC)",
+ "src/_internals/constants/service-items.ts":
+ "LC 116/2003 service list (Sistema Nacional NFS-e, ANEXO B)",
"src/_internals/constants/states.ts": "States (IBGE)",
"src/is-valid-legal-nature/constants.ts": "Legal natures (IBGE/CONCLA)",
"src/is-valid-ncm/constants.ts": "NCM codes (Siscomex)",
From 4305328d0f36981eb61747b545f29b7b024993b6 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sat, 26 Sep 2026 23:13:31 +0000
Subject: [PATCH 12/12] fix(nbs): read a number only when it is a non-negative
safe integer
formatNbs accepted `string | number` and read a number as the string of its digits, sign and
decimal point included: the sign and the decimal point were dropped like mask characters and
what was left was read as a code the number never was. `formatNbs(-101011100)` gave
"1.0101.11.00" and `formatNbs(1e21)` gave "1.21".
The first guard is now `isLookupCode`, as in the validators and lookups, so a number is only
read when it is a non-negative safe integer. Any other number (negative, fractional, not finite
or past `Number.MAX_SAFE_INTEGER`) gives the empty string this function already returns for
null, so each example above now gives "". In a string, "-" and "." are still mask characters:
every string and every non-negative safe integer is read as before.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
---
docs/pt-br/utilities.md | 3 ++-
docs/utilities.md | 3 ++-
src/format-nbs/format-nbs.test.ts | 12 ++++++++++++
src/format-nbs/format-nbs.ts | 11 +++++++----
4 files changed, 23 insertions(+), 6 deletions(-)
diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md
index fa0cb8770..6c2de544f 100644
--- a/docs/pt-br/utilities.md
+++ b/docs/pt-br/utilities.md
@@ -2816,7 +2816,7 @@ isValidNbs('1.0101abc11.00'); // false (não é uma forma documentada)
Formata um código NBS (Nomenclatura Brasileira de Serviços) na máscara `N.NNNN.NN.NN` em que a nomenclatura o imprime. Só a estrutura muda; use `isValidNbs` para conferir um código com a tabela.
- Todo código NBS começa com 1, então, diferente do `formatNcm`, não há opção `pad`.
-- No resto, mesmas regras de `formatCnae`: a máscara é aplicada até onde o valor vai, os caracteres fora dela são descartados e um número é lido como a string dos seus dígitos.
+- No resto, mesmas regras de `formatCnae`: a máscara é aplicada até onde o valor vai, os caracteres fora dela são descartados e um número só é lido como a string dos seus dígitos quando é um inteiro seguro não negativo; qualquer outro número retorna `''`.
```javascript
import { formatNbs } from '@brazilian-utils/brazilian-utils';
@@ -2825,6 +2825,7 @@ formatNbs('101011100'); // 1.0101.11.00
formatNbs(101011100); // 1.0101.11.00
formatNbs('10101'); // 1.0101 (mascarado até onde vai)
formatNbs('abc101011100'); // 1.0101.11.00 (só os dígitos são lidos)
+formatNbs(-101011100); // '' (não é um inteiro seguro não negativo)
```
### getNbs
diff --git a/docs/utilities.md b/docs/utilities.md
index 6b391747a..5a47d74b3 100644
--- a/docs/utilities.md
+++ b/docs/utilities.md
@@ -2816,7 +2816,7 @@ isValidNbs('1.0101abc11.00'); // false (not a documented form)
Format an NBS (Nomenclatura Brasileira de Serviços) code into the `N.NNNN.NN.NN` mask the nomenclature prints. Only the structure changes; use `isValidNbs` to check a code against the table.
- Every NBS code starts with 1, so, unlike `formatNcm`, there is no `pad` option.
-- Same rules as `formatCnae` otherwise: the mask is applied as far as the value goes, characters outside it are dropped, and a number is read as the string of its digits.
+- Same rules as `formatCnae` otherwise: the mask is applied as far as the value goes, characters outside it are dropped, and a number is read as the string of its digits only when it is a non-negative safe integer; any other number returns `''`.
```javascript
import { formatNbs } from '@brazilian-utils/brazilian-utils';
@@ -2825,6 +2825,7 @@ formatNbs('101011100'); // 1.0101.11.00
formatNbs(101011100); // 1.0101.11.00
formatNbs('10101'); // 1.0101 (masked as far as it goes)
formatNbs('abc101011100'); // 1.0101.11.00 (only the digits are read)
+formatNbs(-101011100); // '' (not a non-negative safe integer)
```
### getNbs
diff --git a/src/format-nbs/format-nbs.test.ts b/src/format-nbs/format-nbs.test.ts
index 20dcf0cf7..48751b899 100644
--- a/src/format-nbs/format-nbs.test.ts
+++ b/src/format-nbs/format-nbs.test.ts
@@ -70,6 +70,18 @@ describe("formatNbs", () => {
expectIdempotent(formatNbs, nineDigits);
});
});
+
+ test("when it is a negative, fractional or unsafe number", () => {
+ expect(formatNbs(-101_011_100)).toBe("");
+ expect(formatNbs(-1)).toBe("");
+ expect(formatNbs(1.5)).toBe("");
+ expect(formatNbs(2 ** 53)).toBe("");
+ expect(formatNbs(Number.MAX_VALUE)).toBe("");
+ expect(formatNbs(1e21)).toBe("");
+ expect(formatNbs(Number.NaN)).toBe("");
+ expect(formatNbs(Number.POSITIVE_INFINITY)).toBe("");
+ expect(formatNbs(Number.NEGATIVE_INFINITY)).toBe("");
+ });
});
describe("formatNbs types", () => {
diff --git a/src/format-nbs/format-nbs.ts b/src/format-nbs/format-nbs.ts
index 075b037f6..6a521cb6e 100644
--- a/src/format-nbs/format-nbs.ts
+++ b/src/format-nbs/format-nbs.ts
@@ -1,5 +1,5 @@
import { format } from "../_internals/format/format";
-import { isNullish } from "../_internals/is-nullish/is-nullish";
+import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code";
import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits";
/**
@@ -7,10 +7,12 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d
* Produzam Variações no Patrimônio) code into the `N.NNNN.NN.NN` mask the nomenclature prints.
*
* This is a purely structural transformation, it does not check the code against the official
- * table, use `isValidNbs` for that. Like every formatter of this package, the value is read for
+ * table, use `isValidNbs` for that. Like every formatter of this package, a string is read for
* its digits and masked as far as they go, so a partial code still being typed is masked
* progressively, characters outside the mask are dropped and anything without a digit gives
- * `""` instead of throwing. Every NBS code starts with 1, so there is no zero padding to offer.
+ * `""` instead of throwing. A number is only read when it is a non-negative safe integer: its
+ * sign and decimal point are not mask characters, so a negative, fractional, not finite or unsafe
+ * number gives `""` too. Every NBS code starts with 1, so there is no zero padding to offer.
*
* @param {string|number} value - The NBS code to be formatted.
* @returns {string} The formatted code in the `N.NNNN.NN.NN` pattern, or an empty string when
@@ -21,6 +23,7 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d
* formatNbs("101011100"); // "1.0101.11.00"
* formatNbs(101011100); // "1.0101.11.00"
* formatNbs("10101"); // "1.0101" (partial values are masked as far as they go)
+ * formatNbs(-101011100); // "" (not a non-negative safe integer)
* ```
*
* @see Official: https://www.gov.br/mdic/pt-br/images/REPOSITORIO/scs/decos/NBS/Anexoa_Ia_NBSa_2.0a_coma_alteraa_esa_6.12.18.pdf
@@ -28,4 +31,4 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d
* the `N.NNNN.NN.NN` form every code of the nomenclature is printed in.
*/
export const formatNbs = (value: string | number): string =>
- isNullish(value) ? "" : format({ value: sanitizeToDigits(value), pattern: "0.0000.00.00" });
+ isLookupCode(value) ? format({ value: sanitizeToDigits(value), pattern: "0.0000.00.00" }) : "";