Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/auditoria-bac.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,82 @@
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" };

Check warning on line 18 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyO&open=AZ6Ns-jko-BsXWWipFyO&pullRequest=195
}

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" };

Check warning on line 32 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyP&open=AZ6Ns-jko-BsXWWipFyP&pullRequest=195
}

return;
}

if (auth.role === "CUSTOMER") {
if (order.fk_user !== auth.id_user) {
throw { status: 403, message: "No tienes permiso para acceder a este pedido" };

Check warning on line 40 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyQ&open=AZ6Ns-jko-BsXWWipFyQ&pullRequest=195
}
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 }
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
select: { id_delivery_assignment: true }
});

if (!assignment) {
throw { status: 403, message: "No tienes permiso para acceder a este pedido" };

Check warning on line 56 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyR&open=AZ6Ns-jko-BsXWWipFyR&pullRequest=195
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

return;
}

throw { status: 403, message: "No tienes permiso para acceder a este recurso" };

Check warning on line 62 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyS&open=AZ6Ns-jko-BsXWWipFyS&pullRequest=195
};

export const createAssignmentService = async (data, authenticatedUser) => {
const auth = resolveAuth(authenticatedUser);
if (auth.role !== "SELLER") {
throw { status: 403, message: "No tienes permiso para asignar deliveries" };

Check warning on line 68 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyT&open=AZ6Ns-jko-BsXWWipFyT&pullRequest=195
}

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" };

Check warning on line 86 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyU&open=AZ6Ns-jko-BsXWWipFyU&pullRequest=195
}
Comment thread
Andoumeda marked this conversation as resolved.
Resolved

try {
const newDeliveryAssignment = await prisma.$transaction(async (tx) => {
await expireStalePendingAssignments({ fk_order }, tx);
Expand Down Expand Up @@ -74,11 +140,15 @@
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" };

Check warning on line 150 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyV&open=AZ6Ns-jko-BsXWWipFyV&pullRequest=195
}
if (delivery.delivery_status !== "ACTIVE" || !delivery.status) {
throw { status: 400, message: "El delivery debe estar activo para recibir pedidos" };
}
Expand All @@ -100,15 +170,23 @@
};

// 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 }
}
}
});
Expand All @@ -117,18 +195,46 @@
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" };

Check warning on line 200 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyW&open=AZ6Ns-jko-BsXWWipFyW&pullRequest=195
}
} 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" };

Check warning on line 204 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyX&open=AZ6Ns-jko-BsXWWipFyX&pullRequest=195
}
} 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" };

Check warning on line 208 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyY&open=AZ6Ns-jko-BsXWWipFyY&pullRequest=195
}
} else {
throw { status: 403, message: "No tienes permiso para ver esta asignación" };

Check warning on line 211 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyZ&open=AZ6Ns-jko-BsXWWipFyZ&pullRequest=195
}

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" };

Check warning on line 224 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFya&open=AZ6Ns-jko-BsXWWipFya&pullRequest=195
}

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({
Expand All @@ -144,15 +250,25 @@
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" };

Check warning on line 256 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyb&open=AZ6Ns-jko-BsXWWipFyb&pullRequest=195
}

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" };

Check warning on line 269 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyc&open=AZ6Ns-jko-BsXWWipFyc&pullRequest=195
}

await expireStalePendingAssignments({ fk_delivery: id_delivery });

const where = { fk_delivery: id_delivery };
Expand Down Expand Up @@ -193,15 +309,25 @@
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" };

Check warning on line 315 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyd&open=AZ6Ns-jko-BsXWWipFyd&pullRequest=195
}

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" };

Check warning on line 328 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFye&open=AZ6Ns-jko-BsXWWipFye&pullRequest=195
}

await expireStalePendingAssignments({ fk_delivery: id_delivery });

const pendingAssignments = await prisma.deliveryAssignments.findMany({
Expand All @@ -220,15 +346,27 @@
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" };

Check warning on line 356 in src/modules/delivery/delivery-assignments/delivery-assignments.service.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected an error object to be thrown.

See more on https://sonarcloud.io/project/issues?id=CrisNAC_BackendMarketplace&issues=AZ6Ns-jko-BsXWWipFyf&open=AZ6Ns-jko-BsXWWipFyf&pullRequest=195
}

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,
Expand Down
2 changes: 1 addition & 1 deletion src/modules/delivery/delivery/delivery.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
Expand Down
16 changes: 16 additions & 0 deletions src/modules/users/orders/order.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
},
select: { id_delivery_assignment: true }
});

if (!deliveryAssignment) {
throw new ForbiddenError("No tienes permisos para modificar este pedido.");
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

//validar transiciones permitidas por rol de user autenticado
const isPickup = order.fk_address == null;
const allowedTransitions = {
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/delivery-assignments-create.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe("POST /api/assignments", () => {
prisma.orders.findUnique.mockResolvedValue({
id_order: 5,
fk_store: 10,
store: { fk_user: 1 },
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

prisma.deliveries.findUnique.mockResolvedValue({
Expand All @@ -105,6 +106,7 @@ describe("POST /api/assignments", () => {

mockTx.deliveries.findUnique.mockResolvedValue({
id_delivery: 2,
fk_store: 10,
delivery_status: "ACTIVE",
status: true,
});
Expand Down
Loading
Loading