Pular para conteúdo

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, UNIQUE uq_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 .zip sem data/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, modos PSQL/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 ExcelProcessor e 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 em GET …/{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.