Repository navigation
Endpoint para crear categoría como admin - #116
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 17 minutes and 52 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughSe agrega una nueva funcionalidad de solicitudes de categorías de productos, permitiendo a vendedores crear solicitudes de categorías pendientes de aprobación administrativa. Incluye rutas, controladores, servicios, esquemas OpenAPI y pruebas completas para ambos flujos (comercio y administración). Changes
Sequence Diagram(s)sequenceDiagram
actor Seller
participant Client
participant Server
participant DB as Database
Seller->>Client: Envía formulario con nombre de categoría
Client->>Server: POST /api/commerces/category-requests (JWT + ROLES.SELLER)
Server->>Server: Valida nombre (no vacío, ≤100 caracteres, trimmed)
Server->>DB: Consulta categoría existente (case-insensitive)
alt Categoría ya existe
DB-->>Server: Categoría encontrada
Server-->>Client: 409 Conflict
Client-->>Seller: Error: categoría ya existe
else Categoría no existe
DB-->>Server: No encontrada
Server->>DB: Crea categoría con visible=false, status=true
DB-->>Server: categoryId, name, timestamps
Server-->>Client: 201 Created + response payload
Client-->>Seller: Solicitud creada exitosamente
end
sequenceDiagram
actor Admin
participant Client
participant Server
participant DB as Database
Admin->>Client: Envía decisión (approve/reject) para solicitud
Client->>Server: PATCH /api/admin/categories/:id (JWT + ROLES.ADMIN)
Server->>Server: Valida decision (approve|reject)
Server->>DB: Obtiene categoría por id
alt Categoría no encontrada
DB-->>Server: null
Server-->>Client: 404 Not Found
Client-->>Admin: Error: categoría no existe
else Categoría en estado pendiente (status=true, visible=false)
DB-->>Server: Categoría encontrada
alt Aprobar
Server->>DB: Actualiza visible=true, status=true
DB-->>Server: Categoría actualizada
Server-->>Client: 200 OK + decision=approve
Client-->>Admin: Solicitud aprobada
else Rechazar
Server->>DB: Inicia transacción
DB->>DB: Reasigna products.fk_product_category → 1
DB->>DB: Actualiza categoría visible=false, status=false
DB-->>Server: Transacción completada
Server-->>Client: 200 OK + decision=reject
Client-->>Admin: Solicitud rechazada
end
else Categoría NO en estado pendiente
DB-->>Server: Categoría encontrada pero estado inválido
Server-->>Client: 400 Validation Error
Client-->>Admin: Error: categoría no puede procesarse
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutos Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (6)
src/config/swagger.config.js (1)
47-47: Nombre de tag mezcla idiomas.El resto de tags están en inglés (
Session,Commerces,Products,Users,Orders,Images,Admin) y este se llamaCategorías. Para consistencia y para evitar problemas de orden/alfabetización en Swagger UI, considerá renombrar aCategories. Lostags: [Categorías]en los JSDoc de las rutas deberían actualizarse en paralelo.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/swagger.config.js` at line 47, Rename the Swagger tag label "Categorías" to "Categories" in swagger.config.js (the tags array entry currently { name: "Categorías", description: "Gestión de categorías de productos" }) and update its description to English if desired; then update all JSDoc route annotations that use tags: ["Categorías"] to tags: ["Categories"] so Swagger UI tag ordering and consistency match the other tags (Session, Commerces, Products, Users, Orders, Images, Admin).src/modules/admin/categories/admin-category.controller.js (1)
13-20: Considerá envolver la respuesta 201 con{ message, data }.Mantener consistencia con el controlador de commerce (
category-request.controller.js:21-24) simplifica el consumo desde el front. Alternativamente, unificar ambos controladores en el shape plano. Elegí un único estilo y aplicalo a ambos.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/admin/categories/admin-category.controller.js` around lines 13 - 20, La respuesta del controlador createAdminProductCategory devuelve actualmente el resultado plano; cambia el res.status(201).json(...) para devolver un objeto con { message, data } consistente con category-request.controller.js; es decir, cuando createAdminProductCategoryService(req.body?.name) retorna resultado envuélvelo en { message: 'Category created successfully' (o el texto acordado), data: result } y mantén el manejo de errores en next(error) sin cambios.src/docs/schemas/admin/admin-category.schema.js (1)
118-129: Agregarenumadecisionen la respuesta.El request schema
AdminCategoryRequestDecisionya restringedecisiona["approve","reject"]; en la respuesta queda como string libre. Replicar elenumhace la documentación más precisa y habilita generadores de tipos.Propuesta
- decision: { type: "string", example: "approve" }, + decision: { type: "string", enum: ["approve", "reject"], example: "approve" },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/docs/schemas/admin/admin-category.schema.js` around lines 118 - 129, La respuesta OpenAPI schema AdminCategoryRequestDecisionResponse actualmente declara decision como string libre; añade el mismo enum que el request para limitarlo a ["approve","reject"]. En la definición AdminCategoryRequestDecisionResponse agrega la propiedad enum en decision (por ejemplo decision: { type: "string", enum: ["approve","reject"], example: "approve" }) para que la documentación y los generadores de tipos reflejen las opciones válidas.src/modules/commerce/category-requests/category-request.controller.js (1)
16-28: Shape de respuesta inconsistente concreateAdminProductCategory.Este controlador responde
{ message, data }mientras quesrc/modules/admin/categories/admin-category.controller.js(líneas 13-20) devuelve elresultplano en el 201. Conviene unificar el contrato (ambos con{ message, data }o ambos planos) para simplificar el cliente y la documentación Swagger.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/commerce/category-requests/category-request.controller.js` around lines 16 - 28, The createCategoryRequest controller currently returns { message, data } while the admin controller (createAdminProductCategory) returns the raw result; to unify contracts, change createCategoryRequest (exported function createCategoryRequest) to return the same JSON shape as createAdminProductCategory (i.e., return the created categoryRequest object directly in res.status(201).json(...)), and update any related Swagger docs/tests that expect the previous { message, data } shape to the new unified shape; leave createCategoryRequestService usage intact.src/modules/commerce/category-requests/category-request.service.js (1)
9-21: Lógica de validación duplicada con el servicio de admin.
validateCategoryNameaquí ynormalizeAdminCategoryNameensrc/modules/admin/categories/admin-category.service.js(líneas 10-21) implementan exactamente la misma regla (no vacío, trim, ≤100). Incluso el mensaje de error difiere levemente ("exceder" vs "superar"). Conviene extraer un helper compartido (p. ej. ensrc/utils/validators/category-name.js) para evitar divergencias futuras.♻️ Ejemplo de helper compartido
+// src/utils/validators/category-name.js +import { ValidationError } from "../../lib/errors.js"; + +export const normalizeCategoryName = (name) => { + if (typeof name !== "string" || name.trim().length === 0) { + throw new ValidationError("El nombre de la categoría no puede estar vacío"); + } + const normalized = name.trim(); + if (normalized.length > 100) { + throw new ValidationError("El nombre de la categoría no puede exceder 100 caracteres"); + } + return normalized; +};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/commerce/category-requests/category-request.service.js` around lines 9 - 21, Extraer la lógica común de validación/normalización del nombre de categoría a un helper compartido (p.ej. exportar normalizeCategoryName desde src/utils/validators/category-name.js) y reemplazar las implementaciones duplicadas: hacer que validateCategoryName en category-request.service.js y normalizeAdminCategoryName en admin-category.service.js llamen a normalizeCategoryName; el helper debe validar no vacío, trim, longitud ≤100 y lanzar la misma ValidationError con un mensaje unificado; actualizar las referencias para importar normalizeCategoryName y eliminar la lógica duplicada de ambas funciones.tests/unit/admin/admin-category.test.js (1)
526-705: Sugerencia: agregar caso que ejercite el reject sin productos a reasignar.La prueba de rechazo simula
products.updateManydevolviendo{ count: 4 }, pero en el flujo real una solicitud pendiente (visible=false recién creada) no suele tener productos asociados. Convendría agregar un test específico concount: 0que assertee igualmente la desactivación (visible: false,status: false) y eldecision: "reject"para documentar el comportamiento esperado en el caso típico.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/admin/admin-category.test.js` around lines 526 - 705, Add a new unit test in the "PATCH /api/admin/categories/:id" describe block (e.g., it("rechaza solicitud pendiente sin productos y devuelve 200")) that exercises the reject path when there are zero products to reassign: mock prisma.productCategories.findUnique to return a pending category (visible: false, status: true), mock prisma.products.updateMany to resolve to { count: 0 }, mock prisma.productCategories.update (and prisma.$transaction if used) to return the deactivated category, call the endpoint via asRole(request(app).patch(...).send({ decision: "reject" }), "admin"), and assert res.status is 200, prisma.products.updateMany was called with where: { fk_product_category: <id> } and data: { fk_product_category: 1 }, prisma.productCategories.update was called to set visible: false and status: false, and the response body contains visible: false, status: false, and decision: "reject".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/modules/admin/categories/admin-category.routes.js`:
- Around line 172-208: The PATCH handler router.patch("/:id", authenticate,
requireRole(ROLES.ADMIN), processAdminCategoryRequest) currently treats any
category with status===true && visible===false as a "request", so a regular
category hidden by an admin can be approved/rejected; update
processAdminCategoryRequestService to only operate on true request records by
adding a strict discriminator (e.g., an is_request boolean or checking a
dedicated request table/flag) and return 400/404 when the target category is not
a request, and update the Swagger summary/description to state this endpoint
only handles category requests (or document the new discriminator field) so
behavior is unambiguous.
In `@src/modules/admin/categories/admin-category.service.js`:
- Around line 365-441: The processAdminCategoryRequestService currently treats
pending requests by checking status/visible and performs a products reassignment
to hardcoded id=1 on reject; change this to use an explicit request
discriminator and a named constant for the uncategorized id and avoid
unnecessary reassignment: add or check a dedicated request flag (e.g.,
is_request) when validating pending requests inside
processAdminCategoryRequestService (and update normalizeCategoryRequestDecision
usage if needed), remove or guard the prisma.products.updateMany(...) call so it
only runs if there truly can be products linked to a request, and replace the
hardcoded 1 with a shared UNCATEGORIZED_CATEGORY_ID constant used also by
deleteAdminProductCategoryService to centralize the uncategorized category id.
Ensure references include processAdminCategoryRequestService,
prisma.productCategories.update, prisma.products.updateMany, and
deleteAdminProductCategoryService so reviewers can find and adjust the logic.
- Around line 23-54: createAdminProductCategoryService suffers a TOCTOU race:
using findFirst then create can produce duplicates under concurrency; ensure a
case-insensitive unique index exists on productCategories.name (or a functional
index) at the DB/schema level and remove reliance on findFirst for uniqueness,
then wrap the prisma.productCategories.create call in a try/catch and handle
Prisma's unique-constraint error (Prisma.PrismaClientKnownRequestError with code
"P2002") by throwing the same ValidationError ("Ya existe una categoría con ese
nombre"); keep the normalizedName logic and the create payload but rely on the
DB unique index + P2002 handling to enforce uniqueness.
- Around line 37-39: The duplicate-category branch in admin-category.service.js
currently throws ValidationError when existingCategory is truthy; change that to
throw ConflictError (to match createCategoryRequestService's semantics and
return 409) by replacing the ValidationError instantiation in the
existingCategory check with ConflictError, and update the corresponding test
expectation in admin-category.test.js (the test around the duplicate-name case)
to expect 409 instead of 400.
In `@src/modules/commerce/category-requests/category-request.controller.js`:
- Around line 3-19: El controlador createCategoryRequest está enviando solo name
a createCategoryRequestService y no guarda quién solicitó la categoría
(req.user.id_user según JSDoc), lo que impide trazabilidad; fix: change the
controller to pass req.user (or req.user.id_user/req.user.id_store) into
createCategoryRequestService, then update the service
(createCategoryRequestService) to persist the requester info either by adding
id_user/id_store columns to ProductCategories or by creating a
ProductCategoryRequests table that records name, id_user, id_store, status and
timestamps; alternatively, if requester info is intentionally not required,
update the JSDoc to remove the req.user/id_user documentation to match behavior.
In `@src/modules/commerce/category-requests/category-request.service.js`:
- Around line 28-78: Add a case-insensitive unique constraint on
ProductCategories.name in the Prisma schema (e.g., @@unique([name]) with the
appropriate case-insensitive collation) to prevent TOCTOU duplicates, then
update createCategoryRequestService to catch Prisma unique-constraint errors
(Prisma P2002) thrown by prisma.productCategories.create and rethrow a
ConflictError with the same user-facing message; ensure
checkExistingCategoryOrPendingRequest and validateCategoryName remain but do not
assume they alone prevent duplicates—handle the P2002 from
prisma.productCategories.create and map it to the existing ConflictError
response.
---
Nitpick comments:
In `@src/config/swagger.config.js`:
- Line 47: Rename the Swagger tag label "Categorías" to "Categories" in
swagger.config.js (the tags array entry currently { name: "Categorías",
description: "Gestión de categorías de productos" }) and update its description
to English if desired; then update all JSDoc route annotations that use tags:
["Categorías"] to tags: ["Categories"] so Swagger UI tag ordering and
consistency match the other tags (Session, Commerces, Products, Users, Orders,
Images, Admin).
In `@src/docs/schemas/admin/admin-category.schema.js`:
- Around line 118-129: La respuesta OpenAPI schema
AdminCategoryRequestDecisionResponse actualmente declara decision como string
libre; añade el mismo enum que el request para limitarlo a ["approve","reject"].
En la definición AdminCategoryRequestDecisionResponse agrega la propiedad enum
en decision (por ejemplo decision: { type: "string", enum: ["approve","reject"],
example: "approve" }) para que la documentación y los generadores de tipos
reflejen las opciones válidas.
In `@src/modules/admin/categories/admin-category.controller.js`:
- Around line 13-20: La respuesta del controlador createAdminProductCategory
devuelve actualmente el resultado plano; cambia el res.status(201).json(...)
para devolver un objeto con { message, data } consistente con
category-request.controller.js; es decir, cuando
createAdminProductCategoryService(req.body?.name) retorna resultado envuélvelo
en { message: 'Category created successfully' (o el texto acordado), data:
result } y mantén el manejo de errores en next(error) sin cambios.
In `@src/modules/commerce/category-requests/category-request.controller.js`:
- Around line 16-28: The createCategoryRequest controller currently returns {
message, data } while the admin controller (createAdminProductCategory) returns
the raw result; to unify contracts, change createCategoryRequest (exported
function createCategoryRequest) to return the same JSON shape as
createAdminProductCategory (i.e., return the created categoryRequest object
directly in res.status(201).json(...)), and update any related Swagger
docs/tests that expect the previous { message, data } shape to the new unified
shape; leave createCategoryRequestService usage intact.
In `@src/modules/commerce/category-requests/category-request.service.js`:
- Around line 9-21: Extraer la lógica común de validación/normalización del
nombre de categoría a un helper compartido (p.ej. exportar normalizeCategoryName
desde src/utils/validators/category-name.js) y reemplazar las implementaciones
duplicadas: hacer que validateCategoryName en category-request.service.js y
normalizeAdminCategoryName en admin-category.service.js llamen a
normalizeCategoryName; el helper debe validar no vacío, trim, longitud ≤100 y
lanzar la misma ValidationError con un mensaje unificado; actualizar las
referencias para importar normalizeCategoryName y eliminar la lógica duplicada
de ambas funciones.
In `@tests/unit/admin/admin-category.test.js`:
- Around line 526-705: Add a new unit test in the "PATCH
/api/admin/categories/:id" describe block (e.g., it("rechaza solicitud pendiente
sin productos y devuelve 200")) that exercises the reject path when there are
zero products to reassign: mock prisma.productCategories.findUnique to return a
pending category (visible: false, status: true), mock prisma.products.updateMany
to resolve to { count: 0 }, mock prisma.productCategories.update (and
prisma.$transaction if used) to return the deactivated category, call the
endpoint via asRole(request(app).patch(...).send({ decision: "reject" }),
"admin"), and assert res.status is 200, prisma.products.updateMany was called
with where: { fk_product_category: <id> } and data: { fk_product_category: 1 },
prisma.productCategories.update was called to set visible: false and status:
false, and the response body contains visible: false, status: false, and
decision: "reject".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d0028462-c4b5-4504-848d-93d365a56515
📒 Files selected for processing (14)
src/app.jssrc/config/swagger.config.jssrc/docs/schemas/admin/admin-category.schema.jssrc/docs/schemas/commerce/category-request.schema.jssrc/docs/schemas/index.jssrc/modules/admin/categories/admin-category.controller.jssrc/modules/admin/categories/admin-category.routes.jssrc/modules/admin/categories/admin-category.service.jssrc/modules/commerce/category-requests/category-request.controller.jssrc/modules/commerce/category-requests/category-request.routes.jssrc/modules/commerce/category-requests/category-request.service.jstests/e2e/product/filter-product.test.jstests/unit/admin/admin-category.test.jstests/unit/commerce/category-request.test.js
| /** | ||
| * @swagger | ||
| * /api/admin/categories/{id}: | ||
| * patch: | ||
| * summary: Aprobar o rechazar solicitud de categoría (Admin) | ||
| * tags: [Admin] | ||
| * security: | ||
| * - cookieAuth: [] | ||
| * parameters: | ||
| * - in: path | ||
| * name: id | ||
| * required: true | ||
| * schema: | ||
| * type: integer | ||
| * requestBody: | ||
| * required: true | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * $ref: '#/components/schemas/AdminCategoryRequestDecision' | ||
| * responses: | ||
| * 200: | ||
| * description: Solicitud de categoría procesada | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * $ref: '#/components/schemas/AdminCategoryRequestDecisionResponse' | ||
| * 400: | ||
| * description: Solicitud inválida o ya procesada | ||
| * 401: | ||
| * description: No autenticado | ||
| * 403: | ||
| * description: Sin permisos de administrador | ||
| * 404: | ||
| * description: Solicitud de categoría no encontrada | ||
| */ | ||
| router.patch("/:id", authenticate, requireRole(ROLES.ADMIN), processAdminCategoryRequest); |
There was a problem hiding this comment.
El PATCH opera sobre cualquier productCategories, no solo sobre solicitudes.
La ruta está documentada como "Aprobar o rechazar solicitud de categoría", pero en el servicio la condición de "pendiente" se infiere de status === true && visible === false (ver processAdminCategoryRequestService en admin-category.service.js). Una categoría normal a la que un admin puso visible: false vía PUT /:id queda indistinguible de una solicitud pendiente y, por lo tanto, podría ser "aprobada" (se vuelve visible) o "rechazada" (se desactiva y sus productos se reasignan a id=1) a través de este endpoint. Consideren documentar la restricción en el summary/description y/o introducir un campo discriminador (p. ej. is_request o una tabla separada) para evitar ambigüedad.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/modules/admin/categories/admin-category.routes.js` around lines 172 -
208, The PATCH handler router.patch("/:id", authenticate,
requireRole(ROLES.ADMIN), processAdminCategoryRequest) currently treats any
category with status===true && visible===false as a "request", so a regular
category hidden by an admin can be approved/rejected; update
processAdminCategoryRequestService to only operate on true request records by
adding a strict discriminator (e.g., an is_request boolean or checking a
dedicated request table/flag) and return 400/404 when the target category is not
a request, and update the Swagger summary/description to state this endpoint
only handles category requests (or document the new discriminator field) so
behavior is unambiguous.
| export const createAdminProductCategoryService = async (name) => { | ||
| const normalizedName = normalizeAdminCategoryName(name); | ||
|
|
||
| const existingCategory = await prisma.productCategories.findFirst({ | ||
| where: { | ||
| name: { | ||
| equals: normalizedName, | ||
| mode: "insensitive" | ||
| }, | ||
| status: true | ||
| }, | ||
| select: { id_product_category: true } | ||
| }); | ||
|
|
||
| if (existingCategory) { | ||
| throw new ValidationError("Ya existe una categoría con ese nombre"); | ||
| } | ||
|
|
||
| const createdCategory = await prisma.productCategories.create({ | ||
| data: { | ||
| name: normalizedName, | ||
| visible: true, | ||
| status: true | ||
| }, | ||
| select: { | ||
| id_product_category: true, | ||
| name: true, | ||
| visible: true, | ||
| status: true, | ||
| created_at: true | ||
| } | ||
| }); |
There was a problem hiding this comment.
Misma condición de carrera (TOCTOU) que en el servicio de commerce.
findFirst + create sin transacción ni índice único permite duplicados bajo concurrencia. Ver el comentario detallado en category-request.service.js; la solución (unique index case-insensitive + manejo de P2002) aplica también acá.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/modules/admin/categories/admin-category.service.js` around lines 23 - 54,
createAdminProductCategoryService suffers a TOCTOU race: using findFirst then
create can produce duplicates under concurrency; ensure a case-insensitive
unique index exists on productCategories.name (or a functional index) at the
DB/schema level and remove reliance on findFirst for uniqueness, then wrap the
prisma.productCategories.create call in a try/catch and handle Prisma's
unique-constraint error (Prisma.PrismaClientKnownRequestError with code "P2002")
by throwing the same ValidationError ("Ya existe una categoría con ese nombre");
keep the normalizedName logic and the create payload but rely on the DB unique
index + P2002 handling to enforce uniqueness.
| if (existingCategory) { | ||
| throw new ValidationError("Ya existe una categoría con ese nombre"); | ||
| } |
There was a problem hiding this comment.
Status HTTP inconsistente con el flujo de commerce para el mismo error.
En createCategoryRequestService (commerce) el duplicado se lanza como ConflictError → 409, acá lo lanzan como ValidationError → 400 con el mismo significado semántico ("ya existe una categoría con ese nombre"). Los clientes reciben códigos diferentes para la misma clase de error. Sugiero usar ConflictError también acá para consistencia de API (y ajustar el test de admin-category.test.js línea 153 que espera 400).
🔧 Cambio propuesto
-import { NotFoundError, ValidationError } from "../../../lib/errors.js";
+import { NotFoundError, ValidationError, ConflictError } from "../../../lib/errors.js";
@@
if (existingCategory) {
- throw new ValidationError("Ya existe una categoría con ese nombre");
+ throw new ConflictError("Ya existe una categoría con ese nombre");
}📝 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.
| if (existingCategory) { | |
| throw new ValidationError("Ya existe una categoría con ese nombre"); | |
| } | |
| import { NotFoundError, ValidationError, ConflictError } from "../../../lib/errors.js"; | |
| // ... other code ... | |
| if (existingCategory) { | |
| throw new ConflictError("Ya existe una categoría con ese nombre"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/modules/admin/categories/admin-category.service.js` around lines 37 - 39,
The duplicate-category branch in admin-category.service.js currently throws
ValidationError when existingCategory is truthy; change that to throw
ConflictError (to match createCategoryRequestService's semantics and return 409)
by replacing the ValidationError instantiation in the existingCategory check
with ConflictError, and update the corresponding test expectation in
admin-category.test.js (the test around the duplicate-name case) to expect 409
instead of 400.
| export const processAdminCategoryRequestService = async (id, decision) => { | ||
| const normalizedDecision = normalizeCategoryRequestDecision(decision); | ||
|
|
||
| const category = await prisma.productCategories.findUnique({ | ||
| where: { id_product_category: id }, | ||
| select: { | ||
| id_product_category: true, | ||
| name: true, | ||
| visible: true, | ||
| status: true, | ||
| created_at: true, | ||
| updated_at: true | ||
| } | ||
| }); | ||
|
|
||
| if (!category) { | ||
| throw new NotFoundError("Solicitud de categoría no encontrada"); | ||
| } | ||
|
|
||
| const isPendingRequest = category.status === true && category.visible === false; | ||
| if (!isPendingRequest) { | ||
| throw new ValidationError("La solicitud ya fue procesada"); | ||
| } | ||
|
|
||
| let updatedCategory; | ||
|
|
||
| if (normalizedDecision === "approve") { | ||
| updatedCategory = await prisma.productCategories.update({ | ||
| where: { id_product_category: id }, | ||
| data: { | ||
| visible: true, | ||
| status: true | ||
| }, | ||
| select: { | ||
| id_product_category: true, | ||
| name: true, | ||
| visible: true, | ||
| status: true, | ||
| created_at: true, | ||
| updated_at: true | ||
| } | ||
| }); | ||
| } else { | ||
| const [, rejectedCategory] = await prisma.$transaction([ | ||
| prisma.products.updateMany({ | ||
| where: { fk_product_category: id }, | ||
| data: { fk_product_category: 1 } | ||
| }), | ||
| prisma.productCategories.update({ | ||
| where: { id_product_category: id }, | ||
| data: { | ||
| visible: false, | ||
| status: false | ||
| }, | ||
| select: { | ||
| id_product_category: true, | ||
| name: true, | ||
| visible: true, | ||
| status: true, | ||
| created_at: true, | ||
| updated_at: true | ||
| } | ||
| }) | ||
| ]); | ||
|
|
||
| updatedCategory = rejectedCategory; | ||
| } | ||
|
|
||
| return { | ||
| id: updatedCategory.id_product_category, | ||
| name: updatedCategory.name, | ||
| visible: updatedCategory.visible, | ||
| status: updatedCategory.status, | ||
| decision: normalizedDecision, | ||
| createdAt: updatedCategory.created_at, | ||
| updatedAt: updatedCategory.updated_at | ||
| }; |
There was a problem hiding this comment.
Detección de solicitud pendiente frágil y reasignación innecesaria en el reject.
Dos puntos relacionados:
-
isPendingRequest = status === true && visible === false(línea 384) confunde solicitudes pendientes con categorías normales ocultadas víaPUT /:id. Esto permitiría "aprobar"/"rechazar" una categoría existente, y en particular el rechazo le reasignaría todos sus productos aid=1de forma silenciosa. Idealmente agreguen un discriminador explícito (campois_request, tabla separada, o chequeo de que el registro no haya sido modificado después de creado). Ver también el comentario en la ruta PATCH. -
El
products.updateManydentro del reject (líneas 409-412) asume que una solicitud pendiente puede tener productos, pero por diseño no debería tenerlos (se crea convisible=false). Es código defensivo que, combinado con el punto 1, amplifica el riesgo: si por error entran productos apuntando a esa categoría o si el registro no es realmente una solicitud, se ejecuta una reasignación masiva. Una vez resuelto (1), evalúen si elupdateManysigue siendo necesario.
Además, el id 1 para "Sin categoría" está hardcodeado aquí y en deleteAdminProductCategoryService (línea 269). Extraerlo a una constante (por ejemplo UNCATEGORIZED_CATEGORY_ID) mejoraría la mantenibilidad.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/modules/admin/categories/admin-category.service.js` around lines 365 -
441, The processAdminCategoryRequestService currently treats pending requests by
checking status/visible and performs a products reassignment to hardcoded id=1
on reject; change this to use an explicit request discriminator and a named
constant for the uncategorized id and avoid unnecessary reassignment: add or
check a dedicated request flag (e.g., is_request) when validating pending
requests inside processAdminCategoryRequestService (and update
normalizeCategoryRequestDecision usage if needed), remove or guard the
prisma.products.updateMany(...) call so it only runs if there truly can be
products linked to a request, and replace the hardcoded 1 with a shared
UNCATEGORIZED_CATEGORY_ID constant used also by
deleteAdminProductCategoryService to centralize the uncategorized category id.
Ensure references include processAdminCategoryRequestService,
prisma.productCategories.update, prisma.products.updateMany, and
deleteAdminProductCategoryService so reviewers can find and adjust the logic.
| /** | ||
| * Crear una nueva solicitud de categoría de producto | ||
| * El comercio autenticado solicita la creación de una nueva categoría de productos | ||
| * @route POST /api/commerces/category-requests | ||
| * @access Comercio (SELLER) | ||
| * @param {Object} req - Request object | ||
| * @param {Object} req.body - Request body | ||
| * @param {string} req.body.name - Nombre de la categoría solicitada | ||
| * @param {Object} req.user - Usuario autenticado (inyectado por middleware JWT) | ||
| * @param {number} req.user.id_user - ID del usuario | ||
| * @param {Object} res - Response object | ||
| * @param {Function} next - Next middleware | ||
| */ | ||
| export const createCategoryRequest = async (req, res, next) => { | ||
| try { | ||
| const { name } = req.body; | ||
| const categoryRequest = await createCategoryRequestService(name); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirmar que el schema de Prisma no tiene un campo de solicitante en productCategories
fd -e prisma | xargs rg -n -A2 -B1 'model\s+ProductCategories|productCategories'
# Ver si el service usa req.user en algún lado
rg -n 'req\.user|id_user|id_store' src/modules/commerce/category-requestsRepository: CrisNAC/BackendMarketplace
Length of output: 590
🏁 Script executed:
#!/bin/bash
# Obtener el schema completo de Prisma para ProductCategories
fd -e prisma -exec cat {} \; | rg -A10 'model ProductCategories'Repository: CrisNAC/BackendMarketplace
Length of output: 207
🏁 Script executed:
#!/bin/bash
# Leer la implementación del servicio para ver qué recibe realmente
cat src/modules/commerce/category-requests/category-request.service.jsRepository: CrisNAC/BackendMarketplace
Length of output: 2562
🏁 Script executed:
#!/bin/bash
# Buscar si existe una tabla o modelo para rastrear solicitudes de categoría
rg -i 'categoryrequest|category.request|request.*categor' --type js --type tsRepository: CrisNAC/BackendMarketplace
Length of output: 12787
🏁 Script executed:
#!/bin/bash
# Obtener el archivo schema.prisma completo
find . -name "schema.prisma" -exec cat {} \;Repository: CrisNAC/BackendMarketplace
Length of output: 20634
🏁 Script executed:
#!/bin/bash
# Ver el controlador para confirmar qué se pasa al servicio
cat -n src/modules/commerce/category-requests/category-request.controller.jsRepository: CrisNAC/BackendMarketplace
Length of output: 1288
No se persiste el comercio/usuario solicitante.
El JSDoc documenta req.user.id_user como parámetro, pero el controlador (línea 19) sólo reenvía name al servicio. La tabla ProductCategories en el schema de Prisma no tiene campos para almacenar datos del solicitante (id_user, id_store o similar). Consecuencia: el admin no puede identificar quién solicitó la categoría (no hay trazabilidad), no puede notificar al solicitante sobre la aprobación/rechazo, y las métricas por comercio son imposibles.
Soluciones:
- Pasar
req.useral servicio, guardarid_user/id_storeen ProductCategories o una tabla auxiliar de solicitudes, o - Como mínimo, actualizar el JSDoc para reflejar que el usuario autenticado no se utiliza.
Si el requisito funcional incluye trazabilidad del solicitante, esto debe resolverse antes de mergear.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/modules/commerce/category-requests/category-request.controller.js` around
lines 3 - 19, El controlador createCategoryRequest está enviando solo name a
createCategoryRequestService y no guarda quién solicitó la categoría
(req.user.id_user según JSDoc), lo que impide trazabilidad; fix: change the
controller to pass req.user (or req.user.id_user/req.user.id_store) into
createCategoryRequestService, then update the service
(createCategoryRequestService) to persist the requester info either by adding
id_user/id_store columns to ProductCategories or by creating a
ProductCategoryRequests table that records name, id_user, id_store, status and
timestamps; alternatively, if requester info is intentionally not required,
update the JSDoc to remove the req.user/id_user documentation to match behavior.
| export const checkExistingCategoryOrPendingRequest = async (name) => { | ||
| const existingCategory = await prisma.productCategories.findFirst({ | ||
| where: { | ||
| name: { | ||
| equals: name, | ||
| mode: "insensitive" | ||
| }, | ||
| status: true | ||
| }, | ||
| select: { | ||
| visible: true | ||
| } | ||
| }); | ||
|
|
||
| if (existingCategory) { | ||
| if (existingCategory.visible) { | ||
| throw new ConflictError(`La categoría "${name}" ya existe`); | ||
| } | ||
|
|
||
| throw new ConflictError( | ||
| `Ya existe una solicitud pendiente para la categoría "${name}"` | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Crear una nueva solicitud de categoría | ||
| * @param {string} name - Nombre de la categoría solicitada | ||
| * @returns {Promise<Object>} La solicitud de categoría creada | ||
| */ | ||
| export const createCategoryRequestService = async (name) => { | ||
| const normalizedName = validateCategoryName(name); | ||
|
|
||
| await checkExistingCategoryOrPendingRequest(normalizedName); | ||
|
|
||
| // Se crea en ProductCategories como solicitud pendiente (visible=false) | ||
| const categoryRequest = await prisma.productCategories.create({ | ||
| data: { | ||
| name: normalizedName, | ||
| visible: false, | ||
| status: true | ||
| }, | ||
| select: { | ||
| id_product_category: true, | ||
| name: true, | ||
| visible: true, | ||
| status: true, | ||
| created_at: true, | ||
| updated_at: true | ||
| } | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -e prisma | xargs -I {} sh -c 'echo "=== {} ==="; cat {}'
rg -nP -C3 '(model\s+ProductCategories|productCategories)' --type-add 'prisma:*.prisma' -tprisma
rg -nP -C2 '@@unique|@unique' --type-add 'prisma:*.prisma' -tprismaRepository: CrisNAC/BackendMarketplace
Length of output: 24458
Agregar restricción única a nivel de base de datos en ProductCategories.name para evitar condición de carrera (TOCTOU).
checkExistingCategoryOrPendingRequest y prisma.productCategories.create no se ejecutan dentro de una transacción, y no existe un índice único case-insensitive sobre name en la tabla ProductCategories. Dos peticiones concurrentes con el mismo nombre pueden superar findFirst y crear dos filas duplicadas, evadiendo la validación en aplicación.
Recomendación: Agregar una restricción @@unique([name]) con collation insensible a mayúsculas en el modelo Prisma, luego capturar el error P2002 de Prisma y retornar ConflictError consistentemente.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/modules/commerce/category-requests/category-request.service.js` around
lines 28 - 78, Add a case-insensitive unique constraint on
ProductCategories.name in the Prisma schema (e.g., @@unique([name]) with the
appropriate case-insensitive collation) to prevent TOCTOU duplicates, then
update createCategoryRequestService to catch Prisma unique-constraint errors
(Prisma P2002) thrown by prisma.productCategories.create and rethrow a
ConflictError with the same user-facing message; ensure
checkExistingCategoryOrPendingRequest and validateCategoryName remain but do not
assume they alone prevent duplicates—handle the P2002 from
prisma.productCategories.create and map it to the existing ConflictError
response.
Summary by CodeRabbit
Notas de la versión
Nuevas funcionalidades
Corrección de errores
Documentación