From 088ba0bc22bbe49385d07143ae851255e8057c37 Mon Sep 17 00:00:00 2001 From: Andoumeda <107519532+Andoumeda@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:32:51 -0300 Subject: [PATCH 1/4] =?UTF-8?q?Auditor=C3=ADa=20BAC=20en=20pedidos,=20asig?= =?UTF-8?q?naciones=20y=20notificaciones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/auditoria-bac.md | 24 +++ .../delivery-assignments.controller.js | 12 +- .../delivery-assignments.service.js | 166 ++++++++++++++++-- .../delivery/delivery/delivery.controller.js | 2 +- src/modules/users/orders/order.service.js | 16 ++ tests/e2e/delivery-assignments-create.test.js | 2 + tests/e2e/delivery-register.test.js | 3 +- tests/e2e/notifications.test.js | 2 +- tests/e2e/stock.test.js | 2 +- .../commerce/delivery-assignments.test.js | 17 +- .../delivery-assignments-controller.test.js | 2 +- .../delivery/delivery-assignments.test.js | 97 ++++++++-- tests/unit/delivery/delivery-register.test.js | 1 + tests/unit/order/order.test.js | 9 + 14 files changed, 307 insertions(+), 48 deletions(-) create mode 100644 docs/auditoria-bac.md diff --git a/docs/auditoria-bac.md b/docs/auditoria-bac.md new file mode 100644 index 00000000..674f4c5d --- /dev/null +++ b/docs/auditoria-bac.md @@ -0,0 +1,24 @@ +# Reporte de Auditoría BAC + +Fecha: 03-06-2026 +Alcance: API de pedidos, asignaciones y notificaciones + +## Antes +- Cualquier usuario autenticado podía leer los datos de asignación por pedido, entrega o ID de asignación, incluyendo información personal de los pedidos. +- Un SELLER podía crear una asignación de entrega para pedidos fuera de su tienda. +- Un DELIVERY podía actualizar el estado del pedido a DELIVERED sin demostrar que el pedido le había sido asignado. + +## Después +- Las lecturas de asignación ahora aplican la propiedad por rol (el SELLER es propietario de la tienda, el CUSTOMER es propietario del pedido y el DELIVERY es propietario de la asignación). +- La creación de asignaciones valida que el SELLER sea propietario de la tienda del pedido y que la entrega pertenezca a la misma tienda. +- Las actualizaciones de estado del DELIVERY requieren una asignación aceptada vinculada al usuario de entrega autenticado. + +## Archivos modificados +- src/modules/delivery/delivery-assignments/delivery-assignments.service.js +- src/modules/delivery/delivery-assignments/delivery-assignments.controller.js +- src/modules/users/orders/order.service.js +- tests/unit/delivery/delivery-assignments.test.js +- tests/unit/order/order.test.js +- tests/unit/delivery/delivery-assignments-controller.test.js +- tests/unit/commerce/delivery-assignments.test.js +- tests/e2e/delivery-assignments-create.test.js \ No newline at end of file diff --git a/src/modules/delivery/delivery-assignments/delivery-assignments.controller.js b/src/modules/delivery/delivery-assignments/delivery-assignments.controller.js index 631aa113..7183629d 100644 --- a/src/modules/delivery/delivery-assignments/delivery-assignments.controller.js +++ b/src/modules/delivery/delivery-assignments/delivery-assignments.controller.js @@ -17,7 +17,7 @@ import { export const createAssignment = async (req, res) => { try { const validData = createAssignmentSchema.parse(req.body); - const result = await createAssignmentService(validData); + const result = await createAssignmentService(validData, req.user); res.status(201).json(result); } catch (error) { if (error instanceof ZodError) { @@ -43,7 +43,7 @@ export const getAssignmentById = async (req, res) => { }); } - const result = await getAssignmentByIdService(assignmentId); + const result = await getAssignmentByIdService(assignmentId, req.user); res.json(result); } catch (error) { return res.status(error.status || 500).json({ @@ -64,7 +64,7 @@ export const getOrderAssignments = async (req, res) => { }); } - const result = await getOrderAssignmentsService(orderIdNum); + const result = await getOrderAssignmentsService(orderIdNum, req.user); res.json(result); } catch (error) { return res.status(error.status || 500).json({ @@ -94,7 +94,7 @@ export const getDeliveryAssignments = async (req, res) => { }); } - const result = await getDeliveryAssignmentsService(deliveryIdNum, status || null); + const result = await getDeliveryAssignmentsService(deliveryIdNum, req.user, status || null); res.json(result); } catch (error) { return res.status(error.status || 500).json({ @@ -115,7 +115,7 @@ export const getDeliveryPendingAssignments = async (req, res) => { }); } - const result = await getDeliveryPendingAssignmentsService(deliveryIdNum); + const result = await getDeliveryPendingAssignmentsService(deliveryIdNum, req.user); res.json(result); } catch (error) { return res.status(error.status || 500).json({ @@ -136,7 +136,7 @@ export const getAcceptedAssignment = async (req, res) => { }); } - const result = await getAcceptedAssignmentService(orderIdNum); + const result = await getAcceptedAssignmentService(orderIdNum, req.user); res.json(result); } catch (error) { return res.status(error.status || 500).json({ diff --git a/src/modules/delivery/delivery-assignments/delivery-assignments.service.js b/src/modules/delivery/delivery-assignments/delivery-assignments.service.js index d9442c23..708a79cb 100644 --- a/src/modules/delivery/delivery-assignments/delivery-assignments.service.js +++ b/src/modules/delivery/delivery-assignments/delivery-assignments.service.js @@ -10,16 +10,82 @@ import { isAssignmentResponseWindowOpen, } from "./delivery-assignment-workflow.service.js"; -export const createAssignmentService = async (data) => { +const resolveAuth = (authenticatedUser) => { + const id_user = authenticatedUser?.id_user ?? authenticatedUser?.id; + const role = authenticatedUser?.role; + + if (!id_user || !role) { + throw { status: 403, message: "No tienes permiso para acceder a este recurso" }; + } + + return { id_user, role }; +}; + +const ensureOrderAccess = async (order, auth) => { + if (auth.role === "SELLER") { + const store = await prisma.stores.findFirst({ + where: { id_store: order.fk_store, fk_user: auth.id_user, status: true }, + select: { id_store: true } + }); + + if (!store) { + throw { status: 403, message: "No tienes permiso para acceder a este pedido" }; + } + + return; + } + + if (auth.role === "CUSTOMER") { + if (order.fk_user !== auth.id_user) { + throw { status: 403, message: "No tienes permiso para acceder a este pedido" }; + } + return; + } + + if (auth.role === "DELIVERY") { + const assignment = await prisma.deliveryAssignments.findFirst({ + where: { + fk_order: order.id_order, + status: true, + delivery: { fk_user: auth.id_user } + }, + select: { id_delivery_assignment: true } + }); + + if (!assignment) { + throw { status: 403, message: "No tienes permiso para acceder a este pedido" }; + } + + return; + } + + throw { status: 403, message: "No tienes permiso para acceder a este recurso" }; +}; + +export const createAssignmentService = async (data, authenticatedUser) => { + const auth = resolveAuth(authenticatedUser); + if (auth.role !== "SELLER") { + throw { status: 403, message: "No tienes permiso para asignar deliveries" }; + } + const { fk_order, fk_delivery: fk_delivery_input, status } = data; const order = await prisma.orders.findUnique({ - where: { id_order: fk_order } + where: { id_order: fk_order }, + select: { + id_order: true, + fk_store: true, + store: { select: { fk_user: true } } + } }); if (!order) { throw { status: 404, message: "Pedido no encontrado" }; } + if (order.store?.fk_user !== auth.id_user) { + throw { status: 403, message: "No tienes permiso para asignar este pedido" }; + } + try { const newDeliveryAssignment = await prisma.$transaction(async (tx) => { await expireStalePendingAssignments({ fk_order }, tx); @@ -74,11 +140,15 @@ export const createAssignmentService = async (data) => { fk_delivery = delivery.id_delivery; } else { const delivery = await tx.deliveries.findUnique({ - where: { id_delivery: fk_delivery } + where: { id_delivery: fk_delivery }, + select: { id_delivery: true, delivery_status: true, status: true, fk_store: true } }); if (!delivery) { throw { status: 404, message: "Delivery no encontrado" }; } + if (delivery.fk_store !== order.fk_store) { + throw { status: 403, message: "El delivery no pertenece al comercio del pedido" }; + } if (delivery.delivery_status !== "ACTIVE" || !delivery.status) { throw { status: 400, message: "El delivery debe estar activo para recibir pedidos" }; } @@ -100,15 +170,23 @@ export const createAssignmentService = async (data) => { }; // Obtener asignación por ID -export const getAssignmentByIdService = async (id_delivery_assignment) => { +export const getAssignmentByIdService = async (id_delivery_assignment, authenticatedUser) => { + const auth = resolveAuth(authenticatedUser); + const assignment = await prisma.deliveryAssignments.findUnique({ where: { id_delivery_assignment }, include: { order: { - select: { id_order: true, total: true, created_at: true } + select: { + id_order: true, + total: true, + created_at: true, + fk_user: true, + store: { select: { fk_user: true } } + } }, delivery: { - select: { id_delivery: true, delivery_status: true } + select: { id_delivery: true, delivery_status: true, fk_user: true } } } }); @@ -117,18 +195,46 @@ export const getAssignmentByIdService = async (id_delivery_assignment) => { throw { status: 404, message: "Asignación no encontrada" }; } + if (auth.role === "SELLER") { + if (assignment.order?.store?.fk_user !== auth.id_user) { + throw { status: 403, message: "No tienes permiso para ver esta asignación" }; + } + } else if (auth.role === "CUSTOMER") { + if (assignment.order?.fk_user !== auth.id_user) { + throw { status: 403, message: "No tienes permiso para ver esta asignación" }; + } + } else if (auth.role === "DELIVERY") { + if (assignment.delivery?.fk_user !== auth.id_user) { + throw { status: 403, message: "No tienes permiso para ver esta asignación" }; + } + } else { + throw { status: 403, message: "No tienes permiso para ver esta asignación" }; + } + return assignment; }; // Obtener asignaciones de un pedido -export const getOrderAssignmentsService = async (id_order) => { +export const getOrderAssignmentsService = async (id_order, authenticatedUser) => { + const auth = resolveAuth(authenticatedUser); + if (![ + "SELLER", + "CUSTOMER", + "DELIVERY", + ].includes(auth.role)) { + throw { status: 403, message: "No tienes permiso para ver asignaciones" }; + } + const order = await prisma.orders.findUnique({ - where: { id_order } + where: { id_order }, + select: { id_order: true, fk_store: true, fk_user: true } }); if (!order) { throw { status: 404, message: "Pedido no encontrado" }; } + await ensureOrderAccess(order, auth); + await expireStalePendingAssignments({ fk_order: id_order }); const assignments = await prisma.deliveryAssignments.findMany({ @@ -144,15 +250,25 @@ export const getOrderAssignmentsService = async (id_order) => { return assignments; }; // Obtener asignaciones de un delivery -export const getDeliveryAssignmentsService = async (id_delivery, status = null) => { +export const getDeliveryAssignmentsService = async (id_delivery, authenticatedUser, status = null) => { + const auth = resolveAuth(authenticatedUser); + if (auth.role !== "DELIVERY") { + throw { status: 403, message: "No tienes permiso para ver asignaciones de delivery" }; + } + const delivery = await prisma.deliveries.findUnique({ - where: { id_delivery } + where: { id_delivery }, + select: { id_delivery: true, fk_user: true } }); if (!delivery) { throw { status: 404, message: "Delivery no encontrado" }; } + if (delivery.fk_user !== auth.id_user) { + throw { status: 403, message: "No tienes permiso para ver asignaciones de este delivery" }; + } + await expireStalePendingAssignments({ fk_delivery: id_delivery }); const where = { fk_delivery: id_delivery }; @@ -193,15 +309,25 @@ export const getDeliveryAssignmentsService = async (id_delivery, status = null) return assignments; }; // Obtener asignaciones PENDING de un delivery -export const getDeliveryPendingAssignmentsService = async (id_delivery) => { +export const getDeliveryPendingAssignmentsService = async (id_delivery, authenticatedUser) => { + const auth = resolveAuth(authenticatedUser); + if (auth.role !== "DELIVERY") { + throw { status: 403, message: "No tienes permiso para ver asignaciones de delivery" }; + } + const delivery = await prisma.deliveries.findUnique({ - where: { id_delivery } + where: { id_delivery }, + select: { id_delivery: true, fk_user: true } }); if (!delivery) { throw { status: 404, message: "Delivery no encontrado" }; } + if (delivery.fk_user !== auth.id_user) { + throw { status: 403, message: "No tienes permiso para ver asignaciones de este delivery" }; + } + await expireStalePendingAssignments({ fk_delivery: id_delivery }); const pendingAssignments = await prisma.deliveryAssignments.findMany({ @@ -220,15 +346,27 @@ export const getDeliveryPendingAssignmentsService = async (id_delivery) => { return pendingAssignments; }; // Obtener la asignación aceptada de un pedido -export const getAcceptedAssignmentService = async (id_order) => { +export const getAcceptedAssignmentService = async (id_order, authenticatedUser) => { + const auth = resolveAuth(authenticatedUser); + if (![ + "SELLER", + "CUSTOMER", + "DELIVERY", + ].includes(auth.role)) { + throw { status: 403, message: "No tienes permiso para ver asignaciones" }; + } + const order = await prisma.orders.findUnique({ - where: { id_order } + where: { id_order }, + select: { id_order: true, fk_store: true, fk_user: true } }); if (!order) { throw { status: 404, message: "Pedido no encontrado" }; } + await ensureOrderAccess(order, auth); + const acceptedAssignment = await prisma.deliveryAssignments.findFirst({ where: { fk_order: id_order, diff --git a/src/modules/delivery/delivery/delivery.controller.js b/src/modules/delivery/delivery/delivery.controller.js index adb83eae..fd442565 100644 --- a/src/modules/delivery/delivery/delivery.controller.js +++ b/src/modules/delivery/delivery/delivery.controller.js @@ -51,7 +51,7 @@ export const registerDelivery = async (req, res) => { return res.status(400).json({ error: { code: 400, - message: error.issues[0]?.message ?? "Datos inválidos", + message: "Datos inválidos", details: error.issues, }, }); diff --git a/src/modules/users/orders/order.service.js b/src/modules/users/orders/order.service.js index 5b09af7e..070454b7 100644 --- a/src/modules/users/orders/order.service.js +++ b/src/modules/users/orders/order.service.js @@ -861,6 +861,22 @@ export const updateOrderStatusService = async (authenticatedUserId, orderId, ord if (!customerOrder) throw new ForbiddenError("No tienes permisos para modificar este pedido."); } + if (user.role === "DELIVERY") { + const deliveryAssignment = await prisma.deliveryAssignments.findFirst({ + where: { + fk_order: resolvedOrderId, + assignment_status: "ACCEPTED", + status: true, + delivery: { fk_user: resolvedUserId } + }, + select: { id_delivery_assignment: true } + }); + + if (!deliveryAssignment) { + throw new ForbiddenError("No tienes permisos para modificar este pedido."); + } + } + //validar transiciones permitidas por rol de user autenticado const isPickup = order.fk_address == null; const allowedTransitions = { diff --git a/tests/e2e/delivery-assignments-create.test.js b/tests/e2e/delivery-assignments-create.test.js index 6eeb0420..886e2119 100644 --- a/tests/e2e/delivery-assignments-create.test.js +++ b/tests/e2e/delivery-assignments-create.test.js @@ -80,6 +80,7 @@ describe("POST /api/assignments", () => { prisma.orders.findUnique.mockResolvedValue({ id_order: 5, fk_store: 10, + store: { fk_user: 1 }, }); prisma.deliveries.findUnique.mockResolvedValue({ @@ -105,6 +106,7 @@ describe("POST /api/assignments", () => { mockTx.deliveries.findUnique.mockResolvedValue({ id_delivery: 2, + fk_store: 10, delivery_status: "ACTIVE", status: true, }); diff --git a/tests/e2e/delivery-register.test.js b/tests/e2e/delivery-register.test.js index d80adc86..215bcf5f 100644 --- a/tests/e2e/delivery-register.test.js +++ b/tests/e2e/delivery-register.test.js @@ -104,6 +104,7 @@ describe("POST /api/deliveries/register", () => { prisma.users.findUnique.mockResolvedValue({ id_user: 10, role: "CUSTOMER", + phone: "1234567890", delivery: null, }); @@ -137,7 +138,7 @@ describe("POST /api/deliveries/register", () => { expect(prisma.users.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id_user: 10 }, - data: { role: "DELIVERY" }, + data: expect.objectContaining({ role: "DELIVERY" }), }) ); expect(prisma.deliveries.create).toHaveBeenCalledWith( diff --git a/tests/e2e/notifications.test.js b/tests/e2e/notifications.test.js index bac9df01..6dd252f2 100644 --- a/tests/e2e/notifications.test.js +++ b/tests/e2e/notifications.test.js @@ -4,7 +4,7 @@ vi.mock("../../src/lib/prisma.js", () => ({ prisma: { users: { findUnique: vi.fn() }, carts: { findFirst: vi.fn(), update: vi.fn() }, - orders: { create: vi.fn(), findUnique: vi.fn() }, + orders: { create: vi.fn(), findUnique: vi.fn(), count: vi.fn().mockResolvedValue(0) }, orderItems: { createMany: vi.fn() }, products: { update: vi.fn() }, notifications: { create: vi.fn() }, diff --git a/tests/e2e/stock.test.js b/tests/e2e/stock.test.js index 336fb341..167bf6fd 100644 --- a/tests/e2e/stock.test.js +++ b/tests/e2e/stock.test.js @@ -12,7 +12,7 @@ vi.mock("../../src/lib/prisma.js", () => ({ }, carts: { findFirst: vi.fn(), upsert: vi.fn(), update: vi.fn(), findUnique: vi.fn(), findMany: vi.fn() }, cartItems: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), findMany: vi.fn() }, - orders: { create: vi.fn(), findUnique: vi.fn() }, + orders: { create: vi.fn(), findUnique: vi.fn(), count: vi.fn().mockResolvedValue(0) }, orderItems: { createMany: vi.fn(), findMany: vi.fn() }, notifications: { create: vi.fn() }, $transaction: vi.fn() diff --git a/tests/unit/commerce/delivery-assignments.test.js b/tests/unit/commerce/delivery-assignments.test.js index 4ce219c5..e19fbd44 100644 --- a/tests/unit/commerce/delivery-assignments.test.js +++ b/tests/unit/commerce/delivery-assignments.test.js @@ -216,7 +216,8 @@ describe("GET /api/stores/:storeId/orders/:orderId/deliveries", () => { it("rechaza automáticamente la asignación PENDING anterior si delivery está INACTIVE", async () => { vi.mocked(prisma.orders.findUnique).mockResolvedValueOnce({ id_order: 100, - fk_store: 1 + fk_store: 1, + store: { fk_user: 1 } }); vi.mocked(prisma.deliveries.findUnique).mockResolvedValueOnce({ @@ -255,6 +256,7 @@ describe("GET /api/stores/:storeId/orders/:orderId/deliveries", () => { deliveries: { findUnique: vi.fn().mockResolvedValue({ id_delivery: 5, + fk_store: 1, delivery_status: "ACTIVE", status: true, }), @@ -266,10 +268,10 @@ describe("GET /api/stores/:storeId/orders/:orderId/deliveries", () => { return await callback(tx); }); - const result = await createAssignmentService({ - fk_order: 100, - fk_delivery: 5 - }); + const result = await createAssignmentService( + { fk_order: 100, fk_delivery: 5 }, + { id_user: 1, role: "SELLER" } + ); expect(result.assignment_status).toBe("PENDING"); expect(result.assignment_sequence).toBe(2); @@ -278,7 +280,8 @@ describe("GET /api/stores/:storeId/orders/:orderId/deliveries", () => { it("lanza error 409 cuando ya hay PENDING con delivery ACTIVE", async () => { vi.mocked(prisma.orders.findUnique).mockResolvedValueOnce({ id_order: 100, - fk_store: 1 + fk_store: 1, + store: { fk_user: 1 } }); vi.mocked(prisma.deliveries.findUnique).mockResolvedValueOnce({ @@ -307,7 +310,7 @@ describe("GET /api/stores/:storeId/orders/:orderId/deliveries", () => { }); await expect( - createAssignmentService({ fk_order: 100, fk_delivery: 5 }) + createAssignmentService({ fk_order: 100, fk_delivery: 5 }, { id_user: 1, role: "SELLER" }) ).rejects.toMatchObject({ status: 409, message: "Ya hay una asignación pendiente para este pedido" diff --git a/tests/unit/delivery/delivery-assignments-controller.test.js b/tests/unit/delivery/delivery-assignments-controller.test.js index a5afa47a..ac78be43 100644 --- a/tests/unit/delivery/delivery-assignments-controller.test.js +++ b/tests/unit/delivery/delivery-assignments-controller.test.js @@ -158,7 +158,7 @@ describe("getDeliveryAssignments", () => { const { req, res } = makeCtx({ deliveryId: "1" }, {}, {}); getDeliveryAssignmentsService.mockResolvedValue([]); await getDeliveryAssignments(req, res); - expect(getDeliveryAssignmentsService).toHaveBeenCalledWith(1, null); + expect(getDeliveryAssignmentsService).toHaveBeenCalledWith(1, req.user, null); }); }); diff --git a/tests/unit/delivery/delivery-assignments.test.js b/tests/unit/delivery/delivery-assignments.test.js index c12e6bff..fda51350 100644 --- a/tests/unit/delivery/delivery-assignments.test.js +++ b/tests/unit/delivery/delivery-assignments.test.js @@ -3,6 +3,7 @@ import { vi, describe, it, expect, beforeEach } from "vitest"; import { prisma } from "../../../src/lib/prisma.js"; import { createAssignmentService, + getAssignmentByIdService, respondToAssignmentService, getDeliveryOrderHistoryService, getOrderAssignmentsService, @@ -52,9 +53,11 @@ vi.mock("../../../src/lib/prisma.js", () => ({ }, stores: { findUnique: vi.fn().mockResolvedValue({ fk_user: 1 }), + findFirst: vi.fn().mockResolvedValue({ id_store: 10 }), }, deliveryAssignments: { findFirst: vi.fn(), + findUnique: vi.fn(), findMany: vi.fn().mockResolvedValue([]), create: vi.fn(), update: vi.fn(), @@ -74,7 +77,9 @@ vi.mock("../../../src/lib/prisma.js", () => ({ const mockOrder = { id_order: 1, fk_store: 10, + fk_user: 20, order_status: "PENDING", + store: { fk_user: 1 }, }; const mockDelivery = { @@ -137,16 +142,33 @@ describe("createAssignmentService", () => { prisma.orders.findUnique.mockResolvedValue(null); await expect( - createAssignmentService({ fk_order: 999, fk_delivery: 1 }) + createAssignmentService({ fk_order: 999, fk_delivery: 1 }, { id_user: 1, role: "SELLER" }) ).rejects.toMatchObject({ status: 404, message: /pedido no encontrado/i }); }); + it("lanza error 403 cuando el usuario no es SELLER", async () => { + await expect( + createAssignmentService({ fk_order: 1, fk_delivery: 1 }, { id_user: 1, role: "CUSTOMER" }) + ).rejects.toMatchObject({ status: 403 }); + }); + + it("lanza error 403 cuando el pedido no pertenece al comercio del SELLER", async () => { + prisma.orders.findUnique.mockResolvedValue({ + ...mockOrder, + store: { fk_user: 99 } + }); + + await expect( + createAssignmentService({ fk_order: 1, fk_delivery: 1 }, { id_user: 1, role: "SELLER" }) + ).rejects.toMatchObject({ status: 403 }); + }); + it("lanza error 404 cuando se pasa fk_delivery y el delivery no existe", async () => { prisma.orders.findUnique.mockResolvedValue(mockOrder); prisma.deliveries.findUnique.mockResolvedValue(null); await expect( - createAssignmentService({ fk_order: 1, fk_delivery: 999 }) + createAssignmentService({ fk_order: 1, fk_delivery: 999 }, { id_user: 1, role: "SELLER" }) ).rejects.toMatchObject({ status: 404, message: /delivery no encontrado/i }); }); @@ -157,7 +179,7 @@ describe("createAssignmentService", () => { mockTx.orders.update.mockResolvedValue({}); await expect( - createAssignmentService({ fk_order: 1 }) + createAssignmentService({ fk_order: 1 }, { id_user: 1, role: "SELLER" }) ).rejects.toMatchObject({ status: 404, message: /no hay deliveries disponibles/i }); }); @@ -171,7 +193,7 @@ describe("createAssignmentService", () => { }); await expect( - createAssignmentService({ fk_order: 1, fk_delivery: 1 }) + createAssignmentService({ fk_order: 1, fk_delivery: 1 }, { id_user: 1, role: "SELLER" }) ).rejects.toMatchObject({ status: 409 }); }); @@ -189,7 +211,7 @@ describe("createAssignmentService", () => { assignment_sequence: 1, }); - const result = await createAssignmentService({ fk_order: 1, fk_delivery: 1 }); + const result = await createAssignmentService({ fk_order: 1, fk_delivery: 1 }, { id_user: 1, role: "SELLER" }); expect(mockTx.deliveryAssignments.create).toHaveBeenCalledWith( expect.objectContaining({ @@ -217,7 +239,7 @@ describe("createAssignmentService", () => { assignment_sequence: 1, }); - const result = await createAssignmentService({ fk_order: 1 }); + const result = await createAssignmentService({ fk_order: 1 }, { id_user: 1, role: "SELLER" }); expect(mockTx.deliveryAssignments.create).toHaveBeenCalledWith( expect.objectContaining({ @@ -561,15 +583,26 @@ describe("getOrderAssignmentsService", () => { it("lanza 404 cuando el pedido no existe", async () => { prisma.orders.findUnique.mockResolvedValue(null); - await expect(getOrderAssignmentsService(1)).rejects.toMatchObject({ status: 404, message: "Pedido no encontrado" }); + await expect(getOrderAssignmentsService(1, { id_user: 1, role: "SELLER" })) + .rejects.toMatchObject({ status: 404, message: "Pedido no encontrado" }); + }); + + it("lanza 403 cuando DELIVERY no está asignado al pedido", async () => { + prisma.orders.findUnique.mockResolvedValue(mockOrder); + prisma.deliveryAssignments.findFirst.mockResolvedValue(null); + + await expect( + getOrderAssignmentsService(1, { id_user: 5, role: "DELIVERY" }) + ).rejects.toMatchObject({ status: 403 }); }); it("retorna las asignaciones del pedido", async () => { prisma.orders.findUnique.mockResolvedValue(mockOrder); + prisma.stores.findFirst.mockResolvedValue({ id_store: 10 }); prisma.deliveryAssignments.findMany .mockResolvedValueOnce([]) .mockResolvedValueOnce([mockAssignmentPending]); - const result = await getOrderAssignmentsService(1); + const result = await getOrderAssignmentsService(1, { id_user: 1, role: "SELLER" }); expect(result).toEqual([mockAssignmentPending]); }); }); @@ -584,7 +617,8 @@ describe("getDeliveryAssignmentsService", () => { it("lanza 404 cuando el delivery no existe", async () => { prisma.deliveries.findUnique.mockResolvedValue(null); - await expect(getDeliveryAssignmentsService(1)).rejects.toMatchObject({ status: 404, message: "Delivery no encontrado" }); + await expect(getDeliveryAssignmentsService(1, { id_user: 5, role: "DELIVERY" })) + .rejects.toMatchObject({ status: 404, message: "Delivery no encontrado" }); }); it("retorna asignaciones sin filtro de status", async () => { @@ -592,16 +626,24 @@ describe("getDeliveryAssignmentsService", () => { prisma.deliveryAssignments.findMany .mockResolvedValueOnce([]) .mockResolvedValueOnce([mockAssignmentPending]); - const result = await getDeliveryAssignmentsService(1); + const result = await getDeliveryAssignmentsService(1, { id_user: 5, role: "DELIVERY" }); expect(result).toEqual([mockAssignmentPending]); }); + it("lanza 403 cuando el delivery no pertenece al usuario autenticado", async () => { + prisma.deliveries.findUnique.mockResolvedValue({ ...mockDelivery, fk_user: 99 }); + + await expect( + getDeliveryAssignmentsService(1, { id_user: 5, role: "DELIVERY" }) + ).rejects.toMatchObject({ status: 403 }); + }); + it("retorna asignaciones filtradas por status", async () => { prisma.deliveries.findUnique.mockResolvedValue(mockDelivery); prisma.deliveryAssignments.findMany .mockResolvedValueOnce([]) .mockResolvedValueOnce([mockAssignmentPending]); - await getDeliveryAssignmentsService(1, "PENDING"); + await getDeliveryAssignmentsService(1, { id_user: 5, role: "DELIVERY" }, "PENDING"); expect(prisma.deliveryAssignments.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ assignment_status: "PENDING" }) }) ); @@ -618,7 +660,8 @@ describe("getDeliveryPendingAssignmentsService", () => { it("lanza 404 cuando el delivery no existe", async () => { prisma.deliveries.findUnique.mockResolvedValue(null); - await expect(getDeliveryPendingAssignmentsService(1)).rejects.toMatchObject({ status: 404, message: "Delivery no encontrado" }); + await expect(getDeliveryPendingAssignmentsService(1, { id_user: 5, role: "DELIVERY" })) + .rejects.toMatchObject({ status: 404, message: "Delivery no encontrado" }); }); it("retorna las asignaciones pendientes del delivery", async () => { @@ -626,7 +669,7 @@ describe("getDeliveryPendingAssignmentsService", () => { prisma.deliveryAssignments.findMany .mockResolvedValueOnce([]) .mockResolvedValueOnce([mockAssignmentPending]); - const result = await getDeliveryPendingAssignmentsService(1); + const result = await getDeliveryPendingAssignmentsService(1, { id_user: 5, role: "DELIVERY" }); expect(result).toEqual([mockAssignmentPending]); expect(prisma.deliveryAssignments.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ assignment_status: "PENDING" }) }) @@ -641,20 +684,42 @@ describe("getAcceptedAssignmentService", () => { it("lanza 404 cuando el pedido no existe", async () => { prisma.orders.findUnique.mockResolvedValue(null); - await expect(getAcceptedAssignmentService(1)).rejects.toMatchObject({ status: 404, message: "Pedido no encontrado" }); + await expect(getAcceptedAssignmentService(1, { id_user: 1, role: "SELLER" })) + .rejects.toMatchObject({ status: 404, message: "Pedido no encontrado" }); }); it("lanza 404 cuando no hay asignación aceptada", async () => { prisma.orders.findUnique.mockResolvedValue(mockOrder); + prisma.stores.findFirst.mockResolvedValue({ id_store: 10 }); prisma.deliveryAssignments.findFirst.mockResolvedValue(null); - await expect(getAcceptedAssignmentService(1)).rejects.toMatchObject({ status: 404, message: "No hay asignación aceptada para este pedido" }); + await expect(getAcceptedAssignmentService(1, { id_user: 1, role: "SELLER" })) + .rejects.toMatchObject({ status: 404, message: "No hay asignación aceptada para este pedido" }); }); it("retorna la asignación aceptada", async () => { const accepted = { ...mockAssignmentPending, assignment_status: "ACCEPTED" }; prisma.orders.findUnique.mockResolvedValue(mockOrder); + prisma.stores.findFirst.mockResolvedValue({ id_store: 10 }); prisma.deliveryAssignments.findFirst.mockResolvedValue(accepted); - const result = await getAcceptedAssignmentService(1); + const result = await getAcceptedAssignmentService(1, { id_user: 1, role: "SELLER" }); expect(result.assignment_status).toBe("ACCEPTED"); }); +}); + +// ─── getAssignmentByIdService ─────────────────────────────────────────────── + +describe("getAssignmentByIdService", () => { + beforeEach(() => vi.clearAllMocks()); + + it("lanza 403 cuando el SELLER no es dueño del comercio", async () => { + prisma.deliveryAssignments.findUnique.mockResolvedValue({ + ...mockAssignmentPending, + order: { ...mockAssignmentPending.order, store: { fk_user: 99 }, fk_user: 20 }, + delivery: { id_delivery: 1, delivery_status: "ACTIVE", fk_user: 5 } + }); + + await expect( + getAssignmentByIdService(1, { id_user: 1, role: "SELLER" }) + ).rejects.toMatchObject({ status: 403 }); + }); }); \ No newline at end of file diff --git a/tests/unit/delivery/delivery-register.test.js b/tests/unit/delivery/delivery-register.test.js index c60a1ec5..8b796d61 100644 --- a/tests/unit/delivery/delivery-register.test.js +++ b/tests/unit/delivery/delivery-register.test.js @@ -111,6 +111,7 @@ describe("POST /api/deliveries/register", () => { prisma.users.findUnique.mockResolvedValue({ id_user: 10, role: "CUSTOMER", + phone: "1234567890", delivery: null, }); diff --git a/tests/unit/order/order.test.js b/tests/unit/order/order.test.js index a94e2ad6..f378d7ae 100644 --- a/tests/unit/order/order.test.js +++ b/tests/unit/order/order.test.js @@ -249,6 +249,7 @@ describe("updateOrderStatusService", () => { it("DELIVERY SHIPPED → DELIVERED: transición permitida", async () => { prisma.users.findFirst.mockResolvedValue({ role: "DELIVERY" }); prisma.orders.findFirst.mockResolvedValue({ id_order: 100, order_status: "SHIPPED", fk_store: 10 }); + prisma.deliveryAssignments.findFirst.mockResolvedValue({ id_delivery_assignment: 1 }); const mockTx = createMockTx({ orders: { update: vi.fn().mockResolvedValue({ ...mockOrderFromDB, order_status: "DELIVERED" }) }, @@ -260,6 +261,14 @@ describe("updateOrderStatusService", () => { expect(result.status).toBe("DELIVERED"); }); + it("DELIVERY SHIPPED → DELIVERED: lanza ForbiddenError si no es el delivery asignado", async () => { + prisma.users.findFirst.mockResolvedValue({ role: "DELIVERY" }); + prisma.orders.findFirst.mockResolvedValue({ id_order: 100, order_status: "SHIPPED", fk_store: 10 }); + prisma.deliveryAssignments.findFirst.mockResolvedValue(null); + + await expect(updateOrderStatusService(1, 100, "DELIVERED")).rejects.toThrow(ForbiddenError); + }); + it("SELLER SHIPPED → DELIVERED: transición NO permitida", async () => { prisma.users.findFirst.mockResolvedValue({ role: "SELLER" }); prisma.orders.findFirst.mockResolvedValue({ id_order: 100, order_status: "SHIPPED", fk_store: 10 }); From 87349a1dc04a3c5b2c034dd9e9bc666451ec39eb Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:54:22 +0000 Subject: [PATCH 2/4] fix: apply CodeRabbit auto-fixes Fixed 3 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit --- .../delivery-assignments.service.js | 8 +++-- src/modules/users/orders/order.service.js | 32 +++++++++---------- tsconfig.json | 3 +- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/modules/delivery/delivery-assignments/delivery-assignments.service.js b/src/modules/delivery/delivery-assignments/delivery-assignments.service.js index 708a79cb..eeaeba37 100644 --- a/src/modules/delivery/delivery-assignments/delivery-assignments.service.js +++ b/src/modules/delivery/delivery-assignments/delivery-assignments.service.js @@ -47,7 +47,7 @@ const ensureOrderAccess = async (order, auth) => { where: { fk_order: order.id_order, status: true, - delivery: { fk_user: auth.id_user } + delivery: { fk_user: auth.id_user, status: true } }, select: { id_delivery_assignment: true } }); @@ -75,7 +75,7 @@ export const createAssignmentService = async (data, authenticatedUser) => { select: { id_order: true, fk_store: true, - store: { select: { fk_user: true } } + store: { select: { fk_user: true, status: true } } } }); if (!order) { @@ -86,6 +86,10 @@ export const createAssignmentService = async (data, authenticatedUser) => { throw { status: 403, message: "No tienes permiso para asignar este pedido" }; } + if (!order.store?.status) { + throw { status: 403, message: "No tienes permiso para asignar este pedido" }; + } + try { const newDeliveryAssignment = await prisma.$transaction(async (tx) => { await expireStalePendingAssignments({ fk_order }, tx); diff --git a/src/modules/users/orders/order.service.js b/src/modules/users/orders/order.service.js index 070454b7..f18fb616 100644 --- a/src/modules/users/orders/order.service.js +++ b/src/modules/users/orders/order.service.js @@ -861,22 +861,6 @@ export const updateOrderStatusService = async (authenticatedUserId, orderId, ord if (!customerOrder) throw new ForbiddenError("No tienes permisos para modificar este pedido."); } - if (user.role === "DELIVERY") { - const deliveryAssignment = await prisma.deliveryAssignments.findFirst({ - where: { - fk_order: resolvedOrderId, - assignment_status: "ACCEPTED", - status: true, - delivery: { fk_user: resolvedUserId } - }, - select: { id_delivery_assignment: true } - }); - - if (!deliveryAssignment) { - throw new ForbiddenError("No tienes permisos para modificar este pedido."); - } - } - //validar transiciones permitidas por rol de user autenticado const isPickup = order.fk_address == null; const allowedTransitions = { @@ -903,6 +887,22 @@ export const updateOrderStatusService = async (authenticatedUserId, orderId, ord if (!allowed.includes(order_status)) throw new ValidationError(`Transicion invalida: ${order.order_status} a ${order_status}`); const updated = await prisma.$transaction(async (tx) => { + if (user.role === "DELIVERY") { + const deliveryAssignment = await tx.deliveryAssignments.findFirst({ + where: { + fk_order: resolvedOrderId, + assignment_status: "ACCEPTED", + status: true, + delivery: { is: { fk_user: resolvedUserId } } + }, + select: { id_delivery_assignment: true } + }); + + if (!deliveryAssignment) { + throw new ForbiddenError("No tienes permisos para modificar este pedido."); + } + } + const updatedOrder = await tx.orders.update({ where: { id_order: resolvedOrderId, diff --git a/tsconfig.json b/tsconfig.json index f429a21e..6bc1207d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "strict": true, "esModuleInterop": true, //"ignoreDeprecations": "6.0", - "skipLibCheck": true + "skipLibCheck": true, + "types": ["node"] } } From 80c319088a02d1618f8a6d2fa89b253f4bc45030 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:55:20 +0000 Subject: [PATCH 3/4] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- tests/unit/order/order.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/order/order.test.js b/tests/unit/order/order.test.js index f378d7ae..eb65f2d1 100644 --- a/tests/unit/order/order.test.js +++ b/tests/unit/order/order.test.js @@ -59,7 +59,7 @@ const createMockTx = (overrides = {}) => ({ products: { update: vi.fn().mockResolvedValue({}) }, notifications: { create: vi.fn().mockResolvedValue({}) }, deliveries: { findFirst: vi.fn() }, - deliveryAssignments: { findFirst: vi.fn(), findMany: vi.fn().mockResolvedValue([]), create: vi.fn() }, + deliveryAssignments: { findFirst: vi.fn().mockResolvedValue({ id_delivery_assignment: 1 }), findMany: vi.fn().mockResolvedValue([]), create: vi.fn() }, ...overrides, }); @@ -266,6 +266,11 @@ describe("updateOrderStatusService", () => { prisma.orders.findFirst.mockResolvedValue({ id_order: 100, order_status: "SHIPPED", fk_store: 10 }); prisma.deliveryAssignments.findFirst.mockResolvedValue(null); + const mockTx = createMockTx({ + deliveryAssignments: { findFirst: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]), create: vi.fn() }, + }); + prisma.$transaction.mockImplementation(async (fn) => fn(mockTx)); + await expect(updateOrderStatusService(1, 100, "DELIVERED")).rejects.toThrow(ForbiddenError); }); From adda51961c9dce399037cea29b8db0057f745ddb Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:37:56 +0000 Subject: [PATCH 4/4] fix: apply CodeRabbit auto-fixes Fixed 5 file(s) based on 5 unresolved review comments. Co-authored-by: CodeRabbit --- .../delivery-assignments/delivery-assignments.service.js | 8 ++++++-- tests/e2e/delivery-assignments-create.test.js | 2 +- tests/unit/commerce/delivery-assignments.test.js | 4 ++-- tests/unit/delivery/delivery-assignments.test.js | 2 +- tests/unit/delivery/delivery-register.test.js | 3 +-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/modules/delivery/delivery-assignments/delivery-assignments.service.js b/src/modules/delivery/delivery-assignments/delivery-assignments.service.js index ae2f503a..73e1477a 100644 --- a/src/modules/delivery/delivery-assignments/delivery-assignments.service.js +++ b/src/modules/delivery/delivery-assignments/delivery-assignments.service.js @@ -48,7 +48,7 @@ const ensureOrderAccess = async (order, auth) => { where: { fk_order: order.id_order, status: true, - delivery: { fk_user: auth.id_user, status: true } + delivery: { is: { fk_user: auth.id_user, status: true, delivery_status: 'ACTIVE' } } }, select: { id_delivery_assignment: true } }); @@ -314,7 +314,10 @@ export const getDeliveryAssignmentsService = async (id_delivery, authenticatedUs await expireStalePendingAssignments({ fk_delivery: id_delivery }); - const where = { fk_delivery: id_delivery }; + const where = { + fk_delivery: id_delivery, + delivery: { is: { fk_user: auth.id_user, status: true, delivery_status: 'ACTIVE' } } + }; if (status) { where.assignment_status = status; } @@ -376,6 +379,7 @@ export const getDeliveryPendingAssignmentsService = async (id_delivery, authenti const pendingAssignments = await prisma.deliveryAssignments.findMany({ where: { fk_delivery: id_delivery, + delivery: { is: { fk_user: auth.id_user, status: true, delivery_status: 'ACTIVE' } }, ...activePendingAssignmentWhere(), }, include: { diff --git a/tests/e2e/delivery-assignments-create.test.js b/tests/e2e/delivery-assignments-create.test.js index 886e2119..b1acda3b 100644 --- a/tests/e2e/delivery-assignments-create.test.js +++ b/tests/e2e/delivery-assignments-create.test.js @@ -80,7 +80,7 @@ describe("POST /api/assignments", () => { prisma.orders.findUnique.mockResolvedValue({ id_order: 5, fk_store: 10, - store: { fk_user: 1 }, + store: { fk_user: 1, status: true }, }); prisma.deliveries.findUnique.mockResolvedValue({ diff --git a/tests/unit/commerce/delivery-assignments.test.js b/tests/unit/commerce/delivery-assignments.test.js index e19fbd44..35e19b8c 100644 --- a/tests/unit/commerce/delivery-assignments.test.js +++ b/tests/unit/commerce/delivery-assignments.test.js @@ -217,7 +217,7 @@ describe("GET /api/stores/:storeId/orders/:orderId/deliveries", () => { vi.mocked(prisma.orders.findUnique).mockResolvedValueOnce({ id_order: 100, fk_store: 1, - store: { fk_user: 1 } + store: { fk_user: 1, status: true } }); vi.mocked(prisma.deliveries.findUnique).mockResolvedValueOnce({ @@ -281,7 +281,7 @@ describe("GET /api/stores/:storeId/orders/:orderId/deliveries", () => { vi.mocked(prisma.orders.findUnique).mockResolvedValueOnce({ id_order: 100, fk_store: 1, - store: { fk_user: 1 } + store: { fk_user: 1, status: true } }); vi.mocked(prisma.deliveries.findUnique).mockResolvedValueOnce({ diff --git a/tests/unit/delivery/delivery-assignments.test.js b/tests/unit/delivery/delivery-assignments.test.js index fda51350..8defe864 100644 --- a/tests/unit/delivery/delivery-assignments.test.js +++ b/tests/unit/delivery/delivery-assignments.test.js @@ -79,7 +79,7 @@ const mockOrder = { fk_store: 10, fk_user: 20, order_status: "PENDING", - store: { fk_user: 1 }, + store: { fk_user: 1, status: true }, }; const mockDelivery = { diff --git a/tests/unit/delivery/delivery-register.test.js b/tests/unit/delivery/delivery-register.test.js index c185de86..3b07fef6 100644 --- a/tests/unit/delivery/delivery-register.test.js +++ b/tests/unit/delivery/delivery-register.test.js @@ -122,8 +122,7 @@ describe("POST /api/deliveries/register", () => { id_user: 10, role: "CUSTOMER", phone: "1234567890", - delivery: null, - phone: "1234567890" + delivery: null }); prisma.users.update.mockResolvedValue({