Skip to content

fix(dre): a folha de pagamento volta pro resultado + resguardo de provider da IA - #39

Merged
BarryBits merged 2 commits into
mainfrom
fix/cascata-data-driven
Jul 31, 2026
Merged

fix(dre): a folha de pagamento volta pro resultado + resguardo de provider da IA#39
BarryBits merged 2 commits into
mainfrom
fix/cascata-data-driven

Conversation

@BarryBits

Copy link
Copy Markdown
Owner

Dois fixes que estavam parados fora da main.


1. fix(dre) — a folha de pagamento volta pro resultado

A cascata da DRE estava escrita à mão em três lugares em TS, cada um enumerando categoria_id:

resultadoOperacional = lucroBruto + despVendas + despAdm

Quando a migration 20260713000000 criou a categoria pessoas na mesma linha 50 de despesas_administrativas, o motor SQL absorveu sozinho (ele agrupa por linha_dre), mas os três motores TS não. A folha saiu do resultado sem nenhum teste ficar vermelho. A migration declarava "ZERO-SHIFT" — era verdade só para quem lia o catálogo.

Medido na Vertímetal: R$ 465.406,74/mês · R$ 13.962.202,16 em 30 meses.

Na tela aparecia como uma cascata que não fecha. Em jan/25:

    Lucro Bruto ..................... 554.319,91
  − Despesas Administrativas ....... -91.069,88
    ────────────────────────────────────────────
    deveria dar ..................... 463.250,03
  = Resultado Operacional (EBITDA) ... 35.090,51   ← a tela
    ────────────────────────────────────────────
    sumiam .......................... 428.159,52   ← a folha

Os R$ 428.159,52 estavam desenhados em "Informativo · fora do resultado", ao lado de empréstimo e transferência entre contas — enquanto o SQL já os subtraía dentro do EBITDA.

O que mudou

  • src/lib/dre/cascata.ts (novo) — primitivo único. Agrupa por linhaDre, aplica sinal, respeita entraNoResultado. Categoria fora do catálogo cai em L99, espelhando a blindagem do LEFT JOIN da RPC. Categoria nova entra na cascata sozinha.
  • extrato-processor — as duas escadas (multi-mensal e forense) repointadas.
  • DREPivotTable — ordem de leitura e subtotais vêm do catálogo; abaixo da linha fica só L90/L95/L99.

O tipo Categoria já documentava a intenção desde sempre: "Motor de DRE filtra por essa flag em vez de hardcoded list". Agora cumpre.

Gate

Replay da Vertímetal 2025 inteira pelo caminho de mês ABERTO (motor TS, snapshots ignorados de propósito), contra os 12 snapshots do SQL:

TOTAL 2025   TS aberto: -620.231,74  |  snapshot: -620.231,74  |  pior Δ mensal: 0,00

Delta zero nos 12 meses, em EBITDA e Resultado Líquido. Os dois motores passaram a dizer a mesma coisa, e nenhum número que já estava certo se moveu.

Teste de paridade contra a álgebra do calculate_monthly_dre_v3 escrito antes do primeiro repoint — era a rede que faltava. A divergência de convenção de sinal TS×SQL (estorno dentro da categoria) fica documentada em dois it.fails; unificar invalida snapshot e é trilha própria.

Sem migration, sem re-fechamento

Os KPIs do snapshot sempre vieram do SQL v3 — nunca estiveram errados. O pivot de mês fechado lê col.dre.totais, reconciliado com a v3 no fechamento. Os 120 snapshots seguem válidos.

Ressalva: o dre_detalhado.resultados dos snapshots antigos foi gravado pela escada velha e está errado lá dentro. Conferi quem lê esse campo: ninguém. É lixo dormente; fechamentos novos já gravam certo.


2. fix(ia) — o resguardo de provider agora existe de verdade

Já estava pronto, só não tinha subido. "Gerar nova versão" do Plano de Voo caía com "resposta vazia da IA" quando a OpenAI recusava por quota (429). Três defeitos empilhados: fallback desligado por padrão, override de modelo vazando entre providers, e dois defaults contraditórios.

Zero migration. Não muda qual provider é o primário — muda que a queda de um lado deixa de derrubar o sistema. AI_PROVIDER e AI_FALLBACK_PROVIDER seguem opcionais.


Verificação

  • 1418 testes / 153 arquivos verdes
  • next build limpo
  • tsc limpo em src/
  • eslint: zero erros novos — os 2 do DREPivotTable.tsx são idênticos em HEAD (débito pré-existente: overlay hand-rolled + .cockpit-card-accent)
  • Inclui re-dump do banco: schema.sql volta a 🟢 autoritativo (diff de 10 linhas = as duas migrations pendentes; zero drift fora de migration; calculate_monthly_dre_v3 byte-idêntica)

Falta

Smoke visual na tela real — o gate acima é replay de dados, não render no browser.

🤖 Generated with Claude Code

BarryBits and others added 2 commits July 29, 2026 16:06
"Gerar nova versão" do Plano de Voo falhou com "IA retornou narrativas
inválidas: resposta vazia da IA [modelo=gpt-5-nano]". A IA não devolveu vazio —
a OpenAI recusou por cobrança. A telemetria da casa (`ai_calls`) tinha o erro
cru guardado o tempo todo:

  openai_http_429: "You exceeded your current quota, please check your plan
  and billing"

Padrão nos últimos 30 dias: TODA chamada que foi pro gpt-5-nano falhou assim
(15/07, 21/07 ×2, 23/07, 27/07); TODA que foi pro gemini-2.5-flash-lite passou
com valid=true e zero retry — inclusive as 5 gerações de Plano de Voo que estão
no ar. E `fallback_used` NULL em todas as falhas: o resguardo nunca entrou.

## Por que o resguardo não entrou — e não entraria

Três defeitos empilhados, cada um suficiente pra derrubar a geração:

1. FALLBACK DESLIGADO POR PADRÃO. `getFallbackProvider` devolvia null a menos
   que `AI_FALLBACK_PROVIDER` estivesse setada à mão. Ninguém setou. Com o
   Gemini ao lado, funcionando e de graça, um 429 de cobrança derrubava tudo.
   Agora o padrão é LIGADO: o outro provider assume, desde que tenha chave.
   `AI_FALLBACK_PROVIDER=off` desliga de propósito.

2. O OVERRIDE DE MODELO VAZAVA ENTRE PROVIDERS — e este é o que tornava o
   resguardo decorativo. O caller passa `model: "gpt-5-nano"`; a OpenAI estoura
   a quota; o cliente cai no Gemini... e chama o Gemini pedindo "gpt-5-nano",
   que não existe. Mesmo com a env setada, o fallback morria na porta.
   `resolveModel` agora ignora override que pertence claramente ao OUTRO
   provider (nome desconhecido — fine-tune, alias — segue respeitado).

3. DOIS DEFAULTS CONTRADITÓRIOS. `getProvider()` resolve gemini quando
   `AI_PROVIDER` está ausente; os callers do Plano de Voo e do Retrato faziam
   `(process.env.AI_PROVIDER ?? "openai")` por conta própria só pra decidir o
   override. Efeito: sem a env, o caller mandava nome de modelo da OpenAI e o
   cliente chamava o Gemini com ele. Ou seja, "só remover a variável" teria
   trocado um erro por outro. Os callers agora sempre passam o override da
   família OpenAI e deixam o cliente decidir — com (2) no lugar, é seguro.

## A mensagem também mentia

`meta.error` carregava o 429 e a mensagem montada só mostrava modelo/latência/
retries. Quem depurava ia atrás do prompt em vez da conta. Agora provider,
fallback e a causa crua entram no texto. Mesma correção no `retrato/narrativa`,
que respondia só "IA indisponível".

Nada aqui muda qual provider é o primário: isso segue em `AI_PROVIDER` (e o
default do cliente, gemini, é o plano grátis). O que muda é que a queda de um
lado deixa de ser queda do sistema.

Validação: 151 arquivos, 1403/1403 (13 novos cobrindo default, fallback com/sem
chave, desligamento explícito, o não-vazamento de modelo nos dois sentidos e o
caminho exato do bug) · tsc limpo · eslint 0 · zero migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A cascata da DRE estava escrita à mão em três lugares em TS, cada um
enumerando `categoria_id`:

    resultadoOperacional = lucroBruto + despVendas + despAdm

Quando a migration 20260713000000 criou a categoria `pessoas` na MESMA
linha 50 de `despesas_administrativas`, o motor SQL absorveu sozinho (ele
agrupa por `linha_dre`), mas os três motores TS não. A folha saiu do
resultado sem nenhum teste ficar vermelho. A migration declarava
"ZERO-SHIFT" — era verdade só para quem lia o catálogo.

Medido na Vertímetal: R$ 465.406,74/mês, R$ 13.962.202,16 em 30 meses.

Na tela isso aparecia como uma cascata que não fecha: em jan/25,
554.319,91 (Lucro Bruto) − 91.069,88 (Administrativas) = 463.250,03, mas
o EBITDA exibido era 35.090,51. Os 428.159,52 da folha estavam
desenhados em "Informativo · fora do resultado", ao lado de empréstimo e
transferência entre contas — enquanto o SQL já os subtraía.

Agora a cascata sai do catálogo (linhaDre / entraNoResultado / sinal),
como o tipo `Categoria` já documentava desde sempre: "Motor de DRE filtra
por essa flag em vez de hardcoded list". Categoria nova entra sozinha.

  - src/lib/dre/cascata.ts — primitivo único (agrupa por linha, aplica
    sinal, respeita entraNoResultado; fora do catálogo cai em L99 como
    na blindagem do LEFT JOIN da RPC)
  - extrato-processor: as duas escadas (multi-mensal e forense) repointadas
  - DREPivotTable: ordem de leitura e subtotais vêm do catálogo; abaixo da
    linha fica só L90/L95/L99

Teste de paridade contra a álgebra do calculate_monthly_dre_v3 escrito
ANTES do primeiro repoint — era a rede que faltava. A divergência de
convenção de sinal TS x SQL (estorno dentro da categoria) fica documentada
em dois it.fails; unificar invalida snapshot e é trilha própria.

Gate (dump fresco de 2026-07-30): replay da Vertímetal 2025 pelo caminho
de MÊS ABERTO bate com os 12 snapshots ao centavo, EBITDA e Resultado
Líquido. Total 2025 = -620.231,74 nos dois caminhos.

Inclui re-dump do banco: schema.sql volta a autoritativo (diff de 10
linhas = as duas migrations pendentes; zero drift fora de migration;
calculate_monthly_dre_v3 byte-idêntica).

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

vercel Bot commented Jul 31, 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 Jul 31, 2026 8:28pm

@supabase

supabase Bot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project tdlxqqgechxhkygdmsxq because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

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