atualiza versão do PRD para 1.6 e detalha arquitetura e estrutura de pastas do projeto

This commit is contained in:
2026-03-02 21:39:05 -03:00
parent 9d212747e7
commit 31cd94907a
2 changed files with 613 additions and 12 deletions
+429
View File
@@ -0,0 +1,429 @@
# Roteiro de Implementação — Comparador de Notas
**Versão:** 1.0
**Data:** 02/03/2026
**Baseado no PRD:** v1.6
---
## Fase 0 — Fundação do Projeto
**Objetivo:** Estrutura compilável com todas as dependências.
### 0.1 Configurar `Cargo.toml`
Adicionar todas as dependências:
```toml
[dependencies]
eframe = "0.31"
egui = "0.31"
rusqlite = { version = "0.32", features = ["bundled"] }
csv = "1.3"
calamine = "0.26"
rust_decimal = { version = "1.36", features = ["serde"] }
rust_decimal_macros = "1.36"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
genpdf = "0.2"
chrono = { version = "0.4", features = ["serde"] }
encoding_rs = "0.8"
dirs = "5"
thiserror = "2"
```
### 0.2 Criar estrutura de pastas completa
Conforme definido na seção 9.1 do PRD:
```
src/
main.rs
ui/
mod.rs
app.rs
screens/
import.rs
configuracao_colunas.rs
layouts.rs
resultado.rs
application/
mod.rs
usecases/
importar_arquivo.rs
executar_analise.rs
exportar_pdf.rs
domain/
mod.rs
errors.rs
entities/
nota.rs
serie.rs
layout.rs
resultado_analise.rs
services/
detector_sequencia.rs
detector_duplicidade.rs
parser_monetario.rs
infrastructure/
mod.rs
csv_reader.rs
xlsx_reader.rs
pdf_generator.rs
sqlite/
mod.rs
connection.rs
migrations.rs
layout_repository.rs
```
### 0.3 Criar todos os `mod.rs` com declarações vazias
Garantir que o projeto compila antes de começar a implementar.
---
## Fase 1 — Camada Domain (núcleo puro)
**Dependência:** Fase 0 concluída.
**Restrição:** Zero dependência de egui, rusqlite, calamine, csv ou genpdf. Apenas Rust puro + `rust_decimal` e `chrono`.
### 1.1 `domain/errors.rs`
Definir tipos de erro com `thiserror`:
- `ErroNumero` (Zero, NaoNumerico)
- `ErroSerie` (Invalida, Vazia)
- `ErroValor` (Negativo, NaoNumerico)
- `ErroLayout` (CampoObrigatorioAusente, JsonMalformado, NomeConflitante)
### 1.2 `domain/entities/nota.rs`
```rust
pub struct Nota {
pub numero: u64,
pub serie: String,
pub valor: Option<Decimal>,
pub data: Option<NaiveDate>,
}
```
### 1.3 `domain/entities/serie.rs`
- Validação da regex `[0-9]{1,3}`
- Função `validar_serie(s: &str) -> Result<String, ErroSerie>`
### 1.4 `domain/entities/layout.rs`
- `enum TipoArquivo { Csv, Xlsx }`
- `struct LayoutCsv` com todos os campos da seção 10.4 do PRD
- `struct LayoutXlsx` com todos os campos da seção 10.4 do PRD
- `enum Layout { Csv(LayoutCsv), Xlsx(LayoutXlsx) }`
### 1.5 `domain/entities/resultado_analise.rs`
```rust
pub struct ResultadoPreAnalise {
// min, max, contagem_faltantes por série
pub intervalos_por_serie: HashMap<String, (u64, u64, usize)>,
}
pub struct ResultadoAnalise {
pub faltantes_por_serie: HashMap<String, Vec<u64>>,
pub duplicadas_por_serie: HashMap<String, Vec<(u64, usize)>>,
pub soma_total: Decimal,
pub soma_por_serie: HashMap<String, Decimal>,
}
```
### 1.6 `domain/services/parser_monetario.rs`
Implementar exatamente o algoritmo da seção RF06:
| Regra | Condição | Comportamento |
|-------|----------|---------------|
| 1 | Contém ponto **e** vírgula | Último separador é o decimal |
| 2a | Apenas um separador + exatamente 2 dígitos após | Separador decimal |
| 2b | Apenas um separador + exatamente 3 dígitos após | Separador de milhar |
| 2c | Apenas um separador + outros casos | Separador decimal |
| 3 | Sem separador | Número inteiro |
- Rejeitar valores negativos (precedidos de `-`)
- Armazenar como `rust_decimal::Decimal`
> **Prioridade:** Testar exaustivamente com todos os exemplos da tabela do PRD antes de avançar.
### 1.7 `domain/services/detector_sequencia.rs`
- Recebe `Vec<&Nota>` de uma série
- Ordena por `numero` crescente
- Percorre **incrementalmente** (sem lista intermediária)
- Retorna `Vec<u64>` de faltantes
- Respeitar o limite de 10.000 registros faltantes (RFC04): retornar `ResultadoPreAnalise` antes de expandir
### 1.8 `domain/services/detector_duplicidade.rs`
- Recebe `Vec<&Nota>`
- Retorna `HashMap<(u64, String), usize>` com contagem por grupo
- Filtra apenas grupos com contagem > 1
---
## Fase 2 — Camada Infrastructure
**Dependência:** Fase 1 concluída.
### 2.1 `infrastructure/sqlite/connection.rs`
- Determinar caminho do banco conforme SO via `dirs::config_dir()`:
- Linux: `~/.config/comparador-notas/config.db`
- Windows: `%APPDATA%\comparador-notas\config.db`
- macOS: `~/Library/Application Support/comparador-notas/config.db`
- Criar diretório automaticamente se não existir
- Abrir conexão SQLite
- Tratamento de banco corrompido: renomear para `config.db.bak`, recriar banco vazio
### 2.2 `infrastructure/sqlite/migrations.rs`
- Tabela `schema_version` para controle de versão do schema
- Migration v1: criar tabela `layouts` com todos os campos da seção 10.4 do PRD
- Aplicar migrations automaticamente na inicialização
### 2.3 `infrastructure/sqlite/layout_repository.rs`
```rust
fn salvar(layout: &Layout) -> Result<i64>
fn listar() -> Result<Vec<Layout>>
fn buscar_por_id(id: i64) -> Result<Option<Layout>>
fn excluir(id: i64) -> Result<()>
fn existe_nome(nome: &str) -> Result<bool>
```
### 2.4 `infrastructure/csv_reader.rs`
- Suporte a delimitadores: `,` `;` `\t`
- Suporte a encoding: UTF-8 e Windows-1252 (via `encoding_rs`)
- Respeitar linha de cabeçalho configurável (base 1)
- Ignorar linhas em branco silenciosamente
- Coletar linhas malformadas para relatório consolidado
- Validar limite de 50 MB antes de ler
- Retornar `Vec<Vec<String>>` (linhas × colunas)
### 2.5 `infrastructure/xlsx_reader.rs`
- `listar_abas(path) -> Vec<String>` — chamado imediatamente após seleção do arquivo
- Ler dados da aba selecionada a partir da posição `LetraLinha`
- Converter notação `LetraLinha` (ex: `B3`) para `(col_idx, row_idx)`
- Ignorar linhas em branco silenciosamente
- Validar limite de 50 MB
- Tratar arquivo corrompido retornando erro tipado
### 2.6 `infrastructure/pdf_generator.rs`
- Implementar trait `PdfGenerator` definida em `application/`
- Gerar PDF com `genpdf` contendo:
- Metadados: nome do arquivo importado, data/hora de geração, nome do layout
- Faltantes agrupados por série
- Duplicatas agrupadas por série
- Totais por série e total geral
---
## Fase 3 — Camada Application
**Dependência:** Fases 1 e 2 concluídas.
### 3.1 `application/usecases/importar_arquivo.rs`
1. Validar tamanho do arquivo (recusar > 50 MB)
2. Detectar tipo (CSV ou XLSX)
3. Para XLSX: chamar `listar_abas()` e retornar lista para UI fazer a seleção
4. Chamar leitor adequado com configurações do layout
5. Mapear colunas e construir `Vec<Nota>`
6. Aplicar validações: regex de série, numero zero, valores inválidos
7. Retornar notas válidas + relatório de avisos consolidado (RF06/RNF06)
### 3.2 `application/usecases/executar_analise.rs`
1. Receber `Vec<Nota>`
2. Chamar `parser_monetario` para cada valor
3. Agrupar por série
4. Chamar `detector_sequencia` por série → gerar `ResultadoPreAnalise`
5. Se algum intervalo > 10.000: retornar `ResultadoPreAnalise` para UI solicitar confirmação
6. Após confirmação: expandir faltantes e montar `ResultadoAnalise`
7. Chamar `detector_duplicidade`
8. Calcular somas com `Decimal` (nunca `f64`)
9. Retornar `ResultadoAnalise`
### 3.3 `application/usecases/exportar_pdf.rs`
- Depende da trait `PdfGenerator` (não de `genpdf` diretamente)
- Recebe `ResultadoAnalise` + metadados do arquivo e layout
- Delega geração para a implementação concreta em infrastructure
### 3.4 Gerenciamento de layouts
- `salvar_layout` — valida campos obrigatórios antes de persistir
- `listar_layouts` — lista separada por tipo (CSV / XLSX)
- `carregar_layout` — busca por id
- `excluir_layout` — remove do banco
- `exportar_layout_json` — serializa com `serde_json`, sugere nome do arquivo
- `importar_layout_json` — desserializa + valida campos obrigatórios + verifica conflito de nome
---
## Fase 4 — Camada UI (egui/eframe)
**Dependência:** Fase 3 concluída.
**Regra:** Nenhuma regra de negócio, parsing ou cálculo dentro da UI.
### 4.1 `ui/app.rs` — Estrutura principal
```rust
enum EstadoApp {
Importando,
ConfigurandoColunas,
SelecionandoAba(Vec<String>), // lista de abas XLSX
Analisando,
ConfirmandoIntervalo(ResultadoPreAnalise),
ExibindoResultado(ResultadoAnalise),
GerenciandoLayouts,
}
```
- Gerenciamento de modais/popups bloqueantes (RNF06)
- Struct `App` com estado compartilhado e referência ao banco SQLite
### 4.2 `ui/screens/import.rs`
- Botão "Selecionar arquivo" com diálogo nativo
- Exibir nome do arquivo selecionado
- Para XLSX: exibir dropdown de seleção de aba **imediatamente** após seleção do arquivo
- Dropdown de layouts salvos para carregar configuração existente
- Botão "Gerenciar Layouts" → navegar para `layouts.rs`
### 4.3 `ui/screens/configuracao_colunas.rs`
- Para CSV: campos numéricos para índice de cada coluna (base 0) + delimitador + encoding + linha cabeçalho
- Para XLSX: campos de texto para posição `LetraLinha` de cada coluna
- Validação em tempo real: índices duplicados, campos obrigatórios ausentes
- Bloqueio do botão "Executar Análise" quando configuração inválida
- Botão "Executar Análise"
### 4.4 `ui/screens/layouts.rs`
- Listar layouts salvos separados por tipo (CSV / XLSX)
- Campo de nome + botão "Salvar layout atual"
- Botão "Excluir" com modal de confirmação
- Botão "Exportar para JSON" → diálogo de salvar com nome sugerido baseado no nome do layout
- Botão "Importar de JSON" → diálogo de abertura
- Modal de conflito de nome ao importar: "Sobrescrever" ou "Salvar com novo nome"
### 4.5 `ui/screens/resultado.rs`
- Seções: **Faltantes** | **Duplicatas** | **Totais**
- Agrupamento por série em todas as seções
- Paginação com dropdown: 50 / 100 / 200 / 1000 itens por página
- Valores monetários formatados em pt-BR (ex: `1.234,56`)
- Números de notas sem zeros à esquerda (ex: `0001` exibido como `1`)
- Botão "Exportar PDF" → chamar use case `exportar_pdf`
### 4.6 Modais globais (RNF06)
| Modal | Trigger |
|-------|---------|
| Erro genérico | Qualquer erro com mensagem + botão OK |
| Aviso consolidado | Final de importação com lista de categorias de problema |
| Confirmação de intervalo | Faltantes > 10.000 por série |
| Confirmação de exclusão | Excluir layout |
| Conflito de nome | Importar JSON com nome existente |
| Banco corrompido | Inicialização com `config.db` inválido |
---
## Fase 5 — Testes
**Dependência:** Fases 14 concluídas (testes unitários podem ser escritos junto com cada fase).
### 5.1 Testes unitários — Domain
- `parser_monetario`: todos os casos da tabela do PRD + casos extremos
- `detector_sequencia`: sequência completa, com faltantes, série com um único registro, intervalos > 10.000
- `detector_duplicidade`: sem duplicatas, com duplicatas, múltiplas séries
- Validação de série (regex `[0-9]{1,3}`): válidos e inválidos
- Validação de numero zero: deve ser descartado
### 5.2 Testes de integração — Infrastructure
- Leitura CSV: UTF-8, Windows-1252, diferentes delimitadores, linhas malformadas, arquivo > 50 MB
- Leitura XLSX: aba correta, posição `LetraLinha`, arquivo corrompido, limite de tamanho
- SQLite: migrations, CRUD de layouts, banco corrompido → recriação
### 5.3 Testes de integração — Application
- Fluxo completo CSV → análise → resultado
- Fluxo completo XLSX → análise → resultado
- Importação de layout JSON: válido, malformado, conflito de nome com sobrescrita, conflito com renomeação
- Intervalo > 10.000: verificar que `ResultadoPreAnalise` é retornado antes da expansão
### 5.4 Testes manuais de UI
- Todas as mensagens em PT-BR
- Formatação monetária correta (pt-BR)
- Paginação funcionando
- Geração do PDF com metadados corretos
- Fluxo XLSX: aba exibida imediatamente após seleção do arquivo
---
## Fase 6 — Polimento e Empacotamento
### 6.1 Strings e idioma
- Revisar todas as mensagens de erro/aviso para PT-BR (RNF05)
- Garantir que nenhuma string de erro interna vaze para a UI como texto bruto
### 6.2 Performance
- Validar com arquivo de 100.000 registros (RNF03)
- Confirmar que detecção de faltantes é incremental (sem lista intermediária antes da confirmação)
- Medir tempo de importação e análise
### 6.3 Build multiplataforma
- Linux: validar caminho `~/.config/comparador-notas/config.db`
- Windows: validar caminho `%APPDATA%\comparador-notas\config.db`
- macOS: validar caminho `~/Library/Application Support/comparador-notas/config.db`
### 6.4 Empacotamento
- Configurar ícone da aplicação
- Build de release otimizado: `cargo build --release`
- Testar executável standalone (sem instalação, sem dependências externas)
---
## Ordem de Execução Recomendada
```
Fase 0 → Fase 1 (com testes unitários) → Fase 2.1/2.2/2.3 → Fase 2.4/2.5
→ Fase 3.1/3.2 → Fase 4.1/4.2/4.3/4.5 → Fase 2.6/3.3 → Fase 3.4/4.4
→ Fase 5 → Fase 6
```
> O núcleo de domínio deve estar **totalmente testado** antes de qualquer integração. O `parser_monetario` e o `detector_sequencia` são os componentes mais críticos do sistema.
---
## Dependências entre Componentes
```
domain/errors.rs
└─ domain/entities/ (nota, serie, layout, resultado_analise)
└─ domain/services/ (parser_monetario, detector_sequencia, detector_duplicidade)
└─ application/usecases/ (importar_arquivo, executar_analise, exportar_pdf)
└─ infrastructure/ (csv_reader, xlsx_reader, sqlite/, pdf_generator)
└─ ui/ (app, screens/)
```
Nenhuma camada pode importar de uma camada acima dela na hierarquia.
+184 -12
View File
@@ -1,6 +1,6 @@
# PRD — Comparador de Notas # PRD — Comparador de Notas
**Versão:** 1.5 **Versão:** 1.6
**Data:** 02/03/2026 **Data:** 02/03/2026
**Status:** Planejamento **Status:** Planejamento
@@ -468,22 +468,194 @@ Fluxo principal:
--- ---
# 9. Arquitetura Inicial Sugerida # 9. Arquitetura
Arquitetura desktop local. Arquitetura desktop local com separação em quatro camadas: `domain`, `application`, `infrastructure` e `ui`.
Componentes: Não é Clean Architecture radical. É apenas separação suficiente para manter fronteiras claras, domínio isolado e infraestrutura concreta sem vazar para a lógica de negócio.
* Interface gráfica
* Módulo de importação
* Módulo de processamento
* Módulo de configuração
Sem dependências externas obrigatórias.
--- ---
## 9.1 Stack Tecnológica ## 9.1 Estrutura de Pastas
```
src/
├─ main.rs
├─ ui/
│ ├─ mod.rs
│ ├─ app.rs
│ ├─ screens/
│ │ ├─ import.rs
│ │ ├─ configuracao_colunas.rs
│ │ ├─ layouts.rs
│ │ ├─ resultado.rs
├─ application/
│ ├─ mod.rs
│ ├─ usecases/
│ │ ├─ importar_arquivo.rs
│ │ ├─ executar_analise.rs
│ │ ├─ exportar_pdf.rs
├─ domain/
│ ├─ mod.rs
│ ├─ errors.rs
│ ├─ entities/
│ │ ├─ nota.rs
│ │ ├─ serie.rs
│ │ ├─ layout.rs
│ │ ├─ resultado_analise.rs
│ │
│ ├─ services/
│ │ ├─ detector_sequencia.rs
│ │ ├─ detector_duplicidade.rs
│ │ ├─ parser_monetario.rs
├─ infrastructure/
│ ├─ mod.rs
│ ├─ csv_reader.rs
│ ├─ xlsx_reader.rs
│ ├─ pdf_generator.rs
│ ├─ sqlite/
│ │ ├─ mod.rs
│ │ ├─ connection.rs
│ │ ├─ migrations.rs
│ │ ├─ layout_repository.rs
```
---
## 9.2 Papel de Cada Camada
### Domain (núcleo puro)
Contém toda a lógica de negócio real.
**Não pode depender de:**
* egui
* rusqlite
* calamine
* csv
* genpdf
Apenas Rust puro + crates matemáticas (`rust_decimal`, `chrono`).
#### Entidades
**`Nota`**
| Campo | Tipo |
| ------ | ----------------- |
| numero | `u64` |
| serie | `String` |
| valor | `Option<Decimal>` |
| data | `Option<NaiveDate>` |
**`ResultadoAnalise`**
| Campo | Tipo |
| -------------------- | -------------------------------------- |
| faltantes_por_serie | `HashMap<String, Vec<u64>>` |
| duplicadas_por_serie | `HashMap<String, Vec<(u64, usize)>>` |
| soma_total | `Decimal` |
| soma_por_serie | `HashMap<String, Decimal>` |
> **Importante:** a geração dos faltantes não deve ser eager. O use case `executar_analise` deve primeiro calcular os intervalos por série e retornar um resultado intermediário (`ResultadoPreAnalise`) contendo o intervalo calculado. Somente após confirmação do usuário — quando algum intervalo exceder 10.000 registros (RF04) — o sistema expande e materializa a lista completa de faltantes. Isso evita alocar memória para intervalos gerados por mapeamento incorreto de colunas.
#### Services
`detector_sequencia` — recebe `Vec<Nota>` agrupadas por série, retorna faltantes.
`detector_duplicidade` — retorna mapa de contagem por `(numero, serie)`.
`parser_monetario` — implementa exatamente o algoritmo definido no RF06.
#### Erros
`domain/errors.rs` define os erros do domínio de forma tipada (ex: `ErroSerie::Invalida`, `ErroNumero::Zero`, `ErroValor::Negativo`). Nenhuma camada deve propagar `String` livre como erro de domínio.
---
### Application (orquestração)
Coordenam o fluxo entre domain e infrastructure.
Conhece o domain. O domain não conhece o application.
**`executar_analise.rs`**
1. Recebe dados crus
2. Chama `parser_monetario`
3. Chama `detector_sequencia`
4. Chama `detector_duplicidade`
5. Monta `ResultadoAnalise`
**`exportar_pdf.rs`**
Depende de uma trait abstrata (`PdfGenerator`) definida no próprio módulo application. A implementação concreta fica em `infrastructure/pdf_generator.rs`. Isso evita que o application dependa diretamente de `genpdf`.
---
### Infrastructure (implementações concretas)
Implementa leitores, persistência e geração de arquivos.
| Arquivo | Responsabilidade |
| -------------------------------- | ----------------------------------------- |
| `csv_reader.rs` | Leitura de arquivos CSV via `csv` |
| `xlsx_reader.rs` | Leitura de arquivos XLSX via `calamine` |
| `pdf_generator.rs` | Geração de PDF via `genpdf` |
| `sqlite/connection.rs` | Abertura e inicialização da conexão SQLite |
| `sqlite/migrations.rs` | Aplicação de migrations de schema |
| `sqlite/layout_repository.rs` | CRUD de layouts via `rusqlite` |
Nada de infrastructure sobe para domain.
#### Por que `layout_repository.rs` dentro de `sqlite/`
Manter o repositório dentro de `sqlite/` concentra todos os artefatos SQLite em um único módulo. Se futuramente o sistema armazenar histórico de análises ou configurações do usuário (RF14/10.8), novos repositórios são adicionados no mesmo lugar sem dispersão.
---
### UI (interface)
Apenas coleta input, chama use cases e renderiza resultado.
Nenhuma regra de sequência ou parsing monetário deve estar na camada de UI.
#### Screens
| Arquivo | Responsabilidade |
| ------------------------- | ----------------------------------------------------- |
| `import.rs` | Seleção de arquivo e configurações de importação |
| `configuracao_colunas.rs` | Mapeamento de colunas (RF02) |
| `layouts.rs` | Gerenciamento de layouts: salvar, carregar, excluir (RF08) |
| `resultado.rs` | Exibição de resultados com paginação (RF07) |
> `configuracao_colunas.rs` e `layouts.rs` são mantidos separados porque tratam de responsabilidades distintas do RF02 e RF08, evitando que uma única screen acumule lógica de mapeamento de colunas e gerenciamento de persistência.
---
## 9.3 Fluxo de Execução
```
UI → Application → Domain
Infrastructure entra apenas quando necessário.
```
Exemplo real:
1. UI chama `importar_arquivo`
2. Infrastructure lê CSV/XLSX
3. Application transforma registros em entidades `Nota`
4. Domain executa análise
5. Application retorna `ResultadoAnalise`
6. UI renderiza
---
## 9.4 Stack Tecnológica
| Camada | Tecnologia | Status | | Camada | Tecnologia | Status |
| ---------------- | ------------------ | -------------- | | ---------------- | ------------------ | -------------- |