Etapa 01 — Processador XLS do BI Comercial¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10
Correção 2026-07-16: a porta de ingestão descrita nesta etapa (
POST /api/xls/processar, um.zippor envio, com o lote identificado no servidor opcionais) foi substituída por uma porta única.zip—POST /api/xls/processar, campoarquivo, sem nenhum outro campo. Ver o estado atual no livro. O restante desta etapa (detecção de tipo, máquina de estados, idempotência) segue válido.Etapa fundacional: define a base (recepção, detecção de tipo, parsing, persistência, status) que as etapas seguintes estendem. Idempotência → etapa 02; conformidade com o legado Python → etapa 03; object storage do binário → etapa 04.
1. Contexto e escopo¶
Substitui um fluxo manual baseado em scripts Python por um serviço Java
contínuo que recebe planilhas Excel comerciais da OnPetro (vendas, margens,
metas, cadastros de postos/TRRs), detecta automaticamente o tipo do arquivo,
valida e persiste em PostgreSQL (bi_* para BI sincronizado, xls_* para
metadados internos), 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 (Bearer);
detecção do tipo via ArquivoDetector (9 tipos, incluindo COMPRAS e COTA_PETROBRAS); parsing
StAX por ExcelSheetReader (FastExcel); gravação idempotente pelos 9 processadores; máquina de
estados do processamento; coordenação de lotes (conjunto_id); notificação
Telegram; recuperação no startup; telas de listagem/detalhe.
Fora do escopo: informar o tipo no upload (é detectado do conteúdo — §3.1); edição dos dados pela web (a origem é a planilha); período no POST (vem do conteúdo, diferente do projeto irmão Transporte).
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 04).
2. Contexto do sistema¶
flowchart TB
equipe[Equipe X-Adm]
cliente[Sistema cliente<br/>ERP / job / script Python]
subgraph app[bi-comercial-xls]
direction TB
http[Micronaut<br/>HTTP + Views]
det[ArquivoDetector]
job[Processamento<br/>assíncrono]
end
pg[(PostgreSQL bi_* / xls_*)]
tg[Telegram Bot API]
equipe -->|GET /processamentos| http
cliente -->|POST /api/xls/processar + Bearer| http
http --> det
http -->|agenda| job
job -->|JDBC / Flyway| pg
job -->|notificação| tg
http -->|consultas| pg
3. Arquitetura — blocos e dependências¶
Código package-by-layer (alinhamento cross-project com o bi-transporte;
decisão consciente — ver a decisão 0030). O ArchUnit guarda os invariantes:
sem ciclos entre pacotes top-level, processing não depende de api,
controllers não dependem de repositórios.
| Pacote | Papel |
|---|---|
api |
Controllers HTTP (REST + views JTE), DTOs expostos |
service |
Recepção (ProcessamentoService), processamento assíncrono (ProcessamentoProcessador), coordenação de lotes (ConjuntoLoteService), consultas, nuke, Telegram, recovery |
processing |
Leitura Excel StAX/FastExcel (ExcelSheetReader), detecção (ArquivoDetector + TipoArquivo), 9 processadores especializados, BulkUpsert |
repository |
Acesso JDBC / Micronaut Data (decisão 0007), repositórios por tabela |
domain |
Entidades mapeadas (bi_* PK UUID v7; xls_* PK BIGSERIAL) |
storage |
Object storage (etapa 04) |
security |
Bearer estático + login Firebase (etapa 06) |
exception (top-level) |
Exceções de domínio → HTTP 4xx/5xx (decisão 0012) |
3.1 Detecção automática de tipo¶
O ArquivoDetector (@Singleton) recebe os bytes + o nome do arquivo e devolve
um TipoArquivo — sem o cliente precisar informar nada. A detecção é
determinística e ordenada por prioridade (a primeira condição satisfeita
ganha), decisão 0010:
| Ordem | Tipo | Critério | Grava |
|---|---|---|---|
| 0 | COMPRAS |
aba chamada "Compras" (conteúdo; antes das regras por nome) | bi_fornecedor + bi_compra |
| 1 | META_FERNANDO |
sheet "Meta por vendedor" ou coluna "Tipo de meta" | bi_meta (por vendedor) |
| 2 | POSTOS |
coluna "Vinculação a Distribuidor" | bi_posto (upsert por CNPJ) |
| 3 | TRR |
coluna "Tipo de Instalação" ou "Qualificação da Empresa" | bi_trr (upsert por CNPJ) |
| 4 | RELATORIO18 |
nome contém relatorio/relatório/rel 18 |
bi_movimento + cadastros |
| 5 | MARGEM_CONSOLIDADA |
nome contém margem consolidada |
bi_custo_inventario + bi_movimento |
| 6 | MARGEM_DIA |
nome contém margem - |
bi_movimento (variante diária) |
| 7 | META |
nome contém meta (e nenhuma acima bateu) |
bi_meta (por período + vendedor) |
- Nada bateu (tipo desconhecido) →
BadRequestException→ HTTP 400 na recepção; o arquivo não é persistido (a detecção é síncrona, antes de enfileirar). Não confundir comIGNORADO(§6): esse é um tipo detectado que o processador pula por regra de negócio (período ≤ 10/08/2025). - Nomes iniciando com
~$(temporário do Excel) ou#(revisão interna) →BadRequestException(400), não entram emxls_processamento. - Detalhe e receita para adicionar um tipo novo:
a decisão
0010.
4. Modelo de dados¶
O controle do pipeline é xls_processamento (xls_*, não sincroniza via
PowerSync). Os fatos e dimensões bi_* são o tema da
etapa 02 (é lá que o schema vira contrato
de UPSERT).
erDiagram
xls_processamento ||--o{ bi_movimento : "produz no período"
xls_conjunto_lote ||--o{ xls_processamento : "agrupa lote"
xls_processamento {
bigserial id PK
varchar checksum_sha256 UK "SHA-256; dedup de upload"
varchar tipo_arquivo "detectado pelo ArquivoDetector"
varchar status "PENDENTE|PROCESSANDO|SUCESSO|ERRO|IGNORADO"
date periodo_inicial
date periodo_final
varchar conjunto_id "FK lote (nullable)"
timestamp recebido_em
bigint tempo_ms
integer linhas_efetivas
integer linhas_removidas
}
xls_conjunto_lote {
varchar conjunto_id PK
int total
int duplicados
timestamp notificado_em
}
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, decisão 0006);nome_arquivo;recebido_em(default NOW());tipo_arquivo(detectado);periodo_inicial/periodo_final(lidos do conteúdo);conjunto_id(nullable). - Status/execução:
status(default'PENDENTE');iniciado_em,concluido_em;tempo_ms;erro_mensagem(TEXT);registros_processados;log_arquivo. - Object storage (etapa 04):
objeto_bucket,objeto_chave,objeto_tamanho_bytes;arquivo_bytes(BYTEA). - Métricas de idempotência (etapa 02):
linhas_efetivas,linhas_removidas(2 colunas agregadas;NULL= upload anterior à spec de métricas).
Herança do histórico: as tabelas nasceram sem prefixo (V1), ganharam prefixo
bi_*/xls_*(V5/V6) e asbi_*migraram para PKid UUID v7(V7). A tabela de controle passou a se chamarxls_processamento; o lote,xls_conjunto_lote.
5. Contratos¶
5.1 API REST¶
| Método/rota | Auth | Resultado |
|---|---|---|
POST /api/xls/processar |
Bearer (ROLE_API) |
202 aceito (com lote e itens) · 400 zip inválido/tipo desconhecido · 401 Bearer inválido · 503 storage |
GET /api/comercial/processamentos |
anônimo | lista paginada (page/size/status) |
GET …/{id} · …/{id}/logs |
anônimo | detalhe (JSON, inclui métricas) · log (text/plain) |
O POST não exige período (vem do conteúdo) e aceita conjunto_id +
conjunto_total opcionais para coordenar lotes (decisão
0011). Contrato completo e
exemplos curl/Python: o contrato da API REST.
5.2 Formato do arquivo (contrato com o ERP)¶
.xlsx validado por magic bytes ZIP. A leitura usa ExcelSheetReader (FastExcel-reader,
StAX streaming) — não carrega a planilha inteira em memória. O mapeamento de colunas
vive em cada {Tipo}Processor e nas fixtures golden
(etapa 03): 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 (com o id e o
tipoArquivo detectado); o processamento pesado roda no executor processamento
(pool fixo, 4 threads).
sequenceDiagram
actor C as Cliente
participant CC as ComercialController
participant PS as ProcessamentoService
participant AD as ArquivoDetector
participant PP as Processador (@Async)
participant PR as {Tipo}Processor
participant TG as TelegramService
C->>CC: POST /processar (multipart + Bearer)
CC->>PS: receber(bytes, conjunto?)
PS->>PS: magic bytes + checksum SHA-256 + dedup
PS->>AD: detectar tipo
PS->>PS: grava binário (Garage/Postgres) + linha PENDENTE
PS-->>CC: 202 + id + tipoArquivo
CC-->>C: 202 Accepted
Note over PP: assíncrono (pool "processamento")
PP->>PR: processa (UPSERT diff-aware + DELETE seletivo, etapa 02)
PP->>PP: SUCESSO + métricas (ou ERRO / IGNORADO)
PP->>TG: notifica (agregada se lote)
Máquina de estados do status:
stateDiagram-v2
[*] --> PENDENTE: receber (insert)
PENDENTE --> PROCESSANDO: job inicia
PROCESSANDO --> SUCESSO: métricas + tempo_ms
PROCESSANDO --> ERRO: catch → erro_mensagem
PROCESSANDO --> IGNORADO: período ≤ 10/08/2025 (data mínima)
ERRO --> PENDENTE: reprocessar (botão na tela)
SUCESSO --> [*]
IGNORADO --> [*]
ERRO → PENDENTE é a única transição disparada por gente, pelos botões
Reprocessar da tela de detalhe (o envio, o lote dele) e da lista filtrada por
ERRO (todos). Reenfileirar é seguro porque o .xlsx continua no storage e o
upsert é diff-aware — o que não seria seguro é reenfileirar algo em andamento,
e é por isso que o guard mora no WHERE … AND status = 'ERRO' do
resetarParaPendenteSeErro, não no th:if do template: o botão escondido é
conforto de UI, a trava é o SQL. Fora de ERRO a rota responde 409. Em dev, o
botão aparece para qualquer status (refazer um SUCESSO durante o
desenvolvimento) e usa o reset sem guard.
Guard de dupla execução: o job só processa PENDENTE. StartupRecoveryService
retoma no boot jobs presos em PENDENTE/PROCESSANDO após restart.
6.1 Coordenação de lotes¶
Quando o cliente envia N arquivos com o mesmo conjunto_id (+ conjunto_total=N),
o ConjuntoLoteService registra em xls_conjunto_lote; ao completar os N
(processados + duplicados), dispara uma notificação Telegram agregada com o
resumo de efetivas/removidas por tipo.
7. Configuração¶
| Env | Property | Default | Controla |
|---|---|---|---|
PORT |
micronaut.server.port |
8080 |
porta HTTP |
| — | micronaut.server.multipart.max-file-size |
50 MB | tamanho máx. do upload |
| — | micronaut.executors.processamento |
4 threads | pool assíncrono |
BI_COMERCIAL_XLS_API_TOKEN |
app.api-token |
(obrigatória) | Bearer do /api/** |
| — | app.recovery.enabled |
true |
recovery no startup |
TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID |
telegram.* |
vazio → desabilita | notificação |
Datasource (DATASOURCES_DEFAULT_URL/USERNAME/PASSWORD) e envs das demais
etapas: etapa 04 (Garage),
etapa 05 (PowerSync), etapa 06
(auth). Runbook completo: Deploy.
8. Decisões¶
| Nº | Decisão |
|---|---|
| 0001 | Micronaut vs Spring Boot |
| 0007 | Micronaut Data JDBC, sem Hibernate |
| 0010 | Detecção automática do tipo de arquivo |
| 0011 | Coordenação de lotes via conjunto_id |
| 0012 | Exceções top-level → HTTP |
9. Riscos¶
- Planilha fora do layout → parsing tolerante; erro vira
status=ERRO+ log, nunca derruba o serviço. - Tipo detectado errado → linhas na tabela errada; mitigado pela heurística
ordenada +
ArquivoDetectorTest(cobre as prioridades por tipo + negativos). - Divergência parse Java × Python legado → etapa 03 (golden-master + conformidade em produção).
- Vazamento do
BI_COMERCIAL_XLS_API_TOKEN→ secrets no Coolify, HTTPS obrigatório; rotação é troca atômica (sem janela de dois tokens).
Correção 2026-09-14: o ponteiro para o
CLAUDE.md/HOWTO-DEPLOY.mdpassou para a página dona do fato; os dois arquivos deixaram de carregar esse conteúdo.