Skip to content

fix(dre): fecha a auditoria da DRE — folha contada 2x, recorte que mentia e estorno que sumia - #41

Merged
BarryBits merged 2 commits into
mainfrom
fix/dre-ondas-3-4-5
Aug 4, 2026
Merged

fix(dre): fecha a auditoria da DRE — folha contada 2x, recorte que mentia e estorno que sumia#41
BarryBits merged 2 commits into
mainfrom
fix/dre-ondas-3-4-5

Conversation

@BarryBits

Copy link
Copy Markdown
Owner

Fecha as Ondas 3, 4 e 5 da auditoria da DRE (docs/atros-v3/handoff-2026-07-31-auditoria-dre.md). As Ondas 1 e 2 já estavam em produção (#39, #40).

Dois commits, um por gate.


f1cf688 — Ondas 3 e 4: a folha para de ser contada duas vezes

totaisPorCategoria[catId] tinha dois significados. Em mês aberto era por-categoria de verdade; em mês fechado a reconciliação jogava a L50 inteira em despesas_administrativas e deixava pessoas com o valor do motor TS.

Por isso os leitores erravam em direções opostas:

Leitor Convenção Mês aberto Mês fechado
anomaly-detection, dfc-direct-mapper admin + pessoas ❌ folha 2×
break-even (extrato-processor) admin só ❌ sem a folha
ponto de equilíbrio (kpi-advanced-service) admin só ❌ sem a folha

Os três de baixo o handoff da auditoria não listava.

A tabela CATEGORIA_LINHA_SINAL morreu — o sinal do catálogo reproduz ela inteira. Como a v3 fecha por linha e o dre_detalhado guarda por categoria, a reconciliação passa a ratear a linha entre as categorias que a dividem, com a última absorvendo o resíduo (a soma bate exato, que é a invariante de que os leitores dependem). Valor não atribuível cai na residual: afirmar "a folha foi X" sem saber é pior que dizer "administrativas foi X". Convenção registrada em categorization-rules.ts.

Junto vão três defeitos de recorte:

  • addMonths transbordava — em 31/05, "Mês passado" devolvia MAIO. Idem 31/07, 31/10, 31/12 e 29–31/03.
  • Recorte de um mês abria em coluna semanal enquanto o rótulo prometia "último mês fechado". Era esse recorte que levava erro de cascata aos olhos do dono. O toggle DIA/SEM/MÊS segue disponível.
  • Transação sem categoria sumia da tela — o balde existia no acumulador e o loop iterava só CATEGORIAS. Agora vira linha visível "Não categorizado", em L99, fora do resultado.

Re-fechamento aplicado

Os 30 snapshots gravados estavam todos corrompidos — R$ 13.962.202,16 de folha em dobro, o mesmo número da Onda 1. O fix de código sozinho não alcançava produção, e o refechar-snapshots-catalogo não pegava: o gate dele era headline || subs, e esse drift não mexe em nenhum dos dois. Ganhou o terceiro detector, categoriasComDrift.

Rodado: 30 gravados, diagnóstico depois deu 0 corrompidos, novo dry-run dá 0 mudanças (idempotente). Resultado Líquido não mudou em nenhum mês — muda só o split.


1c05831 — Onda 5: o estorno para de sumir

calculate_monthly_dre_v3 somava um lado só conforme o sinal. Um estorno — crédito numa categoria de despesa (devolução de compra, reembolso) ou débito numa de receita (chargeback) — contribuía zero. E não caía em L90 nem L99: o dinheiro sumia da cascata inteira. O extrato mexia e a DRE não sabia.

Em regime de caixa quem está certo é o TS: se o dinheiro voltou pra conta, ele voltou, e reduz a despesa do mês em que caiu.

A migration passa a somar magnitude líquida. Escolha deliberada de não inverter a convenção do JSONB linhas: linha de despesa continua positiva quando é despesa líquida e a escada continua subtraindo, então nenhum leitor de linhas_dre foi tocado (montarKpisDeV3, cash-cycle-analyzer, dre-v3-aggregate, sinalDaLinha). O caminho óbvio — SQL fazendo credito−debito e escada virando soma — teria raio de explosão enorme à toa. O corpo da função é cópia byte-idêntica do dump com as duas linhas do CASE trocadas.

Por que agora

Medido antes de aplicar: 18.004 transações, 0 estornos. A divergência era 100% latente. Dry-run depois da migration: 30/30 idênticos — nada a gravar. Era essa a hora: com estorno real em campo, a correção passaria a mexer em número que o cliente já viu.

Terceira cópia da convenção

visao-service.ts (ranking top-8 da Visão do Projeto) também somava um lado só — a DRE diria 35.000 e o ranking 40.000 pro mesmo fornecedor no mesmo mês. Alinhada junto.


Banco

Migration 20260804000000_dre_v3_sinal_liquido já aplicada no remoto. Re-dump confirma: diff contra o dump de 30/07 dá exatamente 3 linhas, todas da migration (as 2 do CASE + o COMMENT); roles.sql sem mudança de conteúdo; zero drift fora de migration. Snapshot volta a 🟢 autoritativo (20260804000000 = max(repo)).

Com 0 estornos a saída da RPC é idêntica nas duas versões — então ler a função era a única prova possível de que a migration entrou. Nenhum teste de comportamento serviria.

Gate

  • 1465/1465 testes (157 arquivos), +21 novos
  • f1cf688 verificado verde isoladamente (164 testes) — sem commit intermediário quebrado
  • next build ok · tsc limpo · 0 erro e 0 warning novo de eslint

Armadilha que ficou documentada

O gabarito motorSqlV3 dentro de cascata.test.ts é uma cópia da álgebra do SQL, não uma leitura dela. Trocar o CASE da RPC sem trocar o gabarito deixaria a paridade verde e mentirosa — exatamente o modo de falha que deixou a folha sumir na primeira vez. Aviso no cabeçalho do arquivo.

Como revisar

O check ao vivo que falta é na tela: abrir a DRE da Vertímetal e conferir que entra em coluna mensal e que Administrativas × Pessoas aparecem separadas somando a L50.

🤖 Generated with Claude Code

BarryBits and others added 2 commits August 4, 2026 20:02
`totaisPorCategoria[catId]` tinha DOIS significados. Em mes aberto era
por-categoria de verdade; em mes fechado a reconciliacao jogava a L50
INTEIRA em `despesas_administrativas` e deixava `pessoas` com o valor do
motor TS. Os leitores erravam em direcoes opostas: quem somava os dois
ids contava a folha 2x (anomaly-detection, DFC); quem lia so `admin`
perdia a folha (break-even, ponto de equilibrio, indice de gastos fixos)
— esses tres o handoff da auditoria nao listava.

A tabela `CATEGORIA_LINHA_SINAL` morreu: o `sinal` do catalogo reproduz
ela inteira. A v3 fecha por LINHA e o `dre_detalhado` guarda por
CATEGORIA, entao a reconciliacao passa a RATEAR a linha entre as
categorias que a dividem, na proporcao da composicao TS, com a ultima
absorvendo o residuo — a soma bate exato, que e a invariante de que os
leitores dependem. Valor nao atribuivel cai na RESIDUAL: afirmar "a
folha foi X" sem saber e pior que dizer "administrativas foi X".
Convencao registrada em `categorization-rules.ts`.

Os 5 leitores pararam de enumerar `categoria_id` a mao — agora pedem a
linha via `somarLinha`.

Junto vao tres defeitos de recorte:

- `addMonths` transbordava: em 31/05, "Mes passado" devolvia MAIO. Idem
  31/07, 31/10, 31/12 e 29–31/03; "Ultimos 6 meses" em 31/07 comecava em
  marco. O dia agora e grampeado no ultimo dia valido do mes de destino.
- recorte de UM mes abria em coluna SEMANAL enquanto o rotulo prometia
  "ultimo mes fechado". Era esse recorte que levava erro de cascata aos
  olhos do dono. O toggle DIA/SEM/MES do PeriodPicker segue disponivel.
- transacao sem categoria SUMIA da tela: o balde existia no acumulador e
  o loop iterava so `CATEGORIAS`. Agora vira linha visivel "Nao
  categorizado", em L99, fora do resultado. Isso exigiu alinhar o
  fallback do `dfc-direct-mapper`, que mandava id desconhecido pra
  "operacional" — o inverso da cascata — e quebraria a invariante
  op+inv+fin === resultado liquido.

Os 30 snapshots ja gravados estavam TODOS corrompidos (R$ 13.962.202,16
de folha em dobro). O `refechar-snapshots-catalogo` nao pegava: o gate
dele era `headline || subs`, e esse drift nao mexe em nenhum dos dois.
Ganhou o terceiro detector, `categoriasComDrift`. Re-fechamento rodado:
30 gravados, diagnostico depois deu 0 corrompidos, novo dry-run da 0
mudancas.

Gate: 1463/1463 testes, build ok, 0 erro/warning novo de eslint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`calculate_monthly_dre_v3` somava UM LADO so conforme o `sinal` da
categoria (`'+' -> t.credito`, `'-' -> t.debito`). Um estorno — credito
numa categoria de despesa (devolucao de compra, reembolso de fornecedor)
ou debito numa de receita (chargeback) — contribuia ZERO. E nao caia em
L90 nem em L99: o dinheiro sumia da cascata INTEIRA. O extrato mexia e a
DRE nao sabia.

O motor TS sempre fez `credito - debito`, e em regime de caixa e ele que
esta certo: se o dinheiro voltou pra conta, ele voltou, e reduz a despesa
do mes em que caiu.

A migration passa a somar MAGNITUDE LIQUIDA. Escolha deliberada de NAO
inverter a convencao do JSONB `linhas`: linha de despesa continua
positiva quando e despesa liquida e a escada continua subtraindo, entao
nenhum leitor de `linhas_dre` foi tocado (`montarKpisDeV3`,
`cash-cycle-analyzer`, `dre-v3-aggregate`, `sinalDaLinha`). O caminho
obvio — SQL fazendo credito-debito e escada virando soma — teria raio de
explosao enorme a toa. O corpo da funcao e copia byte-identica do dump,
com as duas linhas do CASE trocadas.

Blast radius medido ANTES de aplicar: 18.004 transacoes, 0 estornos. A
divergencia era 100% latente. Por isso a hora de corrigir era essa: com
estorno real em campo, a correcao passaria a mexer em numero que o
cliente ja viu. Dry-run do re-fechamento depois da migration: 30/30
identicos — nada a gravar.

Os dois `it.fails` viraram `it`, mais dois casos que faltavam: a
devolucao MAIOR que a compra (a linha inverte e o lucro sobe — o caso que
quebraria com o fix ingenuo) e a prova de que o dinheiro do estorno volta
pra propria linha em vez de cair em L90/L99.

O gabarito `motorSqlV3` dentro do teste e uma COPIA da algebra do SQL,
nao uma leitura dela: trocar o CASE da RPC sem trocar o gabarito deixaria
a paridade verde e mentirosa — o mesmo modo de falha que deixou a folha
sumir. Aviso no cabecalho do arquivo.

Terceira copia da convencao, que a auditoria nao citava:
`visao-service.ts` (ranking top-8 da Visao do Projeto) tambem somava um
lado so — a DRE diria 35.000 e o ranking 40.000 pro mesmo fornecedor no
mesmo mes. Alinhada junto.

Re-dump do banco confirma a aplicacao: diff contra o dump de 30/07 da
exatamente 3 linhas, todas da migration (as 2 do CASE + o COMMENT);
`roles.sql` sem mudanca; zero drift fora de migration. Snapshot volta a
autoritativo (20260804000000 = max(repo)). Com 0 estornos a saida da RPC
e identica nas duas versoes, entao ler a funcao era a UNICA prova
possivel.

Gate: 1465/1465 testes, build ok, 0 erro/warning novo de eslint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
atr-os Ready Ready Preview Aug 4, 2026 11:18pm

@supabase

supabase Bot commented Aug 4, 2026

Copy link
Copy Markdown

Updates to Preview Branch (fix/dre-ondas-3-4-5) ↗︎

Deployments Status Updated
Database Tue, 04 Aug 2026 23:14:59 UTC
Services Tue, 04 Aug 2026 23:14:59 UTC
APIs Tue, 04 Aug 2026 23:14:59 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Tue, 04 Aug 2026 23:15:04 UTC
Migrations Tue, 04 Aug 2026 23:15:06 UTC
Seeding ⏸️ Tue, 04 Aug 2026 23:14:54 UTC
Edge Functions ⏸️ Tue, 04 Aug 2026 23:14:54 UTC

❌ Branch Error • Tue, 04 Aug 2026 23:15:06 UTC

ERROR: relation "projects" does not exist (SQLSTATE 42P01)
At statement: 0
-- ============================================================
-- ATR OS - Migration: Project Lifecycle States
-- ============================================================
-- Adiciona campos de lifecycle para o fluxo de onboarding:
-- aguardando_docs → diagnostico_7d → plano_entregue → em_execucao → concluido
-- ============================================================

-- 1. Adicionar colunas de timestamp para cada etapa do lifecycle
ALTER TABLE projects
ADD COLUMN IF NOT EXISTS docs_solicitados_em timestamptz,
ADD COLUMN IF NOT EXISTS docs_recebidos_em timestamptz,
ADD COLUMN IF NOT EXISTS diagnostico_deadline timestamptz,
ADD COLUMN IF NOT EXISTS plano_entregue_em timestamptz,
ADD COLUMN IF NOT EXISTS execucao_inicio timestamptz,
ADD COLUMN IF NOT EXISTS execucao_deadline timestamptz

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@BarryBits
BarryBits merged commit 80603a0 into main Aug 4, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant