Create AGENTS.md

This commit is contained in:
2026-03-04 18:18:11 -03:00
parent 06eca335db
commit 3af14ab957
9 changed files with 918 additions and 1012 deletions
+304
View File
@@ -0,0 +1,304 @@
# Domain — AGENTS.md
Camada de domínio do projeto `comparador-notas`. Contém as entidades, serviços
de negócio e erros tipados. Não possui dependências de infraestrutura — todo
acesso a banco, arquivos ou UI é responsabilidade das camadas superiores.
---
## Estrutura dos arquivos
```
src/domain/
├── mod.rs
├── errors.rs # Enums de erro tipados
├── entities/
│ ├── mod.rs
│ ├── chave_serie.rs # Chave composta (serie + documento_tipo)
│ ├── layout.rs # Entidades de configuração de layout CSV/XLSX
│ ├── nota.rs # Entidade Nota Fiscal
│ ├── resultado_analise.rs # Resultados de análise (pré e completo)
│ └── serie.rs # Validação de série
└── services/
├── mod.rs
├── detector_duplicidade.rs # Detecção de notas duplicadas
├── detector_sequencia.rs # Detecção de notas faltantes na sequência
└── parser_monetario.rs # Parsing e formatação de valores monetários
```
---
## errors.rs
Erros tipados com a crate `thiserror`. Cada domínio tem seu próprio enum.
### `ErroSerie`
| Variante | Mensagem |
|---|---|
| `Invalida(String)` | Série inválida: deve conter de 1 a 3 dígitos numéricos |
| `Vazia` | Série vazia |
### `ErroValor`
| Variante | Mensagem |
|---|---|
| `Negativo(String)` | Valor negativo não é permitido |
| `NaoNumerico(String)` | Valor não numérico |
### `ErroLayout`
| Variante | Mensagem |
|---|---|
| `CampoObrigatorioAusente(String)` | Campo obrigatório ausente |
| `JsonMalformado(String)` | JSON malformado |
| `NomeConflitante(String)` | Layout com esse nome já existe |
### `ErroArquivo`
| Variante | Mensagem |
|---|---|
| `TamanhoExcedido(u64)` | Arquivo > 50 MB |
| `Corrompido(String)` | Arquivo corrompido ou ilegível |
| `ErroLeitura(String)` | Erro genérico de leitura |
### `ResumoAvisos`
Estrutura de avisos não-fatais acumulados durante a importação de um arquivo.
```rust
pub struct ResumoAvisos {
pub linhas_malformadas: usize,
pub numeros_invalidos: usize,
pub series_invalidas: usize,
pub valores_invalidos: usize,
pub detalhes: Vec<String>, // mensagens individuais por linha
}
```
- `tem_avisos()``true` se qualquer contador > 0
- `linhas_para_exibir()``Vec<String>` com resumo para exibição em modal
---
## entities/
### `chave_serie.rs` — `ChaveSerie`
Chave composta que identifica um grupo de notas fiscais. Combina série com
tipo de documento (`NFE`, `NFCE`, etc.).
```rust
pub struct ChaveSerie {
pub serie: String,
pub documento_tipo: Option<String>,
}
```
- Deriva `Hash`, `Eq`, `Ord` — usada como chave em `HashMap` e para ordenação.
- `new(serie, documento_tipo)` — construtor.
- `label()` — formata para exibição:
- `Some(tipo)``"001 / NFE"`
- `None``"001"`
### `nota.rs` — `Nota`
Entidade central. `(numero, serie, documento_tipo)` é o identificador único.
```rust
pub struct Nota {
pub numero: u64,
pub serie: String,
pub documento_tipo: Option<String>, // None quando não mapeado
pub valor: Option<Decimal>,
pub data: Option<NaiveDate>, // usado no PDF, não em regras
}
```
### `serie.rs` — `validar_serie`
```rust
pub fn validar_serie(s: &str) -> Result<String, ErroSerie>
```
Valida via regex `^[0-9]{1,3}$` (1 a 3 dígitos numéricos). Faz trim antes
de validar. Usa `OnceLock` para compilar o regex uma única vez.
### `resultado_analise.rs`
Dois tipos de resultado que modelam o fluxo de análise em duas etapas:
#### `ResultadoPreAnalise`
Resultado intermediário — calculado sem expandir a lista completa de faltantes.
Usado para verificar se algum intervalo excede 10.000 registros (RF04) antes
de pedir confirmação ao usuário.
```rust
pub struct ResultadoPreAnalise {
pub intervalos_por_serie: HashMap<ChaveSerie, IntervaloSerie>,
pub duplicadas_por_serie: HashMap<ChaveSerie, Vec<(u64, usize)>>,
pub soma_total: Decimal,
pub soma_por_serie: HashMap<ChaveSerie, Decimal>,
pub total_por_serie: HashMap<ChaveSerie, usize>,
}
```
#### `IntervaloSerie`
```rust
pub struct IntervaloSerie {
pub minimo: u64,
pub maximo: u64,
pub contagem_faltantes: u64,
}
```
- `excede_limite(limite)``contagem_faltantes > limite`
#### `ResultadoAnalise`
Resultado completo com a lista materializada de faltantes.
```rust
pub struct ResultadoAnalise {
pub faltantes_por_serie: HashMap<ChaveSerie, Vec<u64>>, // ordenados crescentemente
pub duplicadas_por_serie: HashMap<ChaveSerie, Vec<(u64, usize)>>,
pub soma_total: Decimal,
pub soma_por_serie: HashMap<ChaveSerie, Decimal>,
pub total_por_serie: HashMap<ChaveSerie, usize>,
}
```
Métodos auxiliares:
- `sem_inconsistencias()``true` se não há faltantes nem duplicatas
- `total_faltantes()` → soma do tamanho de todas as listas de faltantes
- `total_duplicatas()` → soma do número de grupos de duplicatas
### `layout.rs`
Entidades de configuração de layout de arquivo.
#### `TipoArquivo`
```rust
pub enum TipoArquivo { Csv, Xlsx }
```
#### `LayoutCsv`
| Campo | Tipo | Descrição |
|---|---|---|
| `delimitador` | `char` | `','`, `';'` ou `'\t'` |
| `encoding` | `String` | `"utf-8"` ou `"windows-1252"` |
| `linha_cabecalho` | `usize` | Linha do cabeçalho (base 1). `0` = sem cabeçalho |
| `indice_numero` | `usize` | Índice da coluna Numero (base 0) |
| `indice_serie` | `usize` | Índice da coluna Serie (base 0) |
| `indice_valor` | `Option<usize>` | Índice da coluna Valor (base 0), opcional |
| `indice_data` | `Option<usize>` | Índice da coluna Data (base 0), opcional |
| `indice_documento_tipo` | `Option<usize>` | Índice da coluna Tipo Documento (base 0), opcional |
#### `LayoutXlsx`
| Campo | Tipo | Descrição |
|---|---|---|
| `aba` | `String` | Nome da aba a processar |
| `pos_numero` | `String` | Posição LetraLinha (ex: `"D3"`) |
| `pos_serie` | `String` | Posição LetraLinha (ex: `"B3"`) |
| `pos_valor` | `Option<String>` | Posição LetraLinha, opcional |
| `pos_data` | `Option<String>` | Posição LetraLinha, opcional |
| `pos_documento_tipo` | `Option<String>` | Posição LetraLinha, opcional |
#### `Layout` (enum)
```rust
pub enum Layout {
Csv { id: Option<i64>, nome: String, config: LayoutCsv },
Xlsx { id: Option<i64>, nome: String, config: LayoutXlsx },
}
```
Métodos: `id()`, `nome()`, `tipo()`.
#### `LayoutJson` (serialização)
Representação `serde` com tag `"tipo"` para importação/exportação em JSON.
Implementa `TryFrom<LayoutJson> for Layout` (valida campos obrigatórios) e
`From<&Layout> for LayoutJson`.
---
## services/
### `detector_duplicidade.rs`
#### `detectar_duplicidades(notas) -> HashMap<(u64, String, Option<String>), usize>`
Conta ocorrências de cada `(numero, serie, documento_tipo)`. Retém apenas
grupos com mais de uma ocorrência.
#### `duplicidades_por_serie(notas) -> HashMap<ChaveSerie, Vec<(u64, usize)>>`
Agrupa o resultado de `detectar_duplicidades` por `ChaveSerie`. Cada vetor
é ordenado por `numero` crescente.
Regras:
- O mesmo número em séries diferentes **não** é duplicata.
- O mesmo número com `documento_tipo` diferente **não** é duplicata.
- O mesmo número com mesma série e mesmo `documento_tipo` **é** duplicata.
---
### `detector_sequencia.rs`
#### Constante
```rust
pub const LIMITE_FALTANTES: u64 = 10_000;
```
#### `calcular_intervalo(notas) -> Option<IntervaloSerie>`
Calcula `minimo`, `maximo` e `contagem_faltantes` de forma incremental
(usando `windows(2)`) **sem materializar** a lista de faltantes. Deduplica
números antes do cálculo para não contar duplicatas como faltantes.
#### `detectar_faltantes(notas) -> Vec<u64>`
Materializa a lista completa de faltantes em ordem crescente. Deve ser
chamado apenas após confirmação do usuário quando `calcular_intervalo`
indica que o intervalo excede `LIMITE_FALTANTES`.
#### `agrupar_contiguos(faltantes) -> Vec<(u64, u64)>`
Recebe uma lista **já ordenada** de números faltantes e retorna intervalos
contíguos como pares `(inicio, fim)`. Números isolados têm `inicio == fim`.
Exemplo: `[1, 2, 3, 5, 8, 9]``[(1, 3), (5, 5), (8, 9)]`
---
### `parser_monetario.rs`
#### `parse_valor(input) -> Result<Decimal, ErroValor>`
Faz o parsing de uma string monetária suportando formatos brasileiro e americano.
Rejeita valores negativos.
Algoritmo (RF06):
| Condição | Regra |
|---|---|
| Contém ponto **e** vírgula | Último separador é o decimal |
| Apenas ponto, 3 dígitos após | Separador de milhar (`1.234``1234`) |
| Apenas ponto, outros casos | Decimal (`1000.00`) |
| Apenas vírgula, 3 dígitos após | Separador de milhar (`1,234``1234`) |
| Apenas vírgula, outros casos | Decimal (`1000,00``1000.00`) |
| Sem separador | Número inteiro |
Aceita prefixo `R$` (case-insensitive).
#### `formatar_valor_br(valor) -> String`
Formata `Decimal` para exibição brasileira com 2 casas decimais e pontos de
milhar. Ex: `1234567.89``"1.234.567,89"`.