Pular para conteúdo

0031 — Títulos por posição, com atual e recusa total

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-10-02 · Decidido em: 2026-10-02

Contexto

Novo tipo de arquivo FLUXO_PAGAR_RECEBER (spec 026 do bi-comercial, dono da feature; migração V22): o relatório FluxoPagarReceber que o ERP gera todo dia às 04:01 com os títulos em aberto (a pagar e a receber) numa data — a posição. Shape verificado na amostra de 28/09/2026:

  • cinco abas; Grupo, Filial, Cliente e Detalhado são visões só do RECEBER; o PAGAR existe apenas na SemQuebras (uma linha por título), que é a única aba ingerida;
  • NBSP à esquerda de DOCUMENTO e VCTO; VALOR com ruído binário (1159.3499999999999); FL/PC numéricos; CC como "cod-nome"; CNPJ ou CPF com máscara;
  • a posição está no título "VALORES A RECEBER dd/mm/aaaa" da Grupo (e numa célula de data do cabeçalho da Detalhado); o "Total" da Grupo é o único total de controle (só do receber).

Um título não tem chave natural estável entre posições: pago, ele some; renegociado, muda de valor ou vencimento. O app (PowerSync) precisa só da posição mais recente; a diretoria quer o histórico guardado para análises futuras.

Decisão

  1. bi_titulo guarda todas as posições, e atual = true só na maior data_posicao. O PowerSync sincroniza WHERE atual = 1; ao virar a posição, as linhas antigas saem do bucket e as novas entram, sem reset do banco local. O histórico fica no Postgres (~3 mil linhas por dia).

  2. Idempotência = replace da posição, numa transação: garante as filiais (só o código) → calcula o destino da posição (nova / histórico / substitui) → upsert diff-aware de bi_centro_custo → DELETE FROM bi_titulo WHERE data_posicao = ? → INSERT com atual = false → UPDATE … SET atual = (data_posicao = max) tocando só as linhas que mudam. Arquivo de posição anterior à maior grava o histórico sem mudar a atual.

  3. pg_advisory_xact_lock serializa as importações deste tipo. O executor processamento roda 4 arquivos em paralelo; duas posições recalculando atual ao mesmo tempo poderiam terminar ambas atuais. O lock é por transação (solta no commit/rollback).

  4. Recusa total, sem gravar nada: aba SemQuebras ausente, posição não encontrada, cabeçalho incompleto ou qualquer linha inválida → ResultadoProcessamento.Ignorado (status IGNORADO, motivo com até 20 linhas em erro_mensagem). Arquivo ruim é recusa esperada, não falha técnica: não vai ao GlitchTip (log INFO do pipeline). O processador lê e valida tudo antes de escrever.

  5. Total de controle do RECEBER vira aviso, não recusa: Σ RECEBER × "Total" da Grupo; divergindo, grava e o resumo avisa.

  6. Reenvio de ERRO/IGNORADO reprocessa o mesmo registro (todos os tipos, inclusive pela porta única): antes, o checksum repetido batia no UNIQUE e devolvia 409 "Checksum duplicado (concorrência)", e um IGNORADO não tinha caminho de reprocessamento — um arquivo recusado por bug do parser ficava preso mesmo depois da correção.

  7. A página /xls/importar espera o processamento deste tipo (até 30 s) e mostra o resumo, que o processador devolve no ResultadoProcessamento.Sucesso e o pipeline grava em xls_processamento.resumo (o processador não toca o xls_processamento: trava do ArchUnit).

  8. Detecção por conteúdo, logo após COTA_PETROBRAS: qualquer aba que comece com "Fluxo Pagar-Receber" — não só a SemQuebras, para que a falta dela seja recusada com mensagem clara em vez de "tipo não reconhecido".

Consequências

  • 3ª estratégia sem upsert, ao lado do replace-por-período (0017) e do full-replace (0019): replace-por-posição. Reimportar um arquivo diferente da mesma posição regenera os id e manda ~3 mil remoções e inserções ao aparelho — de propósito (o checksum já barra o reenvio idêntico, e uma chave natural de título por posição não compensa a complexidade). Não "otimizar" para upsert sem rever esta decisão.
  • Credores do PAGAR não vão a bi_fornecedor (que é do Relatório de Compras) nem clientes do RECEBER a bi_cliente: CPF/CNPJ e nome ficam no próprio título, com máscara, para o join.
  • Centro de custo é dimensão nova (bi_centro_custo), a ChaveUnid do X-Adm — numeração distinta da filial.
  • A regra do PowerSync e as raw tables do app são passos externos (repos ../powersync/ e bi-comercial); ordem de deploy: xls → app → regra.
  • Sem retenção de posições antigas por ora (~1 milhão de linhas/ano); reavaliar se pesar.

Alternativas consideradas

  • Substituir tudo a cada arquivo (como a 0019): descartada — perde o histórico de posições.
  • Sincronizar todas as posições: descartada — o app só usa a atual e o volume cresceria sem fim no aparelho.
  • Chave natural por posição + upsert diff-aware: descartada — evitaria o churn no reenvio de arquivo corrigido da mesma posição, raro, ao custo de uma chave (tipo, documento, parcela, filial, CPF/CNPJ) que o ERP não garante.
  • Pular a linha ruim e gravar o resto: descartada — o total importado deixaria de bater com o ERP sem ninguém ver.
  • Manter a recusa como ERRO: descartada — todo ERROR vai ao GlitchTip (norma de log da casa), e cada envio ruim seria um alarme falso.