Skip to content

Endpoint para crear categoría como admin - #116

Merged
SebaKisser merged 2 commits into
devfrom
OM-475
Apr 21, 2026
Merged

SebaKisser merged 2 commits into
devfrom
OM-475

Conversation

@Andoumeda

@Andoumeda Andoumeda commented Apr 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Notas de la versión

  • Nuevas funcionalidades

    • Los vendedores pueden enviar solicitudes para crear nuevas categorías de productos.
    • Los administradores pueden crear, aprobar y rechazar solicitudes de categorías de productos.
  • Corrección de errores

    • Se corrigió el ordenamiento de productos por precio para usar el precio original.
  • Documentación

    • Se agregó documentación de API para operaciones de solicitudes de categorías.

@coderabbitai

coderabbitai Bot commented Apr 19, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@SebaKisser has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 52 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6fb68e05-e085-403f-9480-43343ee6d4c9

📥 Commits

Reviewing files that changed from the base of the PR and between 29564bc and 4243b9e.

📒 Files selected for processing (1)
  • src/modules/admin/categories/admin-category.service.js
📝 Walkthrough

Walkthrough

Se 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

Cohort / File(s) Summary
Bootstrapping & Enrutamiento
src/app.js
Se registra el nuevo router de solicitudes de categoría en /api/commerces/category-requests mediante app.use().
Documentación OpenAPI
src/config/swagger.config.js, src/docs/schemas/index.js
Se importan y exportan esquemas de solicitudes de categoría; se añade etiqueta Categorías al especificador Swagger.
Esquemas de Comercio
src/docs/schemas/commerce/category-request.schema.js
Se define módulo de esquemas OpenAPI con CategoryRequestResponse, CreateCategoryRequestBody, CategoryRequestErrorResponse y CategoryRequestSuccessResponse.
Esquemas de Administración
src/docs/schemas/admin/admin-category.schema.js
Se añaden esquemas AdminCreateCategoryBody, AdminCreateCategoryResponse, AdminCategoryRequestDecision y AdminCategoryRequestDecisionResponse.
Solicitudes de Categoría (Comercio)
src/modules/commerce/category-requests/category-request.controller.js, src/modules/commerce/category-requests/category-request.routes.js, src/modules/commerce/category-requests/category-request.service.js
Nuevo módulo completo con controller, rutas protegidas (JWT + ROLES.SELLER) y servicios para crear solicitudes, incluida validación de nombres, detección de conflictos y persistencia.
Gestión de Categorías (Admin)
src/modules/admin/categories/admin-category.controller.js, src/modules/admin/categories/admin-category.routes.js, src/modules/admin/categories/admin-category.service.js
Se añaden endpoints POST /api/admin/categories (crear categoría) y PATCH /api/admin/categories/:id (procesar solicitud); servicios incluyen normalización de nombres, validación de decisiones (aprobar/rechazar) y reasignación de productos en caso de rechazo.
Pruebas Unitarias
tests/unit/admin/admin-category.test.js, tests/unit/commerce/category-request.test.js
Suite completa de pruebas que cubre autenticación, autorización, validación, conflictos, flujos de aprobación/rechazo y comportamiento transaccional con mocks de Prisma.
Pruebas E2E
tests/e2e/product/filter-product.test.js
Se actualiza comparación de ordenamiento de precios para usar original_price en lugar de price.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutos

Possibly related PRs

Suggested reviewers

  • Benjakr04
  • SebaKisser

Poem

🐰 Salto de alegría, nuevas solicitudes han llegado,
Categorías pendientes, admin y vendedor enlazado,
Validaciones, transacciones y schemas bien definidos,
Pruebas exhaustivas, caminos todos cubiertos,
¡Qué hermosa arquitectura de comercio se ha tejido!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning El título describe solo parcialmente los cambios. Se enfoca en crear categoría como admin, pero la PR incluye además endpoints para procesar solicitudes de categorías de comercios, esquemas OpenAPI, pruebas unitarias y cambios en rutas. Expandir el título para reflejar el alcance completo: 'Agregar endpoints para crear y procesar solicitudes de categorías' o similar que abarque ambos flujos de admin y comercio.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch OM-475

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

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 llama Categorías. Para consistencia y para evitar problemas de orden/alfabetización en Swagger UI, considerá renombrar a Categories. Los tags: [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: Agregar enum a decision en la respuesta.

El request schema AdminCategoryRequestDecision ya restringe decision a ["approve","reject"]; en la respuesta queda como string libre. Replicar el enum hace 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 con createAdminProductCategory.

Este controlador responde { message, data } mientras que src/modules/admin/categories/admin-category.controller.js (líneas 13-20) devuelve el result plano 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.

validateCategoryName aquí y normalizeAdminCategoryName en src/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. en src/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.updateMany devolviendo { 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 con count: 0 que assertee igualmente la desactivación (visible: false, status: false) y el decision: "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

📥 Commits

Reviewing files that changed from the base of the PR and between 58fcd3b and 29564bc.

📒 Files selected for processing (14)
  • src/app.js
  • src/config/swagger.config.js
  • src/docs/schemas/admin/admin-category.schema.js
  • src/docs/schemas/commerce/category-request.schema.js
  • src/docs/schemas/index.js
  • src/modules/admin/categories/admin-category.controller.js
  • src/modules/admin/categories/admin-category.routes.js
  • src/modules/admin/categories/admin-category.service.js
  • src/modules/commerce/category-requests/category-request.controller.js
  • src/modules/commerce/category-requests/category-request.routes.js
  • src/modules/commerce/category-requests/category-request.service.js
  • tests/e2e/product/filter-product.test.js
  • tests/unit/admin/admin-category.test.js
  • tests/unit/commerce/category-request.test.js

Comment on lines +172 to +208
/**
* @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);

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

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.

Comment on lines +23 to +54
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
}
});

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 | 🟡 Minor

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.

Comment on lines +37 to +39
if (existingCategory) {
throw new ValidationError("Ya existe una categoría con ese nombre");
}

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 | 🟡 Minor

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.

Suggested change
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.

Comment on lines +365 to +441
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
};

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

Detección de solicitud pendiente frágil y reasignación innecesaria en el reject.

Dos puntos relacionados:

  1. isPendingRequest = status === true && visible === false (línea 384) confunde solicitudes pendientes con categorías normales ocultadas vía PUT /:id. Esto permitiría "aprobar"/"rechazar" una categoría existente, y en particular el rechazo le reasignaría todos sus productos a id=1 de forma silenciosa. Idealmente agreguen un discriminador explícito (campo is_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.

  2. El products.updateMany dentro del reject (líneas 409-412) asume que una solicitud pendiente puede tener productos, pero por diseño no debería tenerlos (se crea con visible=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 el updateMany sigue 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.

Comment on lines +3 to +19
/**
* 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);

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

🧩 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-requests

Repository: 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.js

Repository: 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 ts

Repository: 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.js

Repository: 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.user al servicio, guardar id_user/id_store en 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.

Comment on lines +28 to +78
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
}
});

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

🧩 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' -tprisma

Repository: 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.

@SebaKisser
SebaKisser merged commit 0bbfed6 into dev Apr 21, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants