Pular para conteúdo

0019 — Cota Petrobrás: full-replace, allow-list de produto e alias de endpoint

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-08-18 · Decidido em: 2026-08-18

Contexto

Novo tipo de arquivo (spec 011, migração V19): a planilha "Cota Petrobrás mês.xlsx" — cota mensal de volume (m³) concedida pela Petrobras por polo de distribuição e produto. Shape verificado no arquivo real (2022–2026): uma aba por ano (^\d{4}$) + aba Planilha1 (resumo derivado, ignorar); cabeçalho POLO | PRODUTO | JANEIRO..DEZEMBRO; célula POLO mesclada (só na 1ª linha do bloco); coluna de ruído JANEIRO.2024 (bleed do mês seguinte, colide com o Janeiro da aba 2024) e TOTAL PRODUTO/ANO; linha TOTAL MÊS; célula "_" (sem cota) e vazia (mês futuro); e uma side-table de anotação solta (na aba 2023: Gas/S10 com número na coluna de mês, produto em branco). A planilha entra pela porta genérica (POST /api/xls/processar) e pela página manual do botão "Importar XLS" do bi-comercial.

Decisão

  1. bi_cota_petrobras: PK surrogate uuidv7(), sem chave natural de linha. A planilha é a matriz inteira; (competencia, polo, produto) poderia repetir sob correções.

  2. Idempotência/correção = full-replace (não replace-por-período como Compras/0017): numa transação, deletarTudo() (DELETE FROM bi_cota_petrobras) + inserirBatch() da matriz inteira. Guard de vazio: arquivo detectado como cota mas sem nenhum produto canônico com número não roda o DELETE — nunca há caso legítimo de esvaziar a matriz via upload.

  3. Guard de emissão = allow-list de produto canônico (Diesel S10/Diesel S500/Gasolina/Diesel R5, comparado por norm = NFD + strip acento + lowercase + colapso de espaços; grava a forma canônica). É o único guard — mata de uma vez a linha TOTAL MÊS (produto vazio), as separadoras e a side-table de anotação. Só os 12 meses canônicos são mapeados por header normalizado; JANEIRO.2024/TOTAL PRODUTO/ANO/ruído são descartados.

  4. Detecção por conteúdo, prioridade logo após COMPRAS: existe aba ^\d{4}$ e cujo cabeçalho começa com POLO + PRODUTO + JANEIRO. Não usa o header da 1ª aba física (é Planilha1).

  5. Endpoint manual: alias /xls/importar (canônico) + /compras/importar (herdado, vivo). A página aberta aceita a allow-list {COMPRAS, COTA_PETROBRAS} e no máximo uma planilha de cota por lote. A rota herdada continua servida porque o botão do bi-comercial já está deployado nela (segredo BI_COMERCIAL_XLS_IMPORT_TOKEN).

Consequências

  • Diverge do diff-aware e também do replace-por-período (0017): é full-replace, a 2ª exceção documentada na cobertura de DELETE do cap. 4 do livro.
  • Premissa: ≤1 planilha de cota por lote. O DELETE FROM full-table não é seguro sob concorrência (o executor roda até 4 arquivos em paralelo; o ORDEM_INSERCAO anti-deadlock só cobre row-locks de ON CONFLICT, não um DELETE de tabela inteira). Mitigado pela guarda ≤1 cota/batch na página de import. Um LOCK TABLE foi considerado e descartado (over-engineering para um risco operacional ~zero).
  • Produto novo/renomeado some em silêncio? Não: linha que parecia dado (polo + valor de mês) com produto não-vazio e não-canônico vira LOG.warn (sinal pro operador), não descarte mudo.
  • Correção reprocessa a matriz inteira → um checkpoint PowerSync no reenvio corrigido (o checksum do xls_processamento barra o reenvio idêntico).
  • Contrato com o cliente Flutter (painel de cotas) e sync rules (../powersync/) assumem esse schema — passo externo, fora deste repo.

Alternativas consideradas

  • Replace-por-período (como 0017): descartada para a v1 — a planilha é a matriz completa (todos os anos), full-replace é mais simples. Fica como caminho futuro se a cota passar a ser enviada partida por ano (Open Question OQ2 da spec 011).
  • Guard "pular polo que começa com TOTAL" (proposto no prompt): frágil — não pega a side-table de anotação da aba 2023 (polo Gas/S10, produto vazio, com número). A allow-list de produto canônico é mais robusta e cobre os três descartes de uma vez.
  • LOCK TABLE bi_cota_petrobras IN EXCLUSIVE MODE no full-replace: descartada — fecharia o buraco de dois lotes simultâneos, mas a probabilidade operacional é ~zero e a premissa ≤1 cota/lote já basta. Simplicidade primeiro.
  • Renomear a rota /compras/importar → /xls/importar (hard): descartada — quebraria o botão do bi-comercial já deployado. Alias mantém as duas vivas; o bi-comercial migra depois e aí a herdada some. Rename da classe ComprasImportController fica deferido (cosmético).

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.

Correção 2026-09-14: o rename da classe saiu junto com o package-by-feature (0030): ComprasImportController passou a importacao.ImportacaoXlsController.