Precificação de Pedidos - Semântica Oficial
Fonte de verdade do cálculo de preços do Verto. O motor vive no backend (
nexrep-api/src/modules/orders/pricing/order-pricing.util.ts) e é espelhado no frontend (nexrep-web/src/app/(private)/orders/_lib/pricing.ts) apenas para preview instantâneo - o valor persistido é sempre o recalculado pelo servidor. A paridade entre os dois é garantida pelo fixture compartilhadopricing-cases.jsonrodando nos testes dos dois repositórios.
Versão do motor
PRICING_ENGINE_VERSION = "1.0". Todo save de pedido grava ds_versao_motor
(ex.: "1.0:ADITIVO") e dt_recalculo_preco. Pedidos anteriores ao motor têm
"legado". Qualquer mudança de fórmula exige incrementar a versão - é o
que permite reproduzir cálculos históricos.
1. Resolução do preço BASE unitário (cadeia de 4 níveis)
Dado produto + contexto (representada, coleção, tabela, variação de grade), o
ProductPriceService.calculatePrice retorna no primeiro nível que existir:
| Ordem | Origem (priceSourceType) | Significado |
|---|---|---|
| 1 | TABLE_GRADE | preço da variação na tabela selecionada |
| 2 | TABLE_BASE | preço base do produto na tabela |
| 3 | PRODUCT_GRADE | preço da variação no cadastro do produto |
| 4 | PRODUCT_BASE | preço de lista do produto (vl_preco_tabela) |
A UI exibe a origem em cada item: ✓ Tabela X (níveis 1-2) vs
⚠ Cadastro do produto (níveis 3-4, fallback).
2. Preço LÍQUIDO unitário (descontos/acréscimos do item)
Ajustes são percentuais (0-100, até 4 casas) em nex_pedido_item_ajuste.
O modo de combinação é configurável por empresa em
Configurações → Pedidos → Cálculo de Preços (nex_configuracao.order_calc_mode):
ADITIVO (default - comportamento histórico)
Todos os percentuais aplicam sobre a MESMA base:
líquido = base × (1 - Σdescontos/100 + Σacréscimos/100)
Exemplo: base 100, desc 10% + 5%, acr 3% → 100 × (1 - 0,15 + 0,03) = 88,00
CASCATA
Cada percentual aplica sobre o resultado anterior, na ordem dos ajustes
(nr_ordem):
líquido = base × Π(1 - d/100) × Π(1 + a/100)
Exemplo: base 100 → -10% = 90,00 → -5% = 85,50 → +3% = 88,07
Regras comuns: piso 0 (nunca negativo); arredondamento ROUND_HALF_UP com 2 casas, aplicado UMA única vez no resultado final (nunca em passos intermediários).
3. Totais do pedido
subtotal = Σ (quantidade × líquido unitário) [mercadoria]
ajuste_condicao = subtotal × (pc_acrescimo - pc_desconto)/100 [condição de pagamento]
total = subtotal + ajuste_condicao + frete
- A condição de pagamento aplica seus percentuais automaticamente, como linha visível do pedido - sobre a mercadoria, nunca sobre o frete. A condição deve pertencer à representada do pedido (validado no backend).
- O frete entra no total (inclusive no PDF).
- Persistidos em
nex_pedido:vl_subtotal,vl_ajuste_condicao,vl_total. Listagem, dashboard, PDF e e-mail leem esses campos - uma fonte, quatro telas, mesmo número.
4. Bonificação - fora da tela e do PDF (decisão RND-21 c, 24/09/2026)
Decidido: "ignorar por enquanto, mas não tirar do banco". O campo saiu da
revisão do pedido e do PDF; o formulário não manda mais bonusValue, e a API
não grava a coluna (o campo do corpo é aceito e ignorado, para um cliente
antigo não receber 400). vl_bonificacao continua no banco com o que já tem e
fora de todo total.
5. Autoridade e auditoria
- O backend recalcula o líquido de cada item a partir de base+ajustes no create/update e ignora o valor enviado pelo cliente (divergência > R$ 0,01 gera warn no log - indica front desatualizado ou payload adulterado).
- Na geração do pedido: recálculo oficial +
js_snapshot_comercialcom nomes (representada/marca/coleção/tabela/condição), os ids do que esses nomes descrevem, percentuais da condição naquele momento, versão e modo do motor, resumo de origens de preço (priceSourcesSummary) e o retrato de cada item (nome, referência, valores de grade). Mudanças posteriores de cadastro não afetam o snapshot. - Pedido gerado congela condição e modo (decisão RND-01 a, 24/09/2026):
todo recálculo de pedido
CONCLUIDOusa os percentuais da condição e o modo (ADITIVO/CASCATA) do snapshot, nunca o cadastro de agora. Mudar o percentual da condição ou o modo da empresa não muda pedido gerado (é o que o texto de Configurações, Cálculo promete). O pedido gerado não troca condição, coleção nem tabela (P-19, 25/09/2026): a tela trava os campos e diz por quê, e para mudar refaz o pedido; itens podem sair e entrar (o item novo com o preço atual). O orçamento segue o cadastro. Pedido gerado antes de existir o snapshot segue o cadastro. - Retrato da venda (decisão RND-02, 24/09/2026): tela, lista, PDF e e-mail
do pedido gerado mostram os nomes da venda (
retratoemGET /orders/:id), inclusive o nome da marca da época; quando a marca foi renomeada, a revisão avisa e oferece usar o nome atual só naquele pedido. - Pedido FATURADO é imutável: itens não podem ser alterados (estorne o faturamento antes).
- PDF fecha a conta: linhas (líquido) = mercadoria; mercadoria + ajuste
da condição (impresso, com nome e percentual congelados) + frete = total
gravado. Na visão da representada, bruto + descontos e acréscimos dos itens
= líquido. Os templates moram em
app_meta.nex_template_pdf, compartilhados pelos tenants; a versão vigente é a denexrep-api/templates/*.hbs, aplicada pornexrep-api/scripts/app-meta-templates-pdf.sql. POST /orders/pricing-previewcalcula sem persistir - alimenta o diff visual ao trocar contexto comercial e a duplicação com preços atuais.
6. Escalas e formatos
| Campo | Escala |
|---|---|
vl_liquido, vl_base, vl_subtotal, vl_total, vl_frete | numeric(15,2) |
nex_tabela_preco_item.vl_preco | numeric(15,4) |
nex_pedido_item_ajuste.vl_ajuste (percentual!) | numeric(15,4) |
pc_desconto/pc_acrescimo da condição | numeric(5,2) |
Decimais trafegam como string na API; o front converte com Number() e exibe
via Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).