Skip to content
53 changes: 51 additions & 2 deletions src/modules/commerce/commerces/store.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getStoresService,
getAllProductsByStoreService,
filterStoreProductsService,
updateStoreStatusService,
deleteStoreService
} from "./store.service.js";
import jwt from "jsonwebtoken";
Expand Down Expand Up @@ -60,13 +61,34 @@ export const updateStore = async (req, res) => {
}
};

// Ruta pública — clientes ven solo comercios ACTIVE
export const getStoreById = async (req, res) => {
try {
const { id } = req.params;
const store = await getStoreByIdService(id);
if (!store || !store.status) {
throw { status: 404, message: "Comercio no encontrado" };
//if (!store || !store.status) {
//throw { status: 404, message: "Comercio no encontrado" };
//}
return res.status(200).json(store);
} catch (error) {
return res.status(error.status || 500).json({
message: error.message || "Error interno"
});
}
};

// Ruta autenticada — SELLER puede ver su comercio aunque esté INACTIVE
export const getMyStore = async (req, res) => {
try {
const { id } = req.params;

// Verificar que el comercio pertenece al usuario autenticado
const store = await getStoreByIdService(id, { ignoreStoreStatus: true });

if (store.user?.id_user !== req.user?.id_user) {
return res.status(403).json({ message: "No tenés permisos para ver este comercio" });
Comment on lines +88 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== authenticate middleware ==\n'
fd -p 'jwt.config.js' src | xargs -r sed -n '1,220p'

printf '\n== usos de req.user.id_user ==\n'
rg -n -C2 '\breq\.user\??\.id_user\b' src

Repository: CrisNAC/BackendMarketplace

Length of output: 19504


Verificá el tipo antes de comparar el owner con !==.

store.user.id_user sale numérico de Prisma, pero req.user.id_user viene como string desde el JWT (como se ve en la línea 25 del mismo archivo con Number(req.user.id_user)). Sin casteo, el dueño legítimo cae en 403 por diferencia de tipo.

Ajuste requerido
-    if (store.user?.id_user !== req.user?.id_user) {
+    if (Number(store.user?.id_user) !== Number(req.user?.id_user)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (store.user?.id_user !== req.user?.id_user) {
return res.status(403).json({ message: "No tenés permisos para ver este comercio" });
if (Number(store.user?.id_user) !== Number(req.user?.id_user)) {
return res.status(403).json({ message: "No tenés permisos para ver este comercio" });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/commerce/commerces/store.controller.js` around lines 88 - 89, The
owner check compares store.user.id_user (a numeric Prisma ID) with
req.user.id_user (a string from the JWT), causing legitimate owners to be
rejected; update the condition to compare like types—e.g., cast req.user.id_user
to Number or cast store.user.id_user to String—so change the guard using
store.user?.id_user !== Number(req.user?.id_user) (or Number(req.user.id_user)
where used) and keep the same null-safe accessors (store.user?.id_user and
req.user?.id_user) to avoid runtime errors.

}

return res.status(200).json(store);
} catch (error) {
return res.status(error.status || 500).json({
Expand Down Expand Up @@ -123,6 +145,33 @@ export const filterStoreProducts = async (req, res) => {
}
};

export const updateStoreStatus = async (req, res) => {
try {
const { id } = req.params;
const { store_status } = req.body;

if (!store_status) {
return res.status(400).json({ message: "store_status es requerido" });
}

const store = await updateStoreStatusService(
req.user?.id_user,
id,
store_status
);

return res.status(200).json({
success: true,
message: `Comercio ${store_status === "ACTIVE" ? "habilitado" : "deshabilitado"} exitosamente`,
data: store
});
} catch (error) {
return res.status(error.status || 500).json({
message: error.message || "Error interno del servidor"
});
}
};

/**
*
* @param {*} req
Expand Down
8 changes: 8 additions & 0 deletions src/modules/commerce/commerces/store.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
createStore,
updateStore,
getStoreById,
getMyStore,
getStores,
getAllProductsByStore,
filterStoreProducts,
updateStoreStatus,
deleteStore
} from "./store.controller.js";
import { parsePagination } from "../../../middlewares/pagination.middleware.js";
Expand Down Expand Up @@ -156,6 +158,9 @@ router.put("/:id", authenticate, updateStore);
*/
router.get("/", getStores);

// Ruta autenticada para el SELLER — va ANTES de /:id para no colisionar
router.get("/my/:id", authenticate, getMyStore); // ← agregar

/**
* @swagger
* /api/commerces/{id}:
Expand Down Expand Up @@ -307,6 +312,9 @@ router.get(
filterStoreProducts
);

// PATCH /api/commerces/:id/status — habilitar/deshabilitar comercio
router.patch("/:id/status", authenticate, updateStoreStatus);

/**
* @swagger
* /api/commerces/{id}:
Expand Down
40 changes: 38 additions & 2 deletions src/modules/commerce/commerces/store.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ export const updateStoreService = async (
}
};

export const getStoreByIdService = async (id) => {
export const getStoreByIdService = async (id, { ignoreStoreStatus = false } = {}) => {
try {
// validaciones básicas
if (!id) {
Expand All @@ -779,6 +779,7 @@ export const getStoreByIdService = async (id) => {
instagram_url: true,
tiktok_url: true,
status: true,
store_status: true,
created_at: true,
user: {
select: { id_user: true, name: true, email: true }
Expand Down Expand Up @@ -826,6 +827,11 @@ export const getStoreByIdService = async (id) => {
throw { status: 404, message: "Comercio no encontrado" };
}

// DESPUÉS — null se trata como ACTIVE (comportamiento por defecto del schema)
if (!ignoreStoreStatus && store.store_status && store.store_status !== "ACTIVE") {
throw { status: 404, message: "Comercio no disponible" };
}

return mapStoreWithPricedProducts(store);

} catch (error) {
Expand Down Expand Up @@ -1059,6 +1065,33 @@ export const filterStoreProductsService = async (id, filters, pagination) => {
}
};

export const updateStoreStatusService = async (authenticatedUserId, storeId, store_status) => {
const store = await getAuthorizedStoreOwnerService(authenticatedUserId, storeId);

const ALLOWED_STATUSES = ["ACTIVE", "INACTIVE"];
if (!ALLOWED_STATUSES.includes(store_status)) {
throw { status: 400, message: "store_status debe ser ACTIVE o INACTIVE" };
}

await prisma.$transaction([
prisma.stores.update({
where: { id_store: store.id_store },
data: { store_status }
}),
// Al desactivar → ocultar productos. Al activar → hacerlos visibles nuevamente.
prisma.products.updateMany({
where: { fk_store: store.id_store, status: true },
data: { visible: store_status === "ACTIVE" }
})
Comment on lines +1076 to +1085

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

No pisen products.visible para reflejar store_status.

Al desactivar guardás visible = false en todos los productos y al reactivar los dejás todos en true. Eso borra la preferencia real del vendedor; además getAllProductsByStoreService no filtra visible, así que esta sincronización tampoco garantiza ocultarlos. El gating público debería depender de stores.status/store_status, o de un flag separado.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/commerce/commerces/store.service.js` around lines 1076 - 1085,
The transaction is overwriting products.visible based on store_status (see
prisma.stores.update and prisma.products.updateMany) which destroys seller
preferences; stop mutating products.visible in store status changes and instead
enforce public gating based on stores.store_status (store_status) or introduce a
dedicated flag (e.g., products.hidden_by_store) if you need a reversible
store-level override; update any consumers (notably
getAllProductsByStoreService) to filter or gate results by the store's status
(or by the new dedicated flag) rather than relying on products.visible being
toggled.

]);

const updated = await prisma.stores.findUnique({
where: { id_store: store.id_store },
select: { id_store: true, name: true, store_status: true, status: true }
});

return updated;
};

/**
* Esta funcion se utiliza para el borrado logico de un comercio y sus respectivos productos en base al usuario autenticado que debe ser el dueño.
Expand Down Expand Up @@ -1091,7 +1124,10 @@ export const getStoresService = async (filters = {}) => {
const categoryIdRaw =
filters.storeCategoryId ?? filters.categoryId ?? filters.fk_store_category;

const where = { status: true };
const where = {
status: true,
store_status: "ACTIVE"
};

if (search) {
where.OR = [
Expand Down
Loading
Loading