Pular para conteúdo

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 .zip por envio, com o lote identificado no servidor opcionais) foi substituída por uma porta única .zip — POST /api/xls/processar, campo arquivo, 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 com IGNORADO (§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 em xls_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, UNIQUE uq_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 as bi_* migraram para PK id UUID v7 (V7). A tabela de controle passou a se chamar xls_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.md passou para a página dona do fato; os dois arquivos deixaram de carregar esse conteúdo.