Pular para o conteúdo principal

Validação Comercial

Última atualização: 14 de julho de 2026

Visão Geral​

Existem três validações comerciais independentes no fluxo de venda, cada uma com propósito e momento diferentes. Não são camadas de uma mesma validação: são serviços separados, cada um usado por uma parte diferente da UI:

ValidaçãoServiçoQuando rodaEfeito
Validação de produto (com alerta)ValidationSaleDraftService.validateProductAo adicionar/alterar quantidade, ao aplicar descontoMostra alerta (appAlert) e pode ajustar automaticamente o valor do campo
Validação silenciosa do carrinhoSaleDraftCartValidationService.validateCartItemsRecalculada a cada evento de carrinho (debounce 120ms)Alimenta badges/indicadores visuais nos itens do carrinho, sem interromper o usuário
Validação rápida de desconto (menu)validateCartDiscounts (client-side, em new-sale-cart.controller.ts)Ação manual "Validar Desconto Máximo" no menu do carrinhoMostra um toast com contagem de itens inválidos

Arquivos-chave​

ArquivoResponsabilidade
services/sale/sale-draft/sale-draft-validation/validation-product.sale-draft.service.tsValidação de produto com alertas: mínimo, estoque, desconto máximo, acréscimo máximo
services/sale/sale-draft/sale-draft-validation/validation-cart.sale-draft.service.tsValidação silenciosa e em lote de todo o carrinho
hooks/new-sale/useNewSaleCartValidationController.tsMantém o mapa de problemas do carrinho atualizado, revalidando a cada evento de carrinho
features/new-sale/new-sale-cart/new-sale-cart.controller.tsvalidateCartDiscounts, checagem rápida client-side (menu "Mais Opções")
features/new-sale/new-sale-cart/components/new-sale-cart-validation/*Modal "Ajustar carrinho" que lista e permite corrigir itens com problema

Validação de produto (a que mostra alerta)​

ValidationSaleDraftService.validateProduct roda quatro checagens configuráveis, cada uma podendo ajustar o produto e/ou exibir um alerta bloqueante:

async validateProduct(params: ValidateProductParams): Promise<Product> {
// ...
if (validations.validateMinQuantity) { /* ... */ }
if (validations.validateStock) { /* ... */ }
if (validations.validateDiscount) { /* ... */ }
if (validations.validateAddition) { /* ... */ }
return currentProduct;
}

Cada caller decide quais das quatro checagens quer rodar. Por exemplo, ao aplicar quantidade (setQuantityInProduct) só validateMinQuantity e validateStock rodam, e ao aplicar desconto (applyDiscount) só validateDiscount e validateAddition rodam, como visto em Descontos e Carrinho e Itens.

Estoque e a configuração DESVALESTOQUE​

A checagem de estoque respeita a configuração DESVALESTOQUE, com três comportamentos possíveis:

const desvalestoque = settings.get("DESVALESTOQUE");

if (
desvalestoque !== "S" &&
(product?.qtdEstoque == 0 ||
Number(product.quantidade) > Number(product?.qtdEstoque))
) {
// "S" → nunca entra aqui, estoque não é validado
// "A" → mostra alerta tipo "warning", mas deixa o vendedor continuar
// qualquer outro valor (ex. "N") → mostra alerta tipo "error", bloqueia e oferece ajustar quantidade
}
DESVALESTOQUEComportamento
"S"Estoque não é validado, venda sem estoque é permitida sem aviso
"A"Avisa (warning), mas permite continuar mesmo sem estoque suficiente
outro (ex. "N")Bloqueia (error) e oferece "Ajustar Quantidade" (usa qtdEstoque) ou "Cancelar" (zera para 1)

Desconto máximo e acréscimo máximo por produto​

Cada produto pode ter um descMax (desconto percentual máximo) e acrescMax (acréscimo percentual máximo, representado como desconto negativo). A validação ajusta automaticamente o valor para o limite permitido quando excedido:

if (
Number(product.percDesc) !== 0 &&
Number(product.percDesc) > Number(product.descMax)
) {
// alerta "Desconto máximo excedido"
// oferece "Ajustar Desconto" (usa descMax) ou "Cancelar" (zera desconto)
return { ...product, percDesc: product?.descMax ?? 0, vlrDesc: product?.vlrDescMax ?? 0 };
}

Se descMax === 0, o produto não pode receber desconto algum: qualquer percentual é zerado com alerta "Desconto não permitido". A mesma lógica espelhada existe para acréscimo (acrescMax, desconto negativo) em validateAddition.

Validação silenciosa do carrinho (badges)​

SaleDraftCartValidationService.validateCartItems roda sobre todos os itens do carrinho, não só os visíveis na página atual, e retorna um mapa de problemas por item, sem mostrar nenhum alerta. Serve para alimentar indicadores visuais, não para bloquear ações:

async validateCartItems(saleDraftId: string): Promise<Record<string, string[]>> {
const items = await this.cartRepo.findAllByDraft(saleDraftId);
const desvalestoque = settings.get("DESVALESTOQUE");
const issues: Record<string, string[]> = {};

for (const item of items) {
const itemIssues: string[] = [];

if (item.quantidadeMinima > 0 && item.quantidade < item.quantidadeMinima) {
itemIssues.push("quantidade_minima");
}
if (item.quantidadeMaxima > 0 && item.quantidade > item.quantidadeMaxima) {
itemIssues.push("quantidade_maxima");
}
if (desvalestoque !== "S") {
if (item.quantidadeEstoque === 0) {
itemIssues.push("sem_estoque");
} else if (item.quantidade > item.quantidadeEstoque) {
itemIssues.push("estoque_insuficiente");
}
}

if (itemIssues.length > 0) {
issues[item.id] = itemIssues;
}
}

return issues;
}

useNewSaleCartValidationController mantém esse mapa vivo, revalidando automaticamente a cada evento do pub/sub de carrinho, o mesmo subscribeSaleDraftCartUpdates usado para refresh de lista, como visto em Carrinho e Itens:

useEffect(() => {
return subscribeSaleDraftCartUpdates((event) => {
if (event.saleDraftId !== saleDraftId) return;
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => void validate(), DEBOUNCE_MS);
});
}, [validate, saleDraftId]);

A chave do mapa é o mesmo id determinístico usado no carrinho (sale_item:draftId:prodId:prodIdExt:seq), então getItemIssues(produtoId, produtoIdExterno, sequencia) consegue localizar o problema de um item específico sem precisar do codTipOper na chave.

Validação rápida de desconto (ação de menu)​

A ação "Validar Desconto Máximo" do menu "Mais Opções" do carrinho (handleValidateMaxDiscount em new-sale.tsx) não chama nenhum dos dois serviços acima. É uma checagem simples e local, feita só sobre a página de itens carregada no momento (draftCart.items), verificando apenas se percDesc está no intervalo [0, 100]:

// new-sale-cart.controller.ts
const validateCartDiscounts = () => {
const invalidItems = draftCart.items.filter((item) => {
const discount = Number(item.percDesc ?? 0);
return !Number.isFinite(discount) || discount < 0 || discount > 100;
});

return {
totalItems: draftCart.items.length,
invalidItemsCount: invalidItems.length,
valid: invalidItems.length === 0,
};
};

Cuidado: essa checagem não usa descMax por produto nem consulta todos os itens do draft, só a página atualmente carregada na tela. Não é equivalente à validação silenciosa do carrinho nem confiável como "validação final" antes de enviar o pedido.

Ao confirmar o cabeçalho, a tela abre NewSaleCartAlertsModal ("Estoque Insuficiente") antes de ir para a aba Carrinho. A intenção, pelo texto da UI, é revisar produtos cujo saldo de estoque caiu desde a última sincronização. Esse modal hoje referencia campos que não existem no NewSaleCartController (pendingAlertProductsCount, alertReviewTabOptions, filteredAlertProducts, openAlertDetailsModal, entre outros não presentes em new-sale-cart.controller.ts). Antes de assumir que esse modal funciona ou de estender esse fluxo, confirme o estado atual do componente. É provável que precise ser conectado a um controller próprio ainda não implementado.

Armadilhas conhecidas​

  • DESVALESTOQUE tem três estados, não dois: "S", "A" e qualquer outro valor. Não trate como booleano.
  • Um produto com descMax === 0 não pode ter desconto algum, diferente de "sem limite definido". Trate 0 explicitamente, não como "falsy = sem regra".
  • A validação silenciosa do carrinho (validateCartItems) e a validação de produto com alerta (validateProduct) têm listas de problemas diferentes. Não assuma que resolver um cobre o outro.
  • A ação "Validar Desconto Máximo" do menu é uma checagem de intervalo simples, não uma validação completa de regras de desconto por produto.