Documentação
Como o sistema interpreta os arquivos fiscais e valida Notas Fiscais contra a base de regras CFOP.
Visão geral
O sistema recebe três tipos de arquivos pela aba Upload: dados de NFs no padrão Rígido (RPS), no padrão Flexível, e a matriz de Regras CFOP que define o que é esperado para cada operação fiscal.
Os dados são processados no browser, normalizados, enviados ao backend e (no futuro) persistidos em SQLite para validação.
Tabs e seus papéis
| Tab | Papel | Origem típica |
|---|---|---|
| Rígidos | Linhas de NF no padrão SAP/RPS | Export do ERP |
| Flexíveis | Linhas de NF no padrão flexível | Export do ERP |
| Regras CFOP | Matriz de regras esperadas por CFOP × imposto | Planilha mantida pelo time fiscal |
Pipeline de transformação (cliente)
- Skip de linhas — Regras CFOP descarta a 1ª linha (cabeçalho-mestre).
- Normalização de colunas — NFKD strip de acentos, lowercase, espaços →
_. - Trim de células — espaços nas pontas são removidos.
- Rename por ocorrência — em Regras CFOP, as 4 colunas duplicadas
clas_itemviramclas_item_icms,clas_item_ipi,clas_item_pis,clas_item_cofins. - Dedupe — para Rígidos/Flexíveis, colunas com o mesmo nome mantêm a primeira ocorrência. Desativado em Regras CFOP.
- Numeric list — em Regras CFOP, células
clas_item_*como"01,02,03 ou 10"ou"06 e 07"são convertidas em listas numéricas ([1, 2, 3, 10]).
Schema da Matriz de Regras CFOP
Após normalização, cada linha tem as colunas:
| Coluna | Tipo | Descrição |
|---|---|---|
| cfop_consolidado | string | Código CFOP de 4 dígitos |
| descricao_cfop | string | Descrição livre da operação |
| clas_item_icms | number[] | Classificações aplicáveis (ICMS) |
| cst_icms | integer? | CST esperado de ICMS |
| base_icms_e_vlr_icms | COM VALOR | VAZIO | ∅ | Exigência da dupla base/valor de ICMS |
| clas_item_ipi | number[] | Classificações aplicáveis (IPI) |
| cod_trib_ipi | integer? | Código tributário IPI esperado |
| base_ipi_e_vlr_ipi | COM VALOR | VAZIO | ∅ | Idem para IPI |
| clas_item_pis | number[] | Classificações aplicáveis (PIS) |
| cod_situacao_pis | integer? | Código de situação PIS esperado |
| vlr_base_pis_e_vlr_pis | COM VALOR | VAZIO | ∅ | Idem para PIS |
| clas_item_cofins | number[] | Classificações aplicáveis (COFINS) |
| cod_situacao_cofins | integer? | Código de situação COFINS esperado |
| vlr_base_cofins_e_vlr_cofins | COM VALOR | VAZIO | ∅ | Idem para COFINS |
Normalização para SQLite
O endpoint POST /api/upload/regras-cfop normaliza cada linha em registros 1-por-tributo:
{
cfop: string,
descricao_cfop: string,
tax: 'icms' | 'ipi' | 'pis' | 'cofins',
clas_item: number[],
cst: number | null,
value_requirement: 'COM VALOR' | 'VAZIO' | null
}Linhas onde todos os três campos (clas_item, cst, value_requirement) estão vazios para um tributo são descartadas — significa que aquele tributo não se aplica àquele CFOP.
Tabela SQLite (apps/web/db/schema.sql):
CREATE TABLE cfop_rule (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cfop TEXT NOT NULL,
descricao_cfop TEXT NOT NULL DEFAULT '',
tax TEXT NOT NULL CHECK (tax IN ('icms','ipi','pis','cofins')),
clas_item INTEGER NOT NULL,
cst INTEGER,
value_requirement TEXT CHECK (value_requirement IN ('COM VALOR','VAZIO')),
UNIQUE (cfop, tax, clas_item)
);O array clas_item é explodido em múltiplas linhas — uma por classificação — pra que a validação seja um lookup direto sem JSON parsing.
Glossário — clas_item (Origem da Mercadoria)
Os códigos de clas_item usados pela matriz CFOP, pelo TRIB_ICMS em Rígidos e pelo Item - Origem produto (Código) em Flexíveis seguem a tabela CST de origem da mercadoria:
| Código | Descrição |
|---|---|
| 0 | Nacional, exceto as indicadas nos códigos 3, 4, 5 e 8 |
| 1 | Estrangeira — importação direta, exceto a indicada no código 6 |
| 2 | Estrangeira — adquirida no mercado interno, exceto a indicada no código 7 |
| 3 | Nacional, com conteúdo de importação superior a 40% |
| 4 | Nacional, produção em conformidade com PPB |
| 5 | Nacional, com conteúdo de importação ≤ 40% |
| 6 | Estrangeira — importação direta, sem similar nacional (lista CAMEX) |
| 7 | Estrangeira — adquirida no mercado interno, sem similar nacional (lista CAMEX) |
| 8 | Nacional, com conteúdo de importação superior a 70% |
| 10 | Wildcard (curinga) — usado pela matriz para casar com qualquer origem |
Interpretações adotadas
A documentação oficial é incompleta em alguns pontos. Estas são as convenções adotadas pelo sistema:
- clas_item = 0 é wildcard. Os documentos não cravam isso, mas é a única interpretação que faz sentido prático — uma regra com
clas_item = 0casa com qualquer classificação na NF. A consulta de validação aplicaWHERE clas_item = ? OR clas_item = 0. - COM VALOR exige base > 0 E valor > 0. Ambos os campos (base e valor do tributo) devem estar preenchidos com valor não nulo na NF.
- VAZIO exige base = 0 E valor = 0. Ambos os campos devem estar zerados/em branco na NF.
- Célula em branco = tributo não aplicável. Quando uma linha de regra deixa os campos de um tributo todos em branco, o sistema não emite nenhuma exigência para aquele tributo nessa operação.
- Sem regra para um CFOP = warning, não erro. Quando a NF traz um CFOP que não aparece na matriz, a linha é sinalizada para revisão manual em vez de ser barrada.
Validação de NF (planejado)
Para cada linha de NF (Rígido ou Flexível), o motor:
- Busca regras em
cfop_rulepor(cod_cfo, tax), filtrando porclas_item = nf.clas_item OR clas_item = 0. - Para cada tributo, compara o CST/cod_situação da NF com o da regra — divergência é violação.
- Aplica a exigência
COM VALOR/VAZIOcontra os pares de colunas correspondentes na NF (base_icms+vlr_icms, etc.). - Acumula violações por linha e retorna um relatório.
Observability
Um serviço Bun em apps/web/observability.ts escuta em :6969/log e grava cada evento em logs.txt. O package @compliance/logger expõe createLogger({ source }).info(...) e é usado tanto no cliente (passos de transformação no upload) quanto no servidor (endpoints de upload).