feat: Enforce consistency in order items' modalidade and ata across mutations and frontend validation in pedidos management.

This commit is contained in:
2025-12-09 14:49:29 -03:00
parent 881f2fbb8b
commit be3fb4ea64
2 changed files with 375 additions and 214 deletions

View File

@@ -13,6 +13,41 @@ async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
return user;
}
// Garante que todos os itens de um pedido utilizem a mesma
// combinação de modalidade e ata (quando houver).
async function ensurePedidoModalidadeAtaConsistency(
ctx: MutationCtx,
pedidoId: Id<'pedidos'>,
modalidade: Doc<'objetoItems'>['modalidade'],
ataId: Id<'atas'> | undefined,
ignoreItemId?: Id<'objetoItems'>
) {
const normalizedNewAtaId = ataId ?? null;
const items = await ctx.db
.query('objetoItems')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedidoId))
.collect();
if (items.length === 0) {
return;
}
for (const item of items) {
if (ignoreItemId && item._id === ignoreItemId) continue;
const normalizedItemAtaId = (('ataId' in item ? item.ataId : undefined) ?? null) as
| Id<'atas'>
| null;
if (item.modalidade !== modalidade || normalizedItemAtaId !== normalizedNewAtaId) {
throw new Error(
'Todos os itens do pedido devem usar a mesma modalidade e a mesma ata (quando houver). Ajuste os itens existentes ou crie um novo pedido para a nova combinação.'
);
}
}
}
// ========== QUERIES ==========
export const list = query({
@@ -619,6 +654,15 @@ export const addItem = mutation({
const pedido = await ctx.db.get(args.pedidoId);
if (!pedido) throw new Error('Pedido não encontrado.');
// Regra global: todos os itens do pedido devem ter a mesma
// modalidade e a mesma ata (quando houver).
await ensurePedidoModalidadeAtaConsistency(
ctx,
args.pedidoId,
args.modalidade,
args.ataId
);
// --- CHECK ANALYSIS / ACCEPTANCE MODE ---
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
await ctx.db.insert('solicitacoesItens', {
@@ -651,7 +695,7 @@ export const addItem = mutation({
if (conflict) {
throw new Error(
'Você já adicionou este item com outra modalidade ou ata. Não é permitido adicionar o mesmo produto com configurações diferentes neste pedido.'
'Este pedido já possui este produto com outra combinação de modalidade e/ou ata para você. Todos os itens do mesmo produto devem usar a mesma modalidade e ata neste pedido. Ajuste o item existente ou crie um novo pedido para a nova combinação.'
);
}
@@ -1011,6 +1055,16 @@ export const updateItem = mutation({
ataId: 'ataId' in item ? item.ataId : undefined
};
// Regra global: ao alterar um item, garantir que os demais itens do pedido
// continuem (ou passem a estar) com a mesma modalidade e ata.
await ensurePedidoModalidadeAtaConsistency(
ctx,
item.pedidoId,
args.modalidade,
args.ataId,
args.itemId
);
// Em pedidos em análise ou aguardando aceite, geramos uma solicitação em vez de alterar diretamente
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
await ctx.db.insert('solicitacoesItens', {
@@ -1074,7 +1128,10 @@ export const getPermissions = query({
canStartAnalysis: false,
canConclude: false,
canRequestAdjustments: false,
canCancel: false
canCancel: false,
canCompleteAdjustments: false,
canManageRequests: false,
currentFuncionarioId: user?.funcionarioId ?? null
};
}
@@ -1099,6 +1156,14 @@ export const getPermissions = query({
.first();
const hasAddedItems = !!userItem;
// Verificar se existem itens adicionados por OUTROS usuários.
const foreignItem = await ctx.db
.query('objetoItems')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
.filter((q) => q.neq(q.field('adicionadoPor'), user.funcionarioId))
.first();
const hasOnlyCreatorItems = !foreignItem;
const isCreator = pedido.criadoPor === user._id;
return {
@@ -1111,8 +1176,13 @@ export const getPermissions = query({
isInComprasSector &&
pedido.aceitoPor === user.funcionarioId,
canCompleteAdjustments: pedido.status === 'precisa_ajustes' && hasAddedItems,
canCancel: pedido.status !== 'cancelado' && pedido.status !== 'concluido' && isCreator,
canManageRequests: pedido.status === 'em_analise' && isInComprasSector
canCancel:
pedido.status !== 'cancelado' &&
pedido.status !== 'concluido' &&
isCreator &&
hasOnlyCreatorItems,
canManageRequests: pedido.status === 'em_analise' && isInComprasSector,
currentFuncionarioId: user.funcionarioId
};
}
});
@@ -1388,23 +1458,28 @@ export const cancelarPedido = mutation({
throw new Error('Pedido já finalizado.');
}
// Anyone involved (creator or compras) can cancel? Or just creator?
// Logic: If it's creator OR Compras.
// Regra: apenas o criador pode cancelar o pedido
const isCreator = pedido.criadoPor === user._id;
let isCompras = false;
const config = await ctx.db.query('config').first();
if (config && config.comprasSetorId && user.funcionarioId) {
const fs = await ctx.db
.query('funcionarioSetores')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
.first();
isCompras = !!fs;
if (!isCreator) {
throw new Error('Apenas quem criou este pedido pode cancelá-lo.');
}
if (!isCreator && !isCompras) {
throw new Error('Permissão negada para cancelar este pedido.');
if (!user.funcionarioId) {
throw new Error('Usuário sem funcionário vinculado. Não é possível cancelar este pedido.');
}
// Regra extra: o pedido só pode ser cancelado se todos os itens
// tiverem sido adicionados pelo mesmo funcionário do criador.
const foreignItem = await ctx.db
.query('objetoItems')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
.filter((q) => q.neq(q.field('adicionadoPor'), user.funcionarioId))
.first();
if (foreignItem) {
throw new Error(
'Não é possível cancelar este pedido porque há itens adicionados por outros usuários.'
);
}
const oldStatus = pedido.status;
@@ -1597,6 +1672,15 @@ export const approveItemRequest = mutation({
// Reuse addItem logic (simplified: insert or increment)
// We trust the request data structure matches addItem args
const newItem = data;
// Regra global: todos os itens do pedido devem ter a mesma
// modalidade e ata (quando houver).
await ensurePedidoModalidadeAtaConsistency(
ctx,
request.pedidoId,
newItem.modalidade,
newItem.ataId
);
// Note: We MUST use the original requester's ID (request.solicitadoPor) as addedBy?
// Or should we attribute it to the requester? YES.
// BUT `addItem` logic usually checks if `existingItem.adicionadoPor === user`.
@@ -1675,6 +1759,15 @@ export const approveItemRequest = mutation({
const item = await ctx.db.get(itemId);
if (item) {
// Regra global também se aplica na alteração de detalhes aprovada
await ensurePedidoModalidadeAtaConsistency(
ctx,
item.pedidoId,
para.modalidade,
para.ataId,
itemId
);
await ctx.db.patch(itemId, {
valorEstimado: para.valorEstimado,
modalidade: para.modalidade,