fix(quotes): alinhar shipping_type=fob vs fob_pre em UI/RPC/dados#5
fix(quotes): alinhar shipping_type=fob vs fob_pre em UI/RPC/dados#5adm01-debug wants to merge 1 commit into
Conversation
BUG: em 18/mai o helper de calculo (quoteHelpers.ts:45) mudou pra considerar
shipping_cost APENAS quando shipping_type='fob_pre'. Mas 3 outras camadas
ficaram com a regra antiga ('fob' OR 'fob_pre'):
- QuoteTotalsSummary.tsx (UI que mostra o total na visualizacao)
- useQuoteViewData.ts (view do orcamento)
- fn_quotes_recalc_subtotal_from_items (trigger RPC server-side)
Resultado: orcamentos com shipping_type='fob' AND shipping_cost>0
mostravam total X na UI mas SALVAVAM Y no banco (o que o trigger recalculasse
dependia de se quote_items foi alterado ou nao). Bug financeiro silencioso.
FIX: alinhar TODAS as 4 camadas com a semantica adotada pelo refactor:
- cif -> Cortesia (sem custo)
- fob -> Cliente paga frete (sem cost no orcamento)
- fob_pre -> FOB Pre-negociado (cost no orcamento, repassado ao cliente)
5 arquivos:
1. QuoteTotalsSummary.tsx: shipping_type==='fob_pre' apenas
2. useQuoteViewData.ts: shipping_type==='fob_pre' apenas
3. types/quote.ts: comentario doc atualizado
4. NOVO migration fix_fn_quotes_recalc_only_fob_pre: trigger RPC alinhado
5. NOVO migration backfill_legacy_fob_with_cost: converte orcamentos legados
com shipping_type='fob' AND shipping_cost>0 para 'fob_pre' (semantica
real); idempotente, nao toca approved/converted
Validacao TS: edits cirurgicos (8 ins, 5 del em 3 arquivos).
Validacao SQL: funcao recriada via CREATE OR REPLACE, preserva 100% da
logica de markup/discount/immutability. Backfill com WHERE explicito.
NOTA: as 2 migrations sao APLICADAS em F3 (transferencia Supabase), nao
agora — banco em uso (pqp) e gerenciado pelo Lovable web. Apos aplicar,
verificar com:
SELECT id, status, shipping_type, shipping_cost, total FROM quotes
WHERE shipping_type IN ('fob','fob_pre');
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
WalkthroughO PR altera a semântica de cálculo de frete em orçamentos: ChangesAlteração de semântica de frete FOB
Sequence Diagram(s)sequenceDiagram
participant Client as Frontend
participant API as API/Hook
participant DB as Banco
Client->>API: Quote com shipping_type=fob_pre e shipping_cost
API->>Client: proposalData com shipValue incluído
Client->>Client: Renderiza "FOB Pré-negociado" com valor
alt Item do quote alterado
DB->>DB: fn_quotes_recalc_subtotal_from_items disparado
DB->>DB: Verifica status (não approved/converted)
DB->>DB: Calcula total COM shipping_cost (fob_pre)
DB->>DB: UPDATE quotes.total
end
Client->>API: Quote com shipping_type=fob
API->>Client: proposalData com shipValue=0
Client->>Client: Renderiza "Por conta do cliente" sem valor
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Pull request overview
Alinha a semântica de shipping_type entre frontend (UI + view) e backend (trigger RPC + dados) para que shipping_cost só componha o total quando shipping_type = 'fob_pre', eliminando divergências financeiras silenciosas.
Changes:
- Frontend: cálculo/label de frete passam a considerar custo apenas em
fob_pre(UI euseQuoteViewData). - Backend:
fn_quotes_recalc_subtotal_from_itemspassa a somar frete apenas emfob_pre. - Dados: adiciona migration de backfill para converter
fobcomshipping_cost > 0parafob_pre.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/components/quotes/QuoteTotalsSummary.tsx |
Ajusta regra de soma do frete e labels exibidos no resumo de totais. |
src/pages/quotes/quote-view/useQuoteViewData.ts |
Ajusta cálculo do total no payload usado para render/PDF para considerar frete apenas em fob_pre. |
src/types/quote.ts |
Atualiza comentário documental para explicitar a semântica de shipping_type. |
supabase/migrations/20260519225000_fix_fn_quotes_recalc_only_fob_pre.sql |
Atualiza função trigger para incluir frete somente em fob_pre. |
supabase/migrations/20260519225100_backfill_legacy_fob_with_cost.sql |
Backfill de dados para reclassificar fob+cost legado como fob_pre. |
Comments suppressed due to low confidence (2)
supabase/migrations/20260519225100_backfill_legacy_fob_with_cost.sql:39
- O UPDATE (e o comentário acima) dizem que “não recalcula totals”, mas esta migration converte exatamente os casos do bug (shipping_type='fob' com shipping_cost>0) onde o
totalpode ter sido persistido sem o frete. Apenas trocarshipping_typeparafob_prenão corrige essestotals e pode manter divergência em telas/rotinas que usamquotes.total. Considere atualizartotalna mesma migration (p.ex. com base em subtotal/discount_amount + shipping_cost) ou forçar um recálculo server-side para os registros convertidos, mantendo consistência imediata.
UPDATE public.quotes
SET shipping_type = 'fob_pre',
updated_at = now()
WHERE shipping_type = 'fob'
AND shipping_cost > 0
AND status NOT IN ('approved', 'converted');
supabase/migrations/20260519225100_backfill_legacy_fob_with_cost.sql:48
- O bloco
DO $$ ... GET DIAGNOSTICS ... ROW_COUNTnão vai capturar o número de linhas afetadas pelo UPDATE anterior, porqueROW_COUNTé local ao último comando SQL executado dentro do próprio bloco DO. Do jeito atual, a mensagem de audit tende a sempre reportar 0. Se a intenção é logar o total convertido, una o UPDATE e o GET DIAGNOSTICS no mesmo bloco (ou useWITH updated AS (UPDATE ... RETURNING 1) SELECT count(*)).
-- Audit log: registrar conversoes para forensics
DO $$
DECLARE
_converted_count integer;
BEGIN
GET DIAGNOSTICS _converted_count = ROW_COUNT;
RAISE NOTICE '[backfill_legacy_fob] Converted % quotes from fob+cost to fob_pre', _converted_count;
END $$;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| -- Persistir | ||
| UPDATE public.quotes | ||
| SET | ||
| subtotal = _new_subtotal, | ||
| discount_amount = _disc_value, | ||
| total = _new_total, | ||
| updated_at = now() | ||
| WHERE id = _quote_id; |
| -- - Não recalcula totals (sera feito pelo trigger fn_quotes_recalc na proxima | ||
| -- alteração de quote_items, ou pode ser feito manualmente via touch). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8d77822e03
ℹ️ 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".
| quote.shipping_type === 'fob_pre' | ||
| ? quote.shipping_cost || 0 |
There was a problem hiding this comment.
Handle legacy fob+cost quotes in total calculation
This now excludes shipping_cost whenever shipping_type='fob', but the companion backfill explicitly leaves approved/converted rows untouched (status NOT IN ('approved','converted')), so immutable legacy quotes can still have shipping_type='fob' with positive freight. For those records, the view/PDF total becomes lower than the persisted negotiated total, creating a customer-facing mismatch on historical approved quotes.
Useful? React with 👍 / 👎.
| subtotal = _new_subtotal, | ||
| discount_amount = _disc_value, | ||
| total = _new_total, | ||
| updated_at = now() | ||
| WHERE id = _quote_id; |
There was a problem hiding this comment.
Reinstate no-op guard on quote totals UPDATE
This replacement removed the prior IS DISTINCT FROM safeguard (present in 20260515000000_onda17_fn_quotes_recalc_subtotal_completo.sql), so the trigger function now rewrites quotes on every invocation even when subtotal/discount/total did not change. Because quotes has a BEFORE UPDATE version trigger (trg_quotes_increment_version in 20260417112433_c918513d-709c-44cc-87e6-5752a1818c3b.sql), non-financial quote_items edits can now cause unnecessary version bumps and updated_at churn, increasing optimistic-lock conflicts and noisy history.
Useful? React with 👍 / 👎.
| const shippingValue = shippingType === "fob_pre" | ||
| ? (shippingCost || 0) : 0; |
There was a problem hiding this comment.
Preserve total consistency in QuoteTotalsSummary for legacy FOB
The summary now drops freight whenever shippingType === 'fob', but the backfill intentionally skips approved/converted rows, so legacy immutable quotes can still be fob with positive shipping_cost and persisted totals that include freight. In those cases this card shows a lower recomputed total than the stored approved amount, which can misstate the contracted value on the quote view.
Useful? React with 👍 / 👎.
Anexa seção "UPDATE 2" ao log do T14 cobrindo os 4 commits adicionais que aconteceram entre 20:45 e 20:51 UTC (~6 min): 1. 5a16641 — test.fixme em 22.1 e 22.2 (bugs pré-existentes, não T14) 2. 8665829 — 🚨 corrupção base64-duplo no playwright.config.ts (sessão paralela) 3. c033e71 — script T-FIX-5 (não relacionado, na linhagem) 4. 3a9d718 — recovery + per-test timeout 90s aplicado em UTF-8 puro Estado real do T14: fix-complete (todos os patches necessários aplicados), mas verificação direta do smoke gate ainda não aconteceu — está bloqueado upstream pelos Bugs #3 (Test Coverage) e #5 (ESLint baseline gate) do plano "10/10", que são tarefas separadas. Validação INDIRETA OK: o job "Edge Functions — Deno typecheck" do CI #463 rodou normalmente, confirmando que playwright.config.ts é TypeScript válido (seria o primeiro a quebrar se o arquivo ainda estivesse base64-corrompido). Documenta também a lição da corrupção: - github_create_or_update_file ESPERA UTF-8 plain text no `content` - a tool faz seu próprio base64-encode antes de submeter ao Git - passar string já em base64 produz blob double-encoded "salvo OK" mas inválido Refs: T14 do plano redeploy 2026-05; Bugs #3 e #5 do plano "10/10".
…TS2698 Commit 5bde589 introduced the importOriginal pattern but didn't type it. Without a type parameter, importOriginal() returns Promise<unknown>, so `const actual = await importOriginal()` makes actual: unknown — spreading unknown is TS2698. Fix: `import type * as ProductsBarrel` + `importOriginal<typeof ProductsBarrel>()` gives TS a concrete module type so { ...actual, ... } is valid. Top-of-file import-type form is used to satisfy @typescript-eslint/consistent-type-imports (inline import() type annotations are forbidden). Net drift: 1330 errors (-3 from baseline 1333) — typecheck gate now green. Validates plan 10/10 #3, #4 (and #5 T-FIX-5 guardrail) on top of #138. All 6 quality gates green locally: - typecheck: 0 regressions (drift -3) - lint:baseline: 0 regressions (drift -47) - check:no-inline-cors: 0 violations - check:edge-cors: 81 functions via SSOT - check:toast-leaks: 0 new (106 legacy) - check:proposed-configs: no orphans Vitest validated on 24 modified test files (1 skipped intentionally per FIXME(useCatalogState-unit-OOM)): NotificationDrawer x6 (closes #119, #120, #121), useCatalogState, MainLayout breadcrumbs, AdminStandardRules, AppLogo.visual, PriceFreshnessBadge snapshots, QuoteBuilderDiscount, QuoteBuilderStepper, AuthBranding x2, MagicUp, simulation-orchestrator, syntax-integrity, ProtectedRoute, AdminRoute, DevRoute, AdminConexoesAccess, route-no-error-element, reduced-app-navigation.
…eset, dead code Bug #2: handleOpenChange não revogava blobUrlRef.current ao fechar. Cada ciclo abrir→gerar→fechar vazava um blob:// na memória do browser. Fix: revokeBlobUrl() extraído como helper, chamado no fechamento e antes de criar novo blob. Bug #4: ESC ou clique fora fechava o dialog durante generateProposalPDFv2. A operação assíncrona continuava em background (zumbi), podendo tentar atualizar state em componente já desmontado. Fix: onInteractOutside + onEscapeKeyDown previnem fechamento enquanto stage === 'generating'. Mensagem 'Aguarde, não feche esta janela' adicionada. Bug #5: progressLabel e pdfVersion não resetavam ao fechar o dialog. Na próxima abertura, progressLabel mostrava valor antigo; pdfVersion começava em v2/v3 em vez de v1. Fix: ambos resetados em handleOpenChange junto com demais states. Bug #8: clientPhone, approvalLink, onWhatsApp, onShareLink declarados na interface mas nunca usados. Marcados como @deprecated para backward-compat. Bug #9: ActionButton.variant 'whatsapp' tinha CSS idêntico a 'primary'. Tipo simplificado para 'default' | 'primary'.
…Propostas PDF (#438) * fix(pdf-generator): root.unmount() movido para finally externo — sem memory leak em erro Bug: se html2canvas lançava exceção durante captura de qualquer página, root.unmount() nunca era chamado. ReactDOM.createRoot ficava pendurado, consumindo memória indefinidamente. Fix: - `let root` declarado fora do try block - `root?.unmount()` movido para o finally externo (sempre executa) - `document.body.removeChild(container)` mantido no mesmo finally - img.complete check aprimorado: naturalWidth > 0 evita tratar imagem com src vazio como 'carregada' * fix(PdfGenerationDialog): 5 bugs — memory leak, zombie async, state reset, dead code Bug #2: handleOpenChange não revogava blobUrlRef.current ao fechar. Cada ciclo abrir→gerar→fechar vazava um blob:// na memória do browser. Fix: revokeBlobUrl() extraído como helper, chamado no fechamento e antes de criar novo blob. Bug #4: ESC ou clique fora fechava o dialog durante generateProposalPDFv2. A operação assíncrona continuava em background (zumbi), podendo tentar atualizar state em componente já desmontado. Fix: onInteractOutside + onEscapeKeyDown previnem fechamento enquanto stage === 'generating'. Mensagem 'Aguarde, não feche esta janela' adicionada. Bug #5: progressLabel e pdfVersion não resetavam ao fechar o dialog. Na próxima abertura, progressLabel mostrava valor antigo; pdfVersion começava em v2/v3 em vez de v1. Fix: ambos resetados em handleOpenChange junto com demais states. Bug #8: clientPhone, approvalLink, onWhatsApp, onShareLink declarados na interface mas nunca usados. Marcados como @deprecated para backward-compat. Bug #9: ActionButton.variant 'whatsapp' tinha CSS idêntico a 'primary'. Tipo simplificado para 'default' | 'primary'. * fix(ProposalProductTable): imagens em branco no PDF + coluna Total ausente Bug #3a: loading="lazy" em imagens offscreen. O browser só carrega lazy quando o elemento entra no viewport. O template é renderizado a -10000px (fora do viewport), então nenhuma imagem carregava → PDF gerado com espaços em branco. Fix: loading="eager". Bug #3b: useState("") causa img.complete falso positivo. Com src="" inicial, img.complete retorna true imediatamente (sem nenhuma imagem ter sido carregada). O generator do PDF interpreta isso como "imagem carregada" e chama html2canvas com src vazio. Fix: useState(src) — imagem sempre tem src válido desde o início; processLogoTransparent atualiza depois com versão sem fundo. Bug #6: coluna "Total" ausente na tabela de produtos. Cliente recebia proposta sem totais por linha, precisando calcular manualmente. Fix: nova coluna Total = allInUnitPrice × quantity − itemDiscount. colSpan dos cabeçalhos de kit ajustado (3→4 / 4→5). * fix(PropostaComercialTailwind): mutable itemIndex in render → React 18 StrictMode bug Bug #7: `let itemIndex = 0` era mutado dentro do .map() do JSX. Em React 18 StrictMode (desenvolvimento), React renderiza o componente duas vezes para detectar side-effects indesejados. Na segunda passagem, itemIndex já tinha o valor acumulado da primeira, dobrando os índices de linha: item 0 virava item 4, item 4 virava item 8, etc. Resultado: numeração errada das linhas na tabela do PDF em dev. Fix: startIndices[] pré-computado com Array.reduce() antes do JSX. Imutável — não sofre efeito de dupla renderização. Substitui: `const startIdx = itemIndex; itemIndex += pageItems.length;` Por: `const startIdx = startIndices[pageIdx];`
O que
Corrige bug financeiro silencioso em
shipping_typeque causava divergência entre UI, view, banco e RPC server-side.Diagnóstico
Em 18/mai (commit
72c8639), o frontend (src/hooks/quotes/quoteHelpers.ts:45) mudou para considerarshipping_costAPENAS quandoshipping_type='fob_pre'. Mas 3 outras camadas ficaram com a regra antiga'fob' || 'fob_pre':quoteHelpers.ts:45(salva no banco)'fob_pre'(mudou em 18/mai)QuoteTotalsSummary.tsx:32(exibe UI)'fob' || 'fob_pre''fob_pre'useQuoteViewData.ts:72(view)'fob' || 'fob_pre''fob_pre'fn_quotes_recalc(RPC server)IN ('fob','fob_pre')'fob_pre'Impacto do bug: orçamentos com
shipping_type='fob' AND shipping_cost>0mostravam valor X na UI mas SALVAVAM Y no banco (o que o triggerfn_quotes_recalccalculasse dependia de sequote_itemsfoi alterado ou não). Cliente fecha contrato com valor visto, sistema persiste outro.Solução
Semântica adotada (alinhada com o refactor de 18/mai):
cif→ Cortesia (sem custo no orçamento)fob→ Cliente paga frete diretamente (sem cost no orçamento)fob_pre→ FOB Pré-negociado (cost no orçamento, repassado ao cliente)Arquivos
Frontend (3 arquivos, 8+/5-)
src/components/quotes/QuoteTotalsSummary.tsx—shippingValuesó somashippingCostse'fob_pre'. Label da UI ajustado:'fob'→ "FOB — Por conta do cliente" (sem cost);'fob_pre'→ "FOB Pré-negociado (R$ X)".src/pages/quotes/quote-view/useQuoteViewData.ts— idem.src/types/quote.ts— comentário doc atualizado.Backend (2 migrations novas — aplicar em F3)
20260519225000_fix_fn_quotes_recalc_only_fob_pre.sql—CREATE OR REPLACEdo trigger RPC, preservando 100% da lógica de markup/discount/immutability check. Apenas a expressão de shipping foi alterada (IN ('fob','fob_pre')→= 'fob_pre').20260519225100_backfill_legacy_fob_with_cost.sql— converte orçamentos legados (shipping_type='fob' AND shipping_cost>0) para'fob_pre'(que é o que eles eram semanticamente). NÃO toca orçamentos com statusapprovedouconverted. Idempotente.Aplicação das migrations
pqpdolkaeqlyzpdpbizoé gerenciado pelo Lovable). Vão aplicar na Fase 3 (transferência Supabase).Para validar antes de aplicar:
Validação
✅ Edits TS cirúrgicos (8 ins, 5 del).
✅ SQL: função recriada com mesma assinatura/lógica, só expressão IN trocada.
✅ Backfill com
WHEREexplícito + proteção contra approved/converted.Risco
CREATE OR REPLACEmantém assinatura (não invalida triggers).fobparafob_pre). Mitigação: só converte quandoshipping_cost>0(caso onde semanticamente já era 'fob_pre'); rastreável viaupdated_at.Notas
Summary by cubic
Aligns shipping calculation so only 'fob_pre' adds shipping_cost to totals across UI, view, and RPC. Fixes the mismatch where 'fob' + cost showed one total in the UI but saved another, and backfills legacy data.
Bug Fixes
Migration
fn_quotes_recalc_subtotal_from_itemsto include shipping_cost only for 'fob_pre'.Written for commit 8d77822. Summary will update on new commits. Review in cubic