8.9 KiB
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.
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()→truese qualquer contador > 0linhas_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.).
pub struct ChaveSerie {
pub serie: String,
pub documento_tipo: Option<String>,
}
- Deriva
Hash,Eq,Ord— usada como chave emHashMape 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.
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
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.
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
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.
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()→truese não há faltantes nem duplicatastotal_faltantes()→ soma do tamanho de todas as listas de faltantestotal_duplicatas()→ soma do número de grupos de duplicatas
layout.rs
Entidades de configuração de layout de arquivo.
TipoArquivo
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)
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_tipodiferente não é duplicata. - O mesmo número com mesma série e mesmo
documento_tipoé duplicata.
detector_sequencia.rs
Constante
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".