Etapa 01 — Processador XLS do BI Transporte¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10
Etapa fundacional: define a base (recepção, parsing, persistência, status) que as etapas seguintes estendem. Idempotência → etapa 02; object storage → etapa 03; cadastro veicular → etapa 04.
1. Contexto e escopo¶
Substitui um fluxo manual baseado em script Python por um serviço Java
contínuo que recebe planilhas Excel de transporte da Vantroba, valida e
persiste faturamento e movimento de frota no PostgreSQL (bi_*), com
rastreabilidade por checksum SHA-256 e histórico acessível por API e web.
Dentro do escopo: recepção do .xlsx por API autenticada; parsing SAX das 3
abas fixas; gravação idempotente em bi_faturamento/bi_movimento; máquina de
status do processamento; notificação Telegram; recuperação no startup; telas de
listagem/detalhe.
Fora do escopo: edição dos dados pela web (a origem é a planilha); upload por
tela (o envio é só por API — ver dev/api-rest).
Integrações: PostgreSQL do BI (escrita); PowerSync (leitura pelos clientes móveis — ver etapa 02); Telegram (notificação); Garage (object storage do binário — etapa 03).
2. Contexto do sistema¶
flowchart TB
equipe[Equipe X-Adm]
cliente[Sistema cliente<br/>ERP / job]
subgraph app[bi-transporte-xls]
http[Micronaut<br/>HTTP + Views]
job[Processamento<br/>assíncrono]
end
pg[(PostgreSQL bi_*)]
tg[Telegram Bot API]
equipe -->|GET /processamentos| http
cliente -->|POST /api/xls/processar .zip + Bearer| http
http -->|agenda| job
job -->|JDBC / Flyway| pg
job -->|notificação| tg
http -->|consultas| pg
3. Arquitetura — blocos e dependências¶
Código package-by-feature (decisão
0022): cada feature carrega suas camadas
Controller → Service → Repository; o transversal vai em comum. Dentro da feature,
o controller só fala com service e a entidade não cruza o controller. O ArchUnit
guarda três invariantes: sem ciclos entre features, comum não depende de feature,
processamento.processing não depende da camada HTTP.
| Pacote | Papel |
|---|---|
processamento |
a feature deste app: controllers REST + views, services (recebimento, async, Telegram, recovery, storage), repositórios JDBC, entidades bi_*/xls_*; inclui processamento.processing — leitura Excel SAX (POI streaming; superado pela decisão 0026 — hoje FastExcel) e mapeamento de colunas |
powersync |
reset do PowerSync e o sinal de reset (etapa 05) |
configuracao |
configuração global runtime (admin) |
seguranca |
Bearer estático (/api/**) + login das views (etapa 07) |
comum |
base transversal: comum.config (typed config), comum.exception (só StorageIndisponivelException), comum.util, comum.notificacao (Telegram). A infra web (RFC 7807, exceções HTTP, health, Sentry, versão) vem da lib xadm-comum-web (decisão 0024) |
4. Modelo de dados¶
A tabela de controle do pipeline é xls_processamento (prefixo xls_*,
não sincroniza via PowerSync). Os fatos bi_faturamento/bi_movimento são
criados aqui (colunas-base, aba 20/30); a chave natural/idempotência é tema da
etapa 02 e as colunas veiculares da
etapa 04.
erDiagram
xls_processamento ||--o{ bi_faturamento : "produz no período"
xls_processamento ||--o{ bi_movimento : "produz no período"
xls_processamento {
bigserial id PK
varchar checksum_sha256 UK "SHA-256 do binário; dedup de upload"
date data_referencia
time hora_referencia
date periodo_inicial
date periodo_final
varchar status "PENDENTE|PROCESSANDO|SUCESSO|ERRO"
timestamp recebido_em
timestamp iniciado_em
timestamp concluido_em
bigint tempo_ms
text erro_mensagem
}
xls_processamento — dicionário (grupos de colunas):
- Identidade/recepção:
id(BIGSERIAL PK);checksum_sha256(VARCHAR(64), NOT NULL, UNIQUEuq_xls_processamento_checksum— identidade do upload);nome_arquivo(VARCHAR(255));recebido_em(TIMESTAMP, default NOW());data_referencia(DATE),hora_referencia(TIME) — legado: eram o período informado no upload (decisão 0004), não mais populados desde o envio por lote.zipsemdata/hora(decisão 0016) — ficam nulos em uploads novos;periodo_inicial/periodo_final(DATE) — período real, lidos da aba 10. - Status/execução:
status(VARCHAR(20), default'PENDENTE');iniciado_em,concluido_em(TIMESTAMP);tempo_ms(BIGINT);erro_mensagem(TEXT, truncado a ~2000 chars);log_arquivo(VARCHAR(500)). - Contagens:
registros_faturamento,registros_movimento(INTEGER). - Object storage (etapa 03):
objeto_bucket(VARCHAR(63)),objeto_chave(VARCHAR(512)),objeto_tamanho_bytes(BIGINT);arquivo_bytes(BYTEA — binário no Postgres, modosPSQL/PSQL_GARAGE). - Métricas de idempotência (etapa 02):
8 colunas
faturamento_*/movimento_*(inseridas/atualizadas/inalteradas/removidas). - Defesa veicular (etapa 04): 5 colunas
defesa_*_qtd+xls_legado(BOOLEAN).
bi_faturamento (aba 20) e bi_movimento (aba 30) recebem as colunas-base em
V1__criar_tabelas.sql, em snake_case, espelhando o ERP (todas nullable, PK
id BIGSERIAL). Os índices de consulta (dt_frete, dt_em, placa, filial,
cod_proj, data_lcto, cod_conta) são criados em V2__criar_indices.sql. O
detalhe coluna-a-coluna e a chave natural ficam na etapa 02
(é lá que o schema vira contrato de UPSERT).
5. Contratos¶
5.1 API REST¶
A interface de entrada e leitura é a API REST — contrato completo
(request/response/status/exemplos) em dev/api-rest e
referência OpenAPI. Resumo:
| Método/rota | Auth | Resultado |
|---|---|---|
POST /api/xls/processar |
Bearer (ROLE_API) |
202 lote recebido (conjuntoId/total/itens[], status por arquivo) · 400 zip inválido/vazio/sem .xlsx |
GET /api/transporte/processamentos |
anônimo | lista paginada (page/size/status) |
GET /api/transporte/processamentos/{id} |
anônimo | detalhe |
GET …/{id}/logs · …/{id}/arquivo |
anônimo | log (text/plain) · XLS original |
5.2 Formato do arquivo (contrato com o ERP)¶
.xlsx validado por magic bytes ZIP (50 4B 03 04). Lidas só as abas
nomeadas exatamente "10", "20", "30" (leitura SAX por linha; índices de
coluna 0-based, A=0). Mapeamento mantido em processing/ExcelProcessor:
- Aba 10 (período/config): linha 0, coluna E (4) = data inicial, F (5)
= data final, formato
dd/MM/yyyy. Ausência → erro"Aba 10 ausente"; vazio →"Período incompleto nas células E1/F1". - Aba 20 (faturamento →
bi_faturamento): colunas 1–28 mapeiam os campos do ERP; linha descartada se col 4 (dt_frete) ou col 17 (valor) nulas. Colunas 33–36 (veicular) → etapa 04. - Aba 30 (movimento →
bi_movimento): colunas 1–26; linha descartada se col 16 (data_lcto) ou col 15 (valor) nulas. Colunas 27–30 (veicular) → etapa 04.
O mapeamento índice→coluna completo das abas 20/30 é o contrato de schema; vive versionado no
ExcelProcessore nas fixtures golden (decisão 0003). Mudança de layout pelo cliente = nova fixture + ajuste no parser, no mesmo PR.
6. Fluxos e estados¶
Pipeline assíncrono: o cliente recebe 202 Accepted na hora; o processamento
pesado roda no executor nomeado processamento (pool fixo, 4 threads).
sequenceDiagram
actor C as Cliente
participant TC as XlsController
participant PS as ProcessamentoService
participant PP as Processador (@Async)
participant EX as ExcelProcessor (SAX)
participant R as Repositories
participant TG as TelegramService
C->>TC: POST /api/xls/processar (.zip, multipart + Bearer)
TC->>PS: receberZip(bytes)
PS->>PS: descompacta o .zip · por .xlsx: magic bytes + checksum SHA-256 + dedup
PS->>R: grava cada binário (Garage/Postgres) + linha PENDENTE
PS-->>TC: itens[] (id/httpStatus por arquivo) + conjuntoId
TC-->>C: 202 Accepted (conjuntoId, total, itens[])
Note over PP: assíncrono (pool "processamento")
PP->>EX: lê abas 10 / 20 / 30 (SAX)
EX->>R: UPSERT diff-aware + DELETE seletivo (etapa 02)
PP->>R: SUCESSO + métricas (ou ERRO + mensagem)
PP->>TG: notifica
Máquina de estados do status (string em xls_processamento, 4 valores):
stateDiagram-v2
[*] --> PENDENTE: receber (insert)
PENDENTE --> PROCESSANDO: job inicia (grava iniciado_em)
PROCESSANDO --> SUCESSO: grava concluido_em, tempo_ms, métricas
PROCESSANDO --> ERRO: catch → erro_mensagem
ERRO --> PENDENTE: reprocessar (reset dos campos de execução)
SUCESSO --> [*]
Guard de dupla execução: o job só processa se o status for PENDENTE (senão
ignora). StartupRecoveryService (app.recovery.enabled, default true) retoma
no boot jobs presos em PENDENTE/PROCESSANDO após restart.
7. Configuração¶
| Env | Property | Default | Controla |
|---|---|---|---|
PORT |
micronaut.server.port |
8080 |
porta HTTP |
| — | micronaut.server.multipart.max-file-size |
52428800 (50 MiB) |
tamanho máx. do upload |
| — | micronaut.executors.processamento |
4 threads | pool do processamento async |
API_BEARER_TOKEN |
app.api-token |
(obrigatória) | Bearer do /api/** |
APP_BASE_URL |
app.base-url |
https://excel.vantroba.xadm.biz |
base de links |
| — | app.logs.dir |
logs/execucoes |
log por processamento |
| — | app.recovery.enabled |
true |
recovery no startup |
TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID |
telegram.* |
vazio → desabilita | notificação |
Datasource (DB_USER/DB_PASSWORD/DATASOURCES_DEFAULT_URL) e storage/envs
das demais etapas: ver etapa 03 (Garage),
etapa 05 (PowerSync) e
etapa 07 (auth). Runbook completo de envs:
operacao/deploy.
8. Decisões¶
| Nº | Decisão |
|---|---|
| 0001 | Micronaut Data JDBC, sem Hibernate |
| 0002 | Processar XLS (POI streaming/SAX) |
| 0003 | Contrato canônico Python↔Java |
| 0004 | Período de referência vem do upload |
9. Qualidade e observabilidade¶
- Log por processamento em
logs/execucoes/processamento-{id}.log(exposto emGET …/{id}/logs). - Telegram notifica sucesso/erro com link para o detalhe.
- Bugsink/Sentry (
SENTRY_DSN) captura exceções; defesa de parsing reporta sem derrubar o lote. - Métricas finas por upload em
xls_processamento(etapa 02).
10. Riscos¶
- Planilha fora do layout → parsing tolerante por linha; o erro vira
status=ERRO - log, nunca derruba o serviço.
- Divergência parse Java × Python legado → contrato canônico + fixtures golden (decisão 0003).
- Vazamento do
API_BEARER_TOKEN→ secrets no Coolify, HTTPS obrigatório. Rotação: gerar novo (openssl rand -hex 32), atualizar no Coolify + redeploy, avisar integradores. Não há janela de dois tokens — troca atômica.