Skip to content

fix(db): validate_quote_real_discount fail-closed em NULL (Onda 7, B-4)#198

Merged
adm01-debug merged 1 commit into
mainfrom
cleanup/onda-7-validate-discount-fail-closed
May 14, 2026
Merged

fix(db): validate_quote_real_discount fail-closed em NULL (Onda 7, B-4)#198
adm01-debug merged 1 commit into
mainfrom
cleanup/onda-7-validate-discount-fail-closed

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented May 14, 2026

Bloqueador B-4 da auditoria de 10/mai. A função trigger validate_quote_real_discount tinha bypass NULL: quando o vendedor não tinha linha em seller_discount_limits, _max_allowed=NULL e a condição "_max_allowed IS NOT NULL AND _real_pct > _max_allowed" era FALSA, deixando passar DESCONTO INFINITO.

Cenário concreto:

  1. Admin promove novo vendedor
  2. Esquece de cadastrar limite em seller_discount_limits
  3. Vendedor cria orçamento com 99% de desconto
  4. Trigger não bloqueia → margem evapora

FIX (Opção B da auditoria):

  • COALESCE(_max_allowed, 0) após o SELECT INTO
  • Mensagem distinta para caso "sem cadastro" vs "estourou limite"

ESCOPO LIMITADO:
O check de admin (role='admin') NÃO foi alterado. Verificação no banco mostrou que existem 2 usuários reais com role='admin' (auditoria assumia 0). Tema do "dual admin pattern" está deferido para decisão arquitetural.

VALIDAÇÃO:

  • Migration aplicada via MCP apply_migration (ADR 0006: zero db push)
  • pg_proc confirma COALESCE no corpo da função
  • 5 vendedores ativos hoje têm limite — fix não quebra ninguém
  • Vendedor novo agora recebe mensagem clara de "sem cadastro"

Versão da migration: 20260514165252 (já em prod).
Detalhes: docs/hardening/ONDA-7-DISCOUNT-FAIL-CLOSED.md

📋 Descrição

🎯 Tipo de mudança

  • 🚀 feat — nova funcionalidade
  • 🐛 fix — correção de bug
  • ♻️ refactor — refatoração (sem mudança de comportamento)
  • 🔧 chore — manutenção, deps, config
  • 📚 docs — documentação
  • ⚡ perf — performance
  • 🔒 security — segurança
  • 🚨 hotfix — correção urgente em produção
  • 💥 breaking change — quebra compatibilidade

🔗 Issues relacionadas

Closes #
Refs #

🌐 Sistemas afetados

  • Bitrix24 (CRM, SPAs, BizProc)
  • Supabase (DB, Edge Functions, RLS, migrations)
  • n8n (workflows)
  • Evolution API / WhatsApp
  • Bling (NFe, OAuth)
  • Cloudflare (Workers, Images, Tunnels)
  • Frontend (UI, dashboards)
  • CI / GitHub Actions
  • Outro: ____

🧪 Como testar

✅ Checklist pré-merge

Qualidade

  • Código segue style guide (ESLint passa)
  • npx tsc --noEmit passa sem erros
  • Testes passam (npm run test)
  • Adicionei testes para novas funcionalidades quando aplicável
  • CodeRabbit revisou o PR (ou justificativa para skip)

Segurança

  • Sem secrets, tokens ou credenciais hardcoded
  • Variáveis de ambiente novas documentadas
  • Sem console.log com payloads sensíveis (usar logger.*)
  • RLS revisado se houve mudança em tabelas
  • Edge functions: input validado com Zod

Documentação

  • Atualizei docs (README / CHANGELOG / docs/) se necessário
  • Memória atualizada (mem://) se a mudança afetar arquitetura/regras
  • Migrations com backup em _backup_*_YYYYMMDD se destrutivas

UI

  • Componentes usam tokens semânticos (sem cores hardcoded)
  • Screenshots / vídeo anexados (se mudança visual)

📸 Screenshots (se UI)

🔄 Plano de rollback

⚠️ Notas para o reviewer


Summary by cubic

Corrige o bypass de NULL na trigger validate_quote_real_discount, fechando o caminho para “desconto infinito” quando o vendedor não tem limite cadastrado. Resolve o bloqueador B-4 da auditoria (Onda 7) e mantém o fluxo atual para quem já tem limite.

  • Bug Fixes
    • Fail-closed em NULL: COALESCE(_max_allowed, 0) e checagem direta _real_pct > _max_allowed.
    • Mensagens diferentes para “sem cadastro” e “limite estourado” (mesmo errcode).
    • Bypass de role='admin' mantido (fora do escopo do B-4).
    • Inclui migration 20260514165252_onda7_validate_quote_real_discount_fail_closed_null.sql e doc docs/hardening/ONDA-7-DISCOUNT-FAIL-CLOSED.md.

Written for commit ed17527. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Corrigido comportamento de validação de desconto em cotações: vendedores sem limite de desconto cadastrado agora têm descontos bloqueados, em vez de ilimitados.
  • Documentation

    • Adicionada documentação detalhada da correção de validação de desconto, incluindo cenários de teste e procedimentos operacionais.

Review Change Stack

Bloqueador B-4 da auditoria de 10/mai. A função trigger
validate_quote_real_discount tinha bypass NULL: quando o vendedor não
tinha linha em seller_discount_limits, _max_allowed=NULL e a condição
"_max_allowed IS NOT NULL AND _real_pct > _max_allowed" era FALSA,
deixando passar DESCONTO INFINITO.

Cenário concreto:
  1. Admin promove novo vendedor
  2. Esquece de cadastrar limite em seller_discount_limits
  3. Vendedor cria orçamento com 99% de desconto
  4. Trigger não bloqueia → margem evapora

FIX (Opção B da auditoria):
  + COALESCE(_max_allowed, 0) após o SELECT INTO
  + Mensagem distinta para caso "sem cadastro" vs "estourou limite"

ESCOPO LIMITADO:
O check de admin (role='admin') NÃO foi alterado. Verificação no banco
mostrou que existem 2 usuários reais com role='admin' (auditoria assumia 0).
Tema do "dual admin pattern" está deferido para decisão arquitetural.

VALIDAÇÃO:
  - Migration aplicada via MCP apply_migration (ADR 0006: zero db push)
  - pg_proc confirma COALESCE no corpo da função
  - 5 vendedores ativos hoje têm limite — fix não quebra ninguém
  - Vendedor novo agora recebe mensagem clara de "sem cadastro"

Versão da migration: 20260514165252 (já em prod).
Detalhes: docs/hardening/ONDA-7-DISCOUNT-FAIL-CLOSED.md
Copilot AI review requested due to automatic review settings May 14, 2026 16:59
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

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

Project Deployment Actions Updated (UTC)
promo-gifts Ready Ready Preview, Comment May 14, 2026 4:59pm

@supabase
Copy link
Copy Markdown

supabase Bot commented May 14, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln due to reaching the limit of concurrent preview branches.
Go to Project Integrations Settings ↗︎ if you wish to update this limit.


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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Walkthrough

PR implementa correção de bug de segurança no trigger de validação de desconto real. Quando vendedor não possui cadastro em seller_discount_limits, a função retornava NULL e permitia descontos ilimitados. Agora trata NULL como 0 via COALESCE, bloqueando descontos "reais" em fail-closed. Inclui migration SQL e documentação completa do processo de hardening.

Changes

Fail-Closed Discount Validation

Layer / File(s) Summary
Documentação de hardening e análise
docs/hardening/ONDA-7-DISCOUNT-FAIL-CLOSED.md
Registra bug anterior (NULL em _max_allowed permitia desconto ilimitado), decisão técnica (COALESCE NULL → 0), impacto em production, cenários antes/depois, validação empírica, e estratégia de rollback. Documenta escopo intencional (admins não alterados) e detalhes operacionais de deployment respeitando ADR 0006.
Migration SQL: implementação do fix
supabase/migrations/20260514165252_onda7_validate_quote_real_discount_fail_closed_null.sql
Atualiza trigger public.validate_quote_real_discount() para tratar NULL como 0 via COALESCE ao buscar limite do vendedor, bloqueando descontos reais quando não há cadastro. Mantém bypass para role='admin' e lança exceções com mensagens distintas: quando limite = 0 (ausência de cadastro) vs. desconto acima do limite (solicitação de aprovação).

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed O título descreve com precisão a mudança principal: correção do trigger validate_quote_real_discount para comportamento fail-closed quando NULL, referenciando a onda/card (B-4). É específico, conciso e reflete o bug corrigido.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cleanup/onda-7-validate-discount-fail-closed

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Corrige um bypass por NULL na trigger public.validate_quote_real_discount para que vendedores sem registro em seller_discount_limits não consigam aplicar descontos arbitrários, e documenta a decisão/rollout da Onda 7 (B-4).

Changes:

  • Atualiza validate_quote_real_discount() para tratar _max_allowed=NULL como 0 (fail-closed) e bloquear descontos > 0 sem aprovação.
  • Ajusta mensagens de erro para distinguir “sem cadastro” vs “estourou limite”.
  • Adiciona documentação de hardening com contexto, opções consideradas e validação em prod.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
supabase/migrations/20260514165252_onda7_validate_quote_real_discount_fail_closed_null.sql Atualiza a função trigger validate_quote_real_discount() para fail-closed quando não há limite cadastrado.
docs/hardening/ONDA-7-DISCOUNT-FAIL-CLOSED.md Documenta o bug, a correção aplicada (Opção B) e o rollout/rollback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +53 to +69
SELECT max_discount_percent INTO _max_allowed
FROM public.seller_discount_limits WHERE user_id = NEW.seller_id;

-- Onda 7 (B-4): fail-CLOSED em NULL. Vendedor sem linha em seller_discount_limits
-- nao tem mais desconto ilimitado — agora trata como 0% (precisa aprovacao para qualquer desconto).
_max_allowed := COALESCE(_max_allowed, 0);

IF _real_pct > _max_allowed THEN
IF NOT EXISTS (
SELECT 1 FROM public.discount_approval_requests
WHERE quote_id = NEW.id AND status = 'approved'
AND requested_discount_percent >= _real_pct
) THEN
-- Mensagens distintas: "sem cadastro" (Onda 7) vs "estourou limite" (comportamento original)
IF _max_allowed = 0 THEN
RAISE EXCEPTION
'Vendedor sem limite de desconto cadastrado. Solicite ao administrador o cadastro em seller_discount_limits, ou peca aprovacao para o desconto de %%%.',
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@supabase/migrations/20260514165252_onda7_validate_quote_real_discount_fail_closed_null.sql`:
- Around line 53-69: The current logic coalesces NULL to 0 and later uses
"_max_allowed = 0" to infer "no cadastro", which misclassifies an explicit 0
limit as "no cadastro"; change the query flow to detect whether a row existed
(use the SELECT ... INTO followed by checking FOUND or assign a boolean like
_has_row) and keep _max_allowed as-is (or COALESCE to 0 only for arithmetic) but
use the row-existence flag to decide the "sem cadastro" message; update the
conditional around _max_allowed and the RAISE EXCEPTION paths (referencing
_max_allowed, the SELECT INTO from public.seller_discount_limits for
NEW.seller_id, and the subsequent IF NOT EXISTS check against
public.discount_approval_requests) so explicit max_discount_percent = 0 produces
the "estourou limite" message while a missing row triggers "sem cadastro".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ed650aa5-f505-48a8-b230-0069928d3a80

📥 Commits

Reviewing files that changed from the base of the PR and between fe9aa33 and ed17527.

📒 Files selected for processing (2)
  • docs/hardening/ONDA-7-DISCOUNT-FAIL-CLOSED.md
  • supabase/migrations/20260514165252_onda7_validate_quote_real_discount_fail_closed_null.sql

Comment on lines +53 to +69
SELECT max_discount_percent INTO _max_allowed
FROM public.seller_discount_limits WHERE user_id = NEW.seller_id;

-- Onda 7 (B-4): fail-CLOSED em NULL. Vendedor sem linha em seller_discount_limits
-- nao tem mais desconto ilimitado — agora trata como 0% (precisa aprovacao para qualquer desconto).
_max_allowed := COALESCE(_max_allowed, 0);

IF _real_pct > _max_allowed THEN
IF NOT EXISTS (
SELECT 1 FROM public.discount_approval_requests
WHERE quote_id = NEW.id AND status = 'approved'
AND requested_discount_percent >= _real_pct
) THEN
-- Mensagens distintas: "sem cadastro" (Onda 7) vs "estourou limite" (comportamento original)
IF _max_allowed = 0 THEN
RAISE EXCEPTION
'Vendedor sem limite de desconto cadastrado. Solicite ao administrador o cadastro em seller_discount_limits, ou peca aprovacao para o desconto de %%%.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Diferenciação de erro pode ficar incorreta para limite explícito 0%.

Em Line 67, a regra usa _max_allowed = 0 para inferir “sem cadastro”. Se existir linha em seller_discount_limits com max_discount_percent = 0, a mensagem “sem limite cadastrado” fica errada (há cadastro, com limite zero).

💡 Ajuste sugerido
 DECLARE
   _markup       NUMERIC := COALESCE(NEW.negotiation_markup_percent, 0);
   _apparent_pct NUMERIC := COALESCE(NEW.discount_percent, 0);
   _presented    NUMERIC := COALESCE(NEW.subtotal, 0);
   _real_sub     NUMERIC;
   _final        NUMERIC;
   _real_pct     NUMERIC;
+  _max_allowed_raw NUMERIC;
   _max_allowed  NUMERIC;
+  _has_limit    BOOLEAN := FALSE;
   _is_admin     BOOLEAN;
 BEGIN
@@
-      SELECT max_discount_percent INTO _max_allowed
+      SELECT max_discount_percent INTO _max_allowed_raw
       FROM public.seller_discount_limits WHERE user_id = NEW.seller_id;
+      _has_limit := FOUND;
 
-      _max_allowed := COALESCE(_max_allowed, 0);
+      _max_allowed := COALESCE(_max_allowed_raw, 0);
@@
-          IF _max_allowed = 0 THEN
+          IF NOT _has_limit OR _max_allowed_raw IS NULL THEN
             RAISE EXCEPTION
               'Vendedor sem limite de desconto cadastrado. Solicite ao administrador o cadastro em seller_discount_limits, ou peca aprovacao para o desconto de %%%.',
               _real_pct
               USING ERRCODE = 'check_violation';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@supabase/migrations/20260514165252_onda7_validate_quote_real_discount_fail_closed_null.sql`
around lines 53 - 69, The current logic coalesces NULL to 0 and later uses
"_max_allowed = 0" to infer "no cadastro", which misclassifies an explicit 0
limit as "no cadastro"; change the query flow to detect whether a row existed
(use the SELECT ... INTO followed by checking FOUND or assign a boolean like
_has_row) and keep _max_allowed as-is (or COALESCE to 0 only for arithmetic) but
use the row-existence flag to decide the "sem cadastro" message; update the
conditional around _max_allowed and the RAISE EXCEPTION paths (referencing
_max_allowed, the SELECT INTO from public.seller_discount_limits for
NEW.seller_id, and the subsequent IF NOT EXISTS check against
public.discount_approval_requests) so explicit max_discount_percent = 0 produces
the "estourou limite" message while a missing row triggers "sem cadastro".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed175270dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


-- Onda 7 (B-4): fail-CLOSED em NULL. Vendedor sem linha em seller_discount_limits
-- nao tem mais desconto ilimitado — agora trata como 0% (precisa aprovacao para qualquer desconto).
_max_allowed := COALESCE(_max_allowed, 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat null limits as exceeded in the quote UI

With this migration, a non-admin seller who has no seller_discount_limits row now has an effective limit of 0%, so any positive real discount on a draft/pending quote raises here. I checked the quote builder path: useQuoteBuilderState currently returns false for isDiscountExceeded when maxDiscountPercent === null, and it only creates approval requests when maxDiscountPercent !== null, so the UI still shows the normal create/save action and then hits this database exception instead of offering the approval flow. Either the frontend needs to treat a missing limit as 0 or the migration needs a compatible path for creating approval requests before blocking these saves.

Useful? React with 👍 / 👎.

NEW.real_subtotal := ROUND(_real_sub, 2);
NEW.real_discount_percent := _real_pct;

IF NEW.status IN ('draft', 'pending') AND NEW.seller_id IS NOT NULL AND _real_pct > 0 THEN
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the fail-closed check outside draft/pending

This fail-closed logic only runs for draft and pending, but I checked the active quote RLS and quotes_insert_scope/quotes_update_scope allow a seller to insert or update their own quote without a status restriction, while pending_approval/sent are valid quote statuses. A seller with no discount-limit row can therefore submit the same discounted quote as pending_approval or sent and skip the _max_allowed check entirely; move the positive-discount validation to all seller-controlled statuses, or require an approved request before any status can bypass it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="supabase/migrations/20260514165252_onda7_validate_quote_real_discount_fail_closed_null.sql">

<violation number="1" location="supabase/migrations/20260514165252_onda7_validate_quote_real_discount_fail_closed_null.sql:67">
P2: A condição `_max_allowed = 0` não distingue “sem cadastro” de “limite cadastrado em 0%”, então pode exibir a mensagem errada para vendedores com limite real de 0%.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

AND requested_discount_percent >= _real_pct
) THEN
-- Mensagens distintas: "sem cadastro" (Onda 7) vs "estourou limite" (comportamento original)
IF _max_allowed = 0 THEN
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A condição _max_allowed = 0 não distingue “sem cadastro” de “limite cadastrado em 0%”, então pode exibir a mensagem errada para vendedores com limite real de 0%.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/migrations/20260514165252_onda7_validate_quote_real_discount_fail_closed_null.sql, line 67:

<comment>A condição `_max_allowed = 0` não distingue “sem cadastro” de “limite cadastrado em 0%”, então pode exibir a mensagem errada para vendedores com limite real de 0%.</comment>

<file context>
@@ -0,0 +1,87 @@
+            AND requested_discount_percent >= _real_pct
+        ) THEN
+          -- Mensagens distintas: "sem cadastro" (Onda 7) vs "estourou limite" (comportamento original)
+          IF _max_allowed = 0 THEN
+            RAISE EXCEPTION
+              'Vendedor sem limite de desconto cadastrado. Solicite ao administrador o cadastro em seller_discount_limits, ou peca aprovacao para o desconto de %%%.',
</file context>
Fix with Cubic

@adm01-debug adm01-debug merged commit 18eb05a into main May 14, 2026
23 of 26 checks passed
@adm01-debug adm01-debug deleted the cleanup/onda-7-validate-discount-fail-closed branch May 14, 2026 17:04
adm01-debug pushed a commit that referenced this pull request May 14, 2026
…I e testes

- Resolve conflitos em baselines (ESLint 853, TSC 1262) e test files
- Corrige non-null assertion em sentry.ts (lint-staged pre-commit)
- Mantém: SidebarNavGroup tests fixed, CI workflow fixes, baseline melhorado

https://claude.ai/code/session_01MuNDxFSRRaJLsvkBdyQ2dK
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.

2 participants