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ção | Serviço | Quando roda | Efeito |
|---|---|---|---|
| Validação de produto (com alerta) | ValidationSaleDraftService.validateProduct | Ao adicionar/alterar quantidade, ao aplicar desconto | Mostra alerta (appAlert) e pode ajustar automaticamente o valor do campo |
| Validação silenciosa do carrinho | SaleDraftCartValidationService.validateCartItems | Recalculada 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 carrinho | Mostra um toast com contagem de itens inválidos |
Arquivos-chave
| Arquivo | Responsabilidade |
|---|---|
services/sale/sale-draft/sale-draft-validation/validation-product.sale-draft.service.ts | Validaçã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.ts | Validação silenciosa e em lote de todo o carrinho |
hooks/new-sale/useNewSaleCartValidationController.ts | Mantém o mapa de problemas do carrinho atualizado, revalidando a cada evento de carrinho |
features/new-sale/new-sale-cart/new-sale-cart.controller.ts | validateCartDiscounts, 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
}
DESVALESTOQUE | Comportamento |
|---|---|
"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
descMaxpor 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.
Modal de alertas de estoque pós-sincronização (estado atual: incompleto)
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
DESVALESTOQUEtem três estados, não dois:"S","A"e qualquer outro valor. Não trate como booleano.- Um produto com
descMax === 0não pode ter desconto algum, diferente de "sem limite definido". Trate0explicitamente, 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.