Skip to content

fix(db): Onda 17 — fn_quotes_recalc aplica markup + discount_percent (item 5.1)#212

Merged
adm01-debug merged 1 commit into
mainfrom
hardening/onda-17-quotes-recalc-completo
May 15, 2026
Merged

fix(db): Onda 17 — fn_quotes_recalc aplica markup + discount_percent (item 5.1)#212
adm01-debug merged 1 commit into
mainfrom
hardening/onda-17-quotes-recalc-completo

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

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

🎯 Objetivo

Atacar o item 5.1 da auditoria pré-prod (integridade financeira) corrigindo bug descoberto durante inspeção dos triggers existentes.

🔍 Estado descoberto durante pré-flight

Issue auditoria 5.1 Status pré-PR Pós-PR
1. subtotal gravado pelo cliente sem recálculo ✅ já tinha trigger
2. discount_amount inconsistente com discount_percent ⚠️ trigger ignorava corrigido
3. Floating point JS arredonda numeric(12,2) server-side
4. UPDATE em items quando status='approved' ✅ immutability trigger
BUG novo descoberto: drift de real_subtotal 🔴 ativo corrigido

🚨 Bug concreto que esta PR corrige

trg_quotes_calc_real_values (BEFORE em quotes):
  real_subtotal := subtotal / (1 + markup/100)
  ← assume que NEW.subtotal vem COM markup

trg_quotes_recalc_from_items (AFTER em quote_items):
  UPDATE quotes SET subtotal := SUM(qty*price + perso)
  ← grava SEM markup ❌

Resultado: qualquer INSERT/UPDATE/DELETE em items corrompe real_subtotal.

Cenário do bug:

1. Quote com markup=10%, items totalizam 1000
2. Frontend envia subtotal=1100 → BEFORE: real_subtotal=1100/1.1=1000 ✅
3. Cliente adiciona 1 item: INSERT quote_items
4. AFTER faz UPDATE quotes SET subtotal=1000 (sem markup!) ❌
5. BEFORE refaz: real_subtotal=1000/1.1=909.09 ❌ CORROMPIDO

real_subtotal é usado para validar alçada de desconto do vendedor, então o bug poderia bloquear ou liberar descontos incorretamente.

📦 Fix — replicar fórmula completa do frontend

-- Antes:
_new_subtotal := SUM(qty*price + perso);           -- sem markup
_new_total := _new_subtotal - discount_amount;     -- sem disc_pct, sem shipping

-- Depois (espelha calculateQuoteTotals do frontend):
_real_subtotal := SUM(qty*unit_price + personalization_cost);
_new_subtotal := ROUND(_real_subtotal * (1 + markup/100.0), 2);     -- ✅ markup
_disc_value := disc_pct > 0
  ? ROUND(_new_subtotal * disc_pct/100.0, 2)                         -- ✅ disc_pct
  : discount_amount_db;
_ship_value := shipping_type IN ('fob','fob_pre') ? shipping_cost : 0; -- ✅ FOB
_new_total := _new_subtotal - _disc_value + _ship_value;

Bonus: grava discount_amount derivado de discount_percent (reconcilia automaticamente — resolve issue 2 da auditoria).

🧪 Validação em PROD via BEGIN/ROLLBACK (5 cenários)

# Cenário Esperado Resultado
1 markup 10%, 2 items 5×R$100=1000 subtotal=1100, real=1000, total=1100
2 + discount_percent=5% disc_amt=55, total=1045
3 + shipping FOB R$200 total=1245
4 + item R$500 (real=1500) subtotal=1650, real=1500, disc=82.50, total=1767.50
5 status=approved + UPDATE items bloqueado por immutability

Quotes existentes em PROD (3 quotes, todos markup=0 sem desconto): estado idêntico antes/depois (snapshot validado).

🌐 Impacto

  • Frontend: zero impacto — mesmo retorno (a fórmula que o trigger usa é exatamente o que o frontend calcula localmente)
  • Quotes legados (markup=0): zero alteração nos valores
  • Quotes futuros com markup > 0: real_subtotal agora consistente entre fluxo via wizard E via INSERT/UPDATE direto em items
  • Validação de alçada de desconto: agora confiável

🎯 Próximas ondas

  • Onda 18 — Roles consolidation 6→3 (audit 6.1)
  • Onda 19numeric(12,2) em todas as colunas monetárias (audit 5.4)
  • Onda 20 — MFA enforced para dev/supervisor (audit 6.4)

📚 Ref


Summary by cubic

Fixes quote total recalculation by applying markup and discount percent, preventing real_subtotal drift after item changes. The server function now mirrors the frontend formula to keep subtotal, total, and discount consistent.

  • Bug Fixes
    • Rewrote fn_quotes_recalc_subtotal_from_items to apply negotiation_markup_percent, prefer discount_percent over discount_amount, include FOB shipping, and update subtotal, total, and discount_amount.
    • Eliminates the bug where item updates stored subtotal without markup, corrupting real_subtotal used in discount authorization.
    • Skips approved/converted quotes and only updates when values change. Resolves audit item 5.1.

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

Summary by CodeRabbit

  • New Features
    • Aprimorado o sistema de cálculo automático de subtotais, totais e descontos em orçamentos. Os valores agora são recalculados de forma automática e mais precisa com base nos itens incluídos no orçamento, considerando descontos percentuais e valores fixos, markups de negociação, custos de frete e despesas de personalização de produtos conforme necessário.

Review Change Stack

… + disc_pct (item 5.1)

Corrige bug onde a funcao recalculadora nao aplicava negotiation_markup_percent
nem discount_percent, causando drift no real_subtotal usado em validacao de
alcada. Replica formula completa do frontend (calculateQuoteTotals).

Validado em PROD via BEGIN/ROLLBACK em 5 cenarios. Aplicada via apply_migration.

Ref: docs/AUDITORIA-PROFUNDA-PROMOGIFTS-PRE-PROD.md (item 5.1)
Copilot AI review requested due to automatic review settings May 15, 2026 00:00
@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 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 15, 2026 0:01am

@supabase
Copy link
Copy Markdown

supabase Bot commented May 15, 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 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 015bc9ef-5f54-4696-8636-878d9487fd0a

📥 Commits

Reviewing files that changed from the base of the PR and between 2e0032b and b7bc072.

📒 Files selected for processing (1)
  • supabase/migrations/20260515000000_onda17_fn_quotes_recalc_subtotal_completo.sql

Visão geral

Adicionada migration com função trigger PL/pgSQL (SECURITY DEFINER) que recalcula automaticamente subtotal, total e discount_amount de quotes quando seus itens sofrem modificações, respeitando markup com limite [0–50%], prioridade de desconto percentual, restrição de frete a tipos FOB e bloqueio de atualização em quotes aprovadas/convertidas.

Mudanças

Recálculo automático de subtotais

Layer / File(s) Resumo
Função trigger de recálculo de subtotais
supabase/migrations/20260515000000_onda17_fn_quotes_recalc_subtotal_completo.sql
Função fn_quotes_recalc_subtotal_from_items() que, ao modificar quote_items, busca dados da quote, soma itens com custo de personalização, aplica negotiation_markup_percent (capped 0–50%), calcula desconto priorizando percentual, inclui frete apenas para shipping_type em ('fob','fob_pre'), bloqueia atualização em status approved/converted, e faz UPDATE condicional para evitar loops de trigger, retornando NEW/OLD para compatibilidade.

Pontos de validação

  • Prevenção de loops: UPDATE condicional verifica mudanças antes de atualizar, retorna NEW/OLD apropriado
  • Validação de markup: negotiation_markup_percent limitado ao intervalo [0, 50]
  • Bloqueio de estados: Quotes com status approved ou converted não sofrem recálculo
  • Prioridade de desconto: discount_percent > 0 usa percentual; caso contrário, discount_amount
  • Restrição de frete: Incluído apenas se shipping_type in ('fob','fob_pre')
  • ⚠️ SECURITY DEFINER: Função executada com privilégios elevados; valide se o role padrão tem permissões mínimas necessárias e se search_path está explicitamente definido para evitar injeção via object resolution

🎯 2 (Simple) | ⏱️ ~10 minutos

🚥 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 é específico, refere-se à mudança principal (fn_quotes_recalc aplicando markup + discount_percent) e deixa claro o contexto (item 5.1 do audit).
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 hardening/onda-17-quotes-recalc-completo

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

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: b7bc072eab

ℹ️ 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".

END IF;

-- REAL: soma pura dos itens (sem markup)
SELECT COALESCE(SUM(quantity * unit_price + COALESCE(personalization_cost, 0)), 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.

P1 Badge Read personalization totals from the persisted table

For quotes with personalization charges, this recalculation no longer matches the current data model: the app inserts those charges into quote_item_personalizations.total_cost after inserting quote_items (buildItemsInsertPayload does not write a personalization_cost field, and the generated quote_items type has no such column). When the quote-items trigger runs, this query either errors with column personalization_cost does not exist on the current schema or, on schemas that still have the legacy column, recalculates subtotals without the personalization rows and overwrites quotes.subtotal/total too low after any item change.

Useful? React with 👍 / 👎.

-- Ref: docs/AUDITORIA-PROFUNDA-PROMOGIFTS-PRE-PROD.md (item 5.1)
-- ============================================================================

CREATE OR REPLACE FUNCTION public.fn_quotes_recalc_subtotal_from_items()
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 Wire the recalculation function to quote_items

When this migration is replayed from the repository in a fresh or staging database, it only defines the trigger function; I checked supabase/migrations for fn_quotes_recalc_subtotal_from_items/trg_quotes_recalc and found no CREATE TRIGGER that attaches it to quote_items (only an earlier REVOKE). In those environments, item INSERT/UPDATE/DELETE operations will never invoke this recalculation, so client-supplied quotes.subtotal, discount_amount, and total can remain stale despite the new function existing.

Useful? React with 👍 / 👎.

_disc_value numeric(12,2);
_new_total numeric(12,2);
BEGIN
_quote_id := COALESCE(NEW.quote_id, OLD.quote_id);
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 Recalculate both quotes when quote_id changes

If an admin/API update moves an existing quote_items row from one draft quote to another, this picks only NEW.quote_id and recalculates the destination quote while leaving the source quote totals with the moved item still included. The column is updatable in the generated Supabase type and the quote-items policy allows updates by quote ownership, so a reassignment can leave the old quote's subtotal, discount_amount, and total stale until another item change happens on that old quote.

Useful? React with 👍 / 👎.

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

This PR updates the server-side quote subtotal recalculation function so quote totals are recomputed from items with markup, discount percent, and FOB shipping logic.

Changes:

  • Replaces fn_quotes_recalc_subtotal_from_items with fuller quote total calculation logic.
  • Reconciles discount_amount from discount_percent when item changes trigger recalculation.
  • Adds migration/audit notes documenting the Onda 17 financial-integrity fix.
Comments suppressed due to low confidence (1)

supabase/migrations/20260515000000_onda17_fn_quotes_recalc_subtotal_completo.sql:90

  • This recalc is tied to quote_items, but the app saves personalization costs in quote_item_personalizations after inserting the item rows. Without recalculating again when those personalization rows are inserted/updated/deleted, quotes created or edited with personalization can be recalculated before those costs exist and end up with totals that exclude them.
  FROM public.quote_items
  WHERE quote_id = _quote_id;

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

Comment on lines +87 to +90
SELECT COALESCE(SUM(quantity * unit_price + COALESCE(personalization_cost, 0)), 0)
INTO _real_subtotal
FROM public.quote_items
WHERE quote_id = _quote_id;
IF _disc_pct > 0 THEN
_disc_value := ROUND(_new_subtotal * (_disc_pct / 100.0), 2);
ELSE
_disc_value := _disc_amount_db;
Comment on lines +68 to +118
_quote_id := COALESCE(NEW.quote_id, OLD.quote_id);
IF _quote_id IS NULL THEN RETURN COALESCE(NEW, OLD); END IF;

SELECT
status,
LEAST(50, GREATEST(0, COALESCE(negotiation_markup_percent, 0))),
COALESCE(discount_amount, 0),
COALESCE(discount_percent, 0),
shipping_type,
COALESCE(shipping_cost, 0)
INTO _quote_status, _markup, _disc_amount_db, _disc_pct, _ship_type, _ship_cost
FROM public.quotes WHERE id = _quote_id;

-- Nao mexer em quotes aprovados/convertidos (imutaveis)
IF _quote_status IN ('approved', 'converted') THEN
RETURN COALESCE(NEW, OLD);
END IF;

-- REAL: soma pura dos itens (sem markup)
SELECT COALESCE(SUM(quantity * unit_price + COALESCE(personalization_cost, 0)), 0)
INTO _real_subtotal
FROM public.quote_items
WHERE quote_id = _quote_id;

-- APRESENTADO ao cliente: aplica markup
_new_subtotal := ROUND(_real_subtotal * (1 + _markup / 100.0), 2);

-- DESCONTO: discount_percent tem prioridade (espelha logica do frontend)
IF _disc_pct > 0 THEN
_disc_value := ROUND(_new_subtotal * (_disc_pct / 100.0), 2);
ELSE
_disc_value := _disc_amount_db;
END IF;

-- FRETE FOB (somente FOB entra no total)
_ship_value := CASE WHEN _ship_type IN ('fob', 'fob_pre') THEN _ship_cost ELSE 0 END;

-- TOTAL final
_new_total := _new_subtotal - _disc_value + _ship_value;

-- UPDATE apenas se mudou (evita loop com trigger BEFORE em quotes)
UPDATE public.quotes
SET subtotal = _new_subtotal,
total = _new_total,
discount_amount = _disc_value,
updated_at = now()
WHERE id = _quote_id
AND (subtotal IS DISTINCT FROM _new_subtotal
OR total IS DISTINCT FROM _new_total
OR discount_amount IS DISTINCT FROM _disc_value);

Comment on lines +87 to +90
SELECT COALESCE(SUM(quantity * unit_price + COALESCE(personalization_cost, 0)), 0)
INTO _real_subtotal
FROM public.quote_items
WHERE quote_id = _quote_id;
-- _new_subtotal := SUM(qty*price + perso); -- sem markup
-- _new_total := _new_subtotal - discount_amount; -- sem disc_pct, sem shipping
--
-- Como trg_quotes_calc_real_values (BEFORE em quotes) calcula
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.

2 issues found across 1 file

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/20260515000000_onda17_fn_quotes_recalc_subtotal_completo.sql">

<violation number="1" location="supabase/migrations/20260515000000_onda17_fn_quotes_recalc_subtotal_completo.sql:48">
P1: This migration only defines the function (`CREATE OR REPLACE FUNCTION`) but does not include a `CREATE TRIGGER` statement attaching it to `quote_items`. On fresh or staging databases built from the migration history, the recalculation will never fire because no trigger invokes it. Consider adding a `CREATE TRIGGER ... AFTER INSERT OR UPDATE OR DELETE ON quote_items` (with `IF NOT EXISTS` or `DROP TRIGGER IF EXISTS` guard) to ensure the function is wired up in all environments.</violation>

<violation number="2" location="supabase/migrations/20260515000000_onda17_fn_quotes_recalc_subtotal_completo.sql:87">
P0: The query references `personalization_cost` on `quote_items`, but the current schema stores personalization amounts in `quote_item_personalizations.total_cost`. If this column doesn't exist on `quote_items`, the trigger will fail with a runtime error on every item INSERT/UPDATE/DELETE. If it exists as a legacy column but is never populated, personalization costs will be silently excluded from recalculated totals. The aggregate should join or subquery against `quote_item_personalizations` to pull `total_cost`.</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.

END IF;

-- REAL: soma pura dos itens (sem markup)
SELECT COALESCE(SUM(quantity * unit_price + COALESCE(personalization_cost, 0)), 0)
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

P0: The query references personalization_cost on quote_items, but the current schema stores personalization amounts in quote_item_personalizations.total_cost. If this column doesn't exist on quote_items, the trigger will fail with a runtime error on every item INSERT/UPDATE/DELETE. If it exists as a legacy column but is never populated, personalization costs will be silently excluded from recalculated totals. The aggregate should join or subquery against quote_item_personalizations to pull total_cost.

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

<comment>The query references `personalization_cost` on `quote_items`, but the current schema stores personalization amounts in `quote_item_personalizations.total_cost`. If this column doesn't exist on `quote_items`, the trigger will fail with a runtime error on every item INSERT/UPDATE/DELETE. If it exists as a legacy column but is never populated, personalization costs will be silently excluded from recalculated totals. The aggregate should join or subquery against `quote_item_personalizations` to pull `total_cost`.</comment>

<file context>
@@ -0,0 +1,126 @@
+  END IF;
+
+  -- REAL: soma pura dos itens (sem markup)
+  SELECT COALESCE(SUM(quantity * unit_price + COALESCE(personalization_cost, 0)), 0)
+  INTO _real_subtotal
+  FROM public.quote_items
</file context>
Fix with Cubic

-- Ref: docs/AUDITORIA-PROFUNDA-PROMOGIFTS-PRE-PROD.md (item 5.1)
-- ============================================================================

CREATE OR REPLACE FUNCTION public.fn_quotes_recalc_subtotal_from_items()
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

P1: This migration only defines the function (CREATE OR REPLACE FUNCTION) but does not include a CREATE TRIGGER statement attaching it to quote_items. On fresh or staging databases built from the migration history, the recalculation will never fire because no trigger invokes it. Consider adding a CREATE TRIGGER ... AFTER INSERT OR UPDATE OR DELETE ON quote_items (with IF NOT EXISTS or DROP TRIGGER IF EXISTS guard) to ensure the function is wired up in all environments.

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

<comment>This migration only defines the function (`CREATE OR REPLACE FUNCTION`) but does not include a `CREATE TRIGGER` statement attaching it to `quote_items`. On fresh or staging databases built from the migration history, the recalculation will never fire because no trigger invokes it. Consider adding a `CREATE TRIGGER ... AFTER INSERT OR UPDATE OR DELETE ON quote_items` (with `IF NOT EXISTS` or `DROP TRIGGER IF EXISTS` guard) to ensure the function is wired up in all environments.</comment>

<file context>
@@ -0,0 +1,126 @@
+-- Ref: docs/AUDITORIA-PROFUNDA-PROMOGIFTS-PRE-PROD.md (item 5.1)
+-- ============================================================================
+
+CREATE OR REPLACE FUNCTION public.fn_quotes_recalc_subtotal_from_items()
+RETURNS trigger
+LANGUAGE plpgsql
</file context>
Fix with Cubic

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