Skip to content

OM-467(reportes): implementar reporte de producto para clientes y resolución para admin - #121

Merged
DavidVillarM merged 4 commits into
devfrom
OM-467-BE-Reportar-producto-Cliente
Apr 22, 2026
Merged

DavidVillarM merged 4 commits into
devfrom
OM-467-BE-Reportar-producto-Cliente

Conversation

@josediaz33

@josediaz33 josediaz33 commented Apr 21, 2026 •

Copy link
Copy Markdown
Collaborator

Contexto:
El módulo de reportes de productos ya existía con funcionalidades para Admin y Seller
(listar, filtrar, actualizar estado). Este ticket extiende ese módulo con las
capacidades del lado del cliente y del administrador para cerrar el ciclo completo.

Cambios en product-report.service.js:

  • Se agrega PRODUCT_REPORT_REASONS: catálogo exportado con { value, label } en español.
    Es la fuente única de verdad para los motivos válidos: usado en la validación de
    createProductReportService y expuesto vía el endpoint /reasons para el frontend.
  • getProductReportReasonsService: devuelve el catálogo sin lógica adicional ni DB.
  • createProductReportService: cliente crea un reporte de producto.
    Valida productId (entero positivo), reason contra el catálogo, verifica que el
    producto exista y esté activo (status=true), y previene duplicados lanzando
    ConflictError si ya existe un reporte PENDING del mismo usuario para el mismo producto.
  • checkProductReportService: cliente consulta si ya tiene un reporte PENDING para
    un producto. Retorna { hasReport: boolean, reportId: number | null }.
  • resolveProductReportAdminService: admin cierra un reporte como RESOLVED o REJECTED.
    Requiere notes obligatorias y no permite modificar reportes ya cerrados.

Cambios en product-report.controller.js:

  • getProductReportReasons: controlador síncrono (sin try/catch ni DB) para el catálogo.
  • createProductReport: 201 Created al crear el reporte.
  • checkProductReport: lee productId desde query params.
  • resolveProductReport: lee reportId desde params y { status, notes } desde body.

Cambios en product-report.routes.js:

  • GET /api/reports/products/reasons → sin auth, catálogo para el frontend
  • POST /api/reports/products → requireRole(CUSTOMER)
  • GET /api/reports/products/check?productId= → requireRole(CUSTOMER)
  • PATCH /api/reports/products/:reportId/resolve → requireRole(ADMIN)

Tests (product-report.test.js):

  • Se actualiza el mock de Prisma agregando products.findFirst y productReports.create.
  • getProductReportReasonsService: 4 tests.
  • createProductReportService: 9 tests.
  • checkProductReportService: 3 tests.
  • resolveProductReportAdminService: 12 tests.
    Total: 67 tests pasando.

Summary by CodeRabbit

Notas de Lanzamiento

  • New Features
    • Se agregó gestión de aprobación de productos en el panel de administración, permitiendo buscar, filtrar y cambiar el estado de productos.
    • Se expandió el sistema de reportes de productos con nuevas capacidades para resolver reportes y visualizar razones de reportes disponibles.

José Díaz added 3 commits April 21, 2026 14:36
…stración

Contexto:
- Primera funcionalidad de moderación de contenido del panel admin
- Los productos tienen approval_status (PENDING/ACTIVE/REJECTED) y visible
- Al aprobar: producto visible en catálogo público
- Al rechazar: producto oculto + notificación automática al vendedor
- Sigue el mismo patrón de aprobación/rechazo que ya existía en admin/stores

Endpoints:
- GET  /api/admin/products — lista productos con filtros y paginación
- PATCH /api/admin/products/:id/status — aprueba o rechaza un producto

Filtros del GET:
- search: busca por nombre de producto o nombre de tienda (case insensitive)
- approvalStatus: PENDING | ACTIVE | REJECTED
- page + limit: paginación vía parsePagination middleware

Lógica del PATCH:
- ACTIVE: approval_status=ACTIVE, visible=true, rejection_reason=null
- REJECTED: approval_status=REJECTED, visible=false, rejection_reason=reason requerido
- Validación de estado idempotente (evita re-aprobar o re-rechazar)
- Notificación al vendedor creada en la misma transacción al rechazar
…olución para admin

Contexto:
  El módulo de reportes de productos ya existía con funcionalidades para Admin y Seller
  (listar, filtrar, actualizar estado). Este ticket extiende ese módulo con las
  capacidades del lado del cliente y del administrador para cerrar el ciclo completo.

Cambios en product-report.service.js:
  - Se agrega PRODUCT_REPORT_REASONS: catálogo exportado con { value, label } en español.
    Es la fuente única de verdad para los motivos válidos: usado en la validación de
    createProductReportService y expuesto vía el endpoint /reasons para el frontend.
  - getProductReportReasonsService: devuelve el catálogo sin lógica adicional ni DB.
  - createProductReportService: cliente crea un reporte de producto.
    Valida productId (entero positivo), reason contra el catálogo, verifica que el
    producto exista y esté activo (status=true), y previene duplicados lanzando
    ConflictError si ya existe un reporte PENDING del mismo usuario para el mismo producto.
  - checkProductReportService: cliente consulta si ya tiene un reporte PENDING para
    un producto. Retorna { hasReport: boolean, reportId: number | null }.
  - resolveProductReportAdminService: admin cierra un reporte como RESOLVED o REJECTED.
    Requiere notes obligatorias y no permite modificar reportes ya cerrados.

Cambios en product-report.controller.js:
  - getProductReportReasons: controlador síncrono (sin try/catch ni DB) para el catálogo.
  - createProductReport: 201 Created al crear el reporte.
  - checkProductReport: lee productId desde query params.
  - resolveProductReport: lee reportId desde params y { status, notes } desde body.

Cambios en product-report.routes.js:
  - GET  /api/reports/products/reasons              → sin auth, catálogo para el frontend
  - POST /api/reports/products                      → requireRole(CUSTOMER)
  - GET  /api/reports/products/check?productId=     → requireRole(CUSTOMER)
  - PATCH /api/reports/products/:reportId/resolve   → requireRole(ADMIN)

Tests (product-report.test.js):
  - Se actualiza el mock de Prisma agregando products.findFirst y productReports.create.
  - getProductReportReasonsService: 4 tests.
  - createProductReportService: 9 tests.
  - checkProductReportService: 3 tests.
  - resolveProductReportAdminService: 12 tests.
  Total: 67 tests pasando.
@coderabbitai

coderabbitai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@josediaz33 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 12 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 16 minutes and 12 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: f896ac84-63e8-4b52-8fbb-f8e85d46f6d5

📥 Commits

Reviewing files that changed from the base of the PR and between a2aeff6 and bb808ce.

📒 Files selected for processing (3)
  • src/modules/admin/products/admin-products.controller.js
  • src/modules/admin/products/admin-products.service.js
  • tests/unit/admin/admin-products.test.js
📝 Walkthrough

Descripción General

Se agrega un módulo completo de gestión de productos para administradores con aprobación de estado, junto con expansión de funcionalidades de reportes de productos incluyendo creación, validación y resolución de reportes con notificaciones condicionales.

Cambios

Cohort / Archivo(s) Resumen
Integración de rutas en la aplicación
src/app.js, src/modules/admin/index.js
Se exponen nuevas rutas de productos administrador mediante importación de adminProductsRoutes y montaje en /api/admin/products.
Controlador de productos administrador
src/modules/admin/products/admin-products.controller.js
Se agregan dos controladores: getProducts (consulta con búsqueda y filtro de estado) y updateProductStatus (actualiza estado de aprobación con autenticación requerida).
Rutas de productos administrador
src/modules/admin/products/admin-products.routes.js
Se define router protegido con endpoints GET / (con paginación) y PATCH /:id/status (requiere estado y razón opcional).
Servicio de productos administrador
src/modules/admin/products/admin-products.service.js
Implementa getProductsAdminService y updateProductApprovalStatusService con validación de estado, mapeadores de respuesta, filtrado por búsqueda/estado y creación condicional de notificaciones al rechazar.
Controlador de reportes de productos extendido
src/modules/global/reports/product/product-report.controller.js
Se agregan cuatro controladores: getProductReportReasons, createProductReport, checkProductReport y resolveProductReport con validación de autenticación por rol.
Rutas de reportes de productos extendidas
src/modules/global/reports/product/product-report.routes.js
Se añaden nuevas rutas públicas (GET /products/reasons) y protegidas por rol (POST /products, GET /products/check para clientes; PATCH /products/:reportId/resolve para administradores).
Servicio de reportes de productos extendido
src/modules/global/reports/product/product-report.service.js
Se exportan PRODUCT_REPORT_REASONS, y se agregan servicios: getProductReportReasonsService, createProductReportService, checkProductReportService y resolveProductReportAdminService con validación exhaustiva y manejo de conflictos.
Pruebas unitarias
tests/unit/admin/admin-products.test.js, tests/unit/report/product-report.test.js
Se agregan suites de pruebas Vitest para validación de entrada, mapeo de datos, filtrado, paginación, manejo de notificaciones y errores en ambos módulos.

Diagrama de Secuencia

sequenceDiagram
    actor Admin as Administrador
    participant Route as Ruta admin/products
    participant Controller as Controlador
    participant Service as Servicio
    participant DB as Base de Datos
    participant Notif as Servicio Notificaciones
    
    Admin->>Route: PATCH /api/admin/products/:id/status<br/>{status, reason}
    Route->>Controller: updateProductStatus(req, res, next)
    
    rect rgba(100, 150, 255, 0.5)
    Note over Controller: Validación de autenticación
    Controller->>Service: updateProductApprovalStatusService(adminId, productId, {status, reason})
    end
    
    rect rgba(100, 200, 100, 0.5)
    Note over Service: Validación de entrada
    Service->>Service: Validar status (ACTIVE/REJECTED)
    Service->>Service: Validar reason si REJECTED
    end
    
    rect rgba(200, 200, 100, 0.5)
    Note over Service: Operaciones en BD
    Service->>DB: findFirst(producto activo)
    DB-->>Service: producto
    Service->>Service: Verificar estado actual != nuevo estado
    end
    
    alt Status = REJECTED
        rect rgba(255, 150, 100, 0.5)
        Note over Service: Rechazo con notificación
        Service->>DB: update(rejection_reason, visible=false)
        Service->>Notif: create(notificación al vendedor)
        DB-->>Service: ok
        Notif-->>Service: ok
        end
    else Status = ACTIVE
        rect rgba(150, 255, 100, 0.5)
        Note over Service: Aprobación sin notificación
        Service->>DB: update(clear rejection_reason, visible=true)
        DB-->>Service: ok
        end
    end
    
    Service-->>Controller: producto actualizado
    Controller-->>Admin: 200 JSON{producto}
Loading

Esfuerzo estimado de revisión del código

🎯 4 (Complejo) | ⏱️ ~45 minutos

PRs posiblemente relacionados

Revisores sugeridos

  • SebaKisser
  • leoAchu16
  • CrisNAC

Poema

🐰 Con patas veloces y orejas alertas,
nuevas rutas de productos abro las puertas,
reportes validados, estados sinceros,
aprobaciones y rechazos muy certeros,
¡la administración salta más ligero! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed El título describe con precisión las funcionalidades principales: implementación del reporte de productos para clientes y resolución para administradores, lo que se refleja en los cambios del pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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-467-BE-Reportar-producto-Cliente

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: 2

🧹 Nitpick comments (8)
src/modules/admin/products/admin-products.service.js (1)

139-162: Reusar reason normalizado.

reason.toString().trim() se calcula en Line 142 y Line 159. Computarlo una sola vez mejora legibilidad y evita divergencias futuras.

♻️ Refactor propuesto
+  const trimmedReason = isApproving ? null : reason.toString().trim();
+
   const updateData = {
     approval_status: normalizedStatus,
     visible: isApproving,
-    rejection_reason: isApproving ? null : reason.toString().trim(),
+    rejection_reason: trimmedReason,
   };
@@
-          prisma.notifications.create({
-            data: {
-              fk_user: product.store.fk_user,
-              title: "Producto rechazado",
-              message: `Tu producto "${product.name}" ha sido rechazado. Motivo: ${reason.toString().trim()}`,
-            },
-          }),
+          prisma.notifications.create({
+            data: {
+              fk_user: product.store.fk_user,
+              title: "Producto rechazado",
+              message: `Tu producto "${product.name}" ha sido rechazado. Motivo: ${trimmedReason}`,
+            },
+          }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/admin/products/admin-products.service.js` around lines 139 - 162,
Compute a single normalizedReason once (e.g., const normalizedReason =
reason.toString().trim()) and use that variable in updateData.rejection_reason
and in the notifications.create message instead of calling
reason.toString().trim() twice; keep the existing logic that sets
rejection_reason to null when isApproving is true and use normalizedReason only
when creating the rejection notification and the message string.
tests/unit/admin/admin-products.test.js (1)

260-284: Cobertura sugerida: validar formato de productId no numérico.

Hay test para productId negativo, pero no para strings no numéricos (p. ej., "abc") ni 0. Dado que productId viene de req.params.id (siempre string), vale la pena una aserción explícita para esos casos, especialmente Number("abc") → NaN.

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

In `@tests/unit/admin/admin-products.test.js` around lines 260 - 284, Add tests
covering non-numeric and zero productId inputs for
updateProductApprovalStatusService: add one test that calls
updateProductApprovalStatusService(1, "abc", { status: "ACTIVE" }) (or with
params coming from req like "abc") and expects a ValidationError, and another
that calls updateProductApprovalStatusService(1, 0, { status: "ACTIVE" })
(and/or "0") and expects a ValidationError; ensure tests use the same mocking
pattern (e.g., prisma.products.findFirst mocked where needed) and mirror
existing negative/REJECTED test structure so these invalid productId cases are
asserted consistently.
src/modules/admin/products/admin-products.controller.js (1)

16-30: Chequeo de req.user redundante e inconsistente con el resto del módulo.

La ruta ya aplica authenticate + requireRole(ROLES.ADMIN) (ver admin-products.routes.js Line 14), por lo que req.user.id_user siempre existirá al llegar aquí; si no, cualquier falla de auth se gestiona vía next(ForbiddenError/UnauthorizedError) y el errorHandler global. Además, la respuesta ad-hoc { success: false, message: ... } rompe la convención de respuestas del resto del controlador (que devuelve el payload del servicio directamente). Sugerido eliminar el bloque.

♻️ Cambio propuesto
 export const updateProductStatus = async (req, res, next) => {
   try {
-    if (!req.user?.id_user) {
-      return res.status(401).json({ success: false, message: "Usuario autenticado requerido" });
-    }
     const result = await updateProductApprovalStatusService(
       req.user.id_user,
       req.params.id,
       req.body
     );
     return res.status(200).json(result);
   } catch (error) {
     next(error);
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/admin/products/admin-products.controller.js` around lines 16 -
30, The updateProductStatus controller contains a redundant req.user check and
an ad-hoc 401 JSON response that breaks the module's response convention; remove
the entire if (!req.user?.id_user) branch and its custom res.status(401)...
return so the function always calls
updateProductApprovalStatusService(req.user.id_user, req.params.id, req.body)
and returns res.status(200).json(result) (keeping the existing try/catch and
next(error)), thereby relying on the authenticate/requireRole middleware to
handle auth errors and preserving consistent service-returned payloads.
src/modules/admin/products/admin-products.routes.js (1)

11-14: Rutas correctamente protegidas con authenticate + requireRole(ROLES.ADMIN).

Cadena de middlewares consistente con el resto de módulos admin; parsePagination solo se aplica al GET / como corresponde. Considerar, como mejora opcional, validar el body de PATCH /:id/status con un validador (zod/joi) a nivel de middleware en lugar de delegar toda la validación al servicio; hoy el servicio ya cubre los casos, por lo que es un nice-to-have.

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

In `@src/modules/admin/products/admin-products.routes.js` around lines 11 - 14,
Routes are correctly protected, but add request-body validation for PATCH
/:id/status instead of relying solely on the service; create a middleware (e.g.,
validateProductStatusBody) using your chosen schema library (zod or joi) that
enforces { status: "ACTIVE" | "REJECTED", reason?: string } and returns 400 on
validation failure, then insert that middleware into the route chain before
updateProductStatus in admin-products.routes.js (router.patch("/:id/status",
authenticate, requireRole(ROLES.ADMIN), validateProductStatusBody,
updateProductStatus)) so updateProductStatus receives already-validated input.
src/modules/global/reports/product/product-report.service.js (2)

237-270: Condición de carrera en la verificación de duplicados.

El patrón findFirst + create es TOCTOU: dos requests concurrentes del mismo cliente sobre el mismo producto pueden pasar el chequeo antes de que cualquiera inserte, terminando con dos reportes PENDING duplicados y evadiendo el ConflictError. Para reportes desde UI es poco probable pero no imposible (doble clic, reintentos de red).

Como respaldo a nivel de datos, te recomiendo agregar un índice parcial único en Postgres sobre (fk_product, fk_reporter) cuando report_status = 'PENDING' y status = true, y capturar P2002 en el create para relanzarlo como ConflictError. Esto garantiza la unicidad incluso ante concurrencia.

Ejemplo de migración (Prisma no soporta índices parciales de forma declarativa, usar Unsupported/migrationSQL):

CREATE UNIQUE INDEX IF NOT EXISTS uq_product_reports_pending_reporter
  ON "ProductReports" (fk_product, fk_reporter)
  WHERE report_status = 'PENDING' AND status = true;

Y en el servicio:

   try {
-    const report = await prisma.productReports.create({ ... });
+    const report = await prisma.productReports.create({ ... });
+    return report;
+  } catch (e) {
+    if (e?.code === "P2002") throw new ConflictError("Ya tenés un reporte pendiente para este producto");
+    throw e;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/global/reports/product/product-report.service.js` around lines
237 - 270, The current TOCTOU check using prisma.productReports.findFirst
followed by prisma.productReports.create can produce duplicate PENDING reports
under concurrent requests; add a Postgres partial unique index on (fk_product,
fk_reporter) WHERE report_status = 'PENDING' AND status = true (e.g. CREATE
UNIQUE INDEX ...), then wrap the prisma.productReports.create call in a
try/catch that detects Prisma's P2002 unique-constraint error and rethrows a
ConflictError("Ya tenés un reporte pendiente para este producto"); keep the
existing findFirst check for fast-fail UX but rely on the DB constraint plus
P2002 handling around productReports.create to guarantee correctness.

10-20: Catálogo mutable compartido por referencia.

getProductReportReasonsService devuelve la misma referencia que PRODUCT_REPORT_REASONS, que también es la fuente única para validar reason en createProductReportService. Si algún consumidor (handler, middleware, etc.) llega a mutar el array o alguno de sus objetos, queda corrompida la validación y todas las respuestas futuras de GET /reasons. Como protección barata, considerá congelar el catálogo.

♻️ Sugerencia
-export const PRODUCT_REPORT_REASONS = [
-  { value: "DEFECTIVE",        label: "Producto en mal estado" },
-  { value: "WRONG_ITEM",       label: "Producto incorrecto o no es lo que pedí" },
-  { value: "MISSING_ITEM",     label: "Producto incompleto o faltante" },
-  { value: "LATE_DELIVERY",    label: "Entrega tardía o no recibida" },
-  { value: "CUSTOMER_SERVICE", label: "Mala atención del vendedor" },
-  { value: "OTHER",            label: "Otro" },
-];
+export const PRODUCT_REPORT_REASONS = Object.freeze([
+  Object.freeze({ value: "DEFECTIVE",        label: "Producto en mal estado" }),
+  Object.freeze({ value: "WRONG_ITEM",       label: "Producto incorrecto o no es lo que pedí" }),
+  Object.freeze({ value: "MISSING_ITEM",     label: "Producto incompleto o faltante" }),
+  Object.freeze({ value: "LATE_DELIVERY",    label: "Entrega tardía o no recibida" }),
+  Object.freeze({ value: "CUSTOMER_SERVICE", label: "Mala atención del vendedor" }),
+  Object.freeze({ value: "OTHER",            label: "Otro" }),
+]);

Nota: el test retorna exactamente el catálogo exportado PRODUCT_REPORT_REASONS (línea 551) seguirá pasando porque sigue siendo la misma referencia.

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

In `@src/modules/global/reports/product/product-report.service.js` around lines 10
- 20, PRODUCT_REPORT_REASONS is mutable and returned by
getProductReportReasonsService, so freeze the shared catalog to prevent
accidental mutation: deep-freeze each reason object then freeze the
PRODUCT_REPORT_REASONS array where it’s defined; keep
getProductReportReasonsService returning the same reference (it will now be
immutable) and leave createProductReportService validation using
PRODUCT_REPORT_REASONS unchanged. Ensure you only modify the declaration of
PRODUCT_REPORT_REASONS (freeze objects then the array) and do not change the API
of getProductReportReasonsService or createProductReportService.
src/modules/global/reports/product/product-report.controller.js (1)

81-122: LGTM — handlers consistentes con el resto del archivo.

El patrón (401 defensivo + try/catch + next(error)) es coherente con los handlers existentes. Un detalle menor: en resolveProductReport accedés a req.params.reportId inline, mientras que updateProductReport (línea 46) desestructura const { reportId } = req.params;. Unificar ayuda a la lectura, pero no es bloqueante.

♻️ Nit opcional
 export const resolveProductReport = async (req, res, next) => {
   try {
     if (!req.user?.id_user) {
       return res.status(401).json({ success: false, message: "Usuario autenticado requerido" });
     }
+    const { reportId } = req.params;
     const { status, notes } = req.body;
-    const result = await resolveProductReportAdminService(req.user.id_user, req.params.reportId, { status, notes });
+    const result = await resolveProductReportAdminService(req.user.id_user, reportId, { status, notes });
     return res.status(200).json(result);
   } catch (error) {
     next(error);
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/global/reports/product/product-report.controller.js` around lines
81 - 122, Unify the parameter access style by destructuring reportId from
req.params in resolveProductReport (currently using req.params.reportId inline)
to match updateProductReport; update the resolveProductReport function to
extract const { reportId } = req.params before calling
resolveProductReportAdminService so the handler style is consistent with other
handlers like updateProductReport.
src/modules/global/reports/product/product-report.routes.js (1)

197-207: Faltan bloques Swagger para los nuevos endpoints.

Los endpoints existentes (GET /products, GET /products/filtered, PUT /products/:reportId) tienen bloques @swagger completos. Los cuatro nuevos (GET /products/reasons, POST /products, GET /products/check, PATCH /products/:reportId/resolve) no los tienen, lo cual rompe la consistencia de la documentación OpenAPI del módulo y dificulta el consumo desde el frontend.

Si usás swagger-jsdoc, agregá los bloques arriba de cada router.<method> replicando el estilo del archivo. No bloquea el merge, pero sería bueno cerrarlo antes de liberar a consumidores.

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

In `@src/modules/global/reports/product/product-report.routes.js` around lines 197
- 207, Add Swagger JSDoc blocks above the four routes for
getProductReportReasons, createProductReport, checkProductReport, and
resolveProductReport to match the existing style in this file: create an
`@swagger` block for GET /products/reasons (public, tag "Reports", summary, 200
response schema), for POST /products (authenticated CUSTOMER, include
requestBody schema for report payload, 201/400/401 responses and security bearer
token), for GET /products/check (authenticated CUSTOMER with productId query
param and 200/401 responses), and for PATCH /products/:reportId/resolve
(authenticated ADMIN with path param reportId and requestBody for resolution
payload plus 200/400/401 responses and security). Use the same field names and
response examples as the other blocks and the swagger-jsdoc conventions already
used in this file so the OpenAPI docs remain consistent.
🤖 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/products/admin-products.service.js`:
- Around line 102-166: In updateProductApprovalStatusService: when transitioning
REJECTED→ACTIVE do not force visible: true and send a notification to the
seller; specifically, change the updateData construction to preserve the current
product.visible on approval (e.g., visible: isApproving ? product.visible :
product.visible) or simply omit changing visible when isApproving, and add a
prisma.notifications.create entry in the transaction for the approval path
(similar to the rejection notification) with a title like "Producto aprobado"
and a message informing the seller their product was reapproved; update the
transaction spread that currently only adds notifications on rejection to
include the approval notification when isApproving is true and remove the
unconditional forcing of visible to true in updateData.

In `@src/modules/global/reports/product/product-report.service.js`:
- Around line 383-415: The update is vulnerable to TOCTOU: after fetching the
report with prisma.productReports.findFirst you must include the current
report_status in the prisma.productReports.update where clause to ensure you
only update if the status is still the same (use the fetched
report.report_status as the expected current status), and convert the P2025
catch branch to throw a ConflictError indicating the report was modified by
another process rather than NotFound; keep the same update data
(normalizedStatus, trimmedNotes, resolvedAdminId, resolved_at) and selected
fields but change the where to include id_product_report: resolvedReportId,
status: true, report_status: report.report_status.

---

Nitpick comments:
In `@src/modules/admin/products/admin-products.controller.js`:
- Around line 16-30: The updateProductStatus controller contains a redundant
req.user check and an ad-hoc 401 JSON response that breaks the module's response
convention; remove the entire if (!req.user?.id_user) branch and its custom
res.status(401)... return so the function always calls
updateProductApprovalStatusService(req.user.id_user, req.params.id, req.body)
and returns res.status(200).json(result) (keeping the existing try/catch and
next(error)), thereby relying on the authenticate/requireRole middleware to
handle auth errors and preserving consistent service-returned payloads.

In `@src/modules/admin/products/admin-products.routes.js`:
- Around line 11-14: Routes are correctly protected, but add request-body
validation for PATCH /:id/status instead of relying solely on the service;
create a middleware (e.g., validateProductStatusBody) using your chosen schema
library (zod or joi) that enforces { status: "ACTIVE" | "REJECTED", reason?:
string } and returns 400 on validation failure, then insert that middleware into
the route chain before updateProductStatus in admin-products.routes.js
(router.patch("/:id/status", authenticate, requireRole(ROLES.ADMIN),
validateProductStatusBody, updateProductStatus)) so updateProductStatus receives
already-validated input.

In `@src/modules/admin/products/admin-products.service.js`:
- Around line 139-162: Compute a single normalizedReason once (e.g., const
normalizedReason = reason.toString().trim()) and use that variable in
updateData.rejection_reason and in the notifications.create message instead of
calling reason.toString().trim() twice; keep the existing logic that sets
rejection_reason to null when isApproving is true and use normalizedReason only
when creating the rejection notification and the message string.

In `@src/modules/global/reports/product/product-report.controller.js`:
- Around line 81-122: Unify the parameter access style by destructuring reportId
from req.params in resolveProductReport (currently using req.params.reportId
inline) to match updateProductReport; update the resolveProductReport function
to extract const { reportId } = req.params before calling
resolveProductReportAdminService so the handler style is consistent with other
handlers like updateProductReport.

In `@src/modules/global/reports/product/product-report.routes.js`:
- Around line 197-207: Add Swagger JSDoc blocks above the four routes for
getProductReportReasons, createProductReport, checkProductReport, and
resolveProductReport to match the existing style in this file: create an
`@swagger` block for GET /products/reasons (public, tag "Reports", summary, 200
response schema), for POST /products (authenticated CUSTOMER, include
requestBody schema for report payload, 201/400/401 responses and security bearer
token), for GET /products/check (authenticated CUSTOMER with productId query
param and 200/401 responses), and for PATCH /products/:reportId/resolve
(authenticated ADMIN with path param reportId and requestBody for resolution
payload plus 200/400/401 responses and security). Use the same field names and
response examples as the other blocks and the swagger-jsdoc conventions already
used in this file so the OpenAPI docs remain consistent.

In `@src/modules/global/reports/product/product-report.service.js`:
- Around line 237-270: The current TOCTOU check using
prisma.productReports.findFirst followed by prisma.productReports.create can
produce duplicate PENDING reports under concurrent requests; add a Postgres
partial unique index on (fk_product, fk_reporter) WHERE report_status =
'PENDING' AND status = true (e.g. CREATE UNIQUE INDEX ...), then wrap the
prisma.productReports.create call in a try/catch that detects Prisma's P2002
unique-constraint error and rethrows a ConflictError("Ya tenés un reporte
pendiente para este producto"); keep the existing findFirst check for fast-fail
UX but rely on the DB constraint plus P2002 handling around
productReports.create to guarantee correctness.
- Around line 10-20: PRODUCT_REPORT_REASONS is mutable and returned by
getProductReportReasonsService, so freeze the shared catalog to prevent
accidental mutation: deep-freeze each reason object then freeze the
PRODUCT_REPORT_REASONS array where it’s defined; keep
getProductReportReasonsService returning the same reference (it will now be
immutable) and leave createProductReportService validation using
PRODUCT_REPORT_REASONS unchanged. Ensure you only modify the declaration of
PRODUCT_REPORT_REASONS (freeze objects then the array) and do not change the API
of getProductReportReasonsService or createProductReportService.

In `@tests/unit/admin/admin-products.test.js`:
- Around line 260-284: Add tests covering non-numeric and zero productId inputs
for updateProductApprovalStatusService: add one test that calls
updateProductApprovalStatusService(1, "abc", { status: "ACTIVE" }) (or with
params coming from req like "abc") and expects a ValidationError, and another
that calls updateProductApprovalStatusService(1, 0, { status: "ACTIVE" })
(and/or "0") and expects a ValidationError; ensure tests use the same mocking
pattern (e.g., prisma.products.findFirst mocked where needed) and mirror
existing negative/REJECTED test structure so these invalid productId cases are
asserted consistently.
🪄 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: 8f5a45a3-fc8f-4fff-b58d-8f21f2fc7a11

📥 Commits

Reviewing files that changed from the base of the PR and between f2d1a7a and a2aeff6.

📒 Files selected for processing (10)
  • src/app.js
  • src/modules/admin/index.js
  • src/modules/admin/products/admin-products.controller.js
  • src/modules/admin/products/admin-products.routes.js
  • src/modules/admin/products/admin-products.service.js
  • src/modules/global/reports/product/product-report.controller.js
  • src/modules/global/reports/product/product-report.routes.js
  • src/modules/global/reports/product/product-report.service.js
  • tests/unit/admin/admin-products.test.js
  • tests/unit/report/product-report.test.js

Comment on lines +102 to +166
export const updateProductApprovalStatusService = async (adminUserId, productId, { status, reason }) => {
const resolvedProductId = Number(productId);
if (!Number.isInteger(resolvedProductId) || resolvedProductId <= 0) {
throw new ValidationError("productId debe ser un entero positivo");
}

const normalizedStatus = status?.toString().trim().toUpperCase();
if (!["ACTIVE", "REJECTED"].includes(normalizedStatus)) {
throw new ValidationError("status debe ser ACTIVE o REJECTED");
}

if (normalizedStatus === "REJECTED" && !reason?.toString().trim()) {
throw new ValidationError("reason es requerido al rechazar un producto");
}

const product = await prisma.products.findFirst({
where: { id_product: resolvedProductId, status: true },
select: {
id_product: true,
name: true,
approval_status: true,
store: { select: { fk_user: true, name: true } },
},
});

if (!product) {
throw new NotFoundError("Producto no encontrado");
}

if (product.approval_status === normalizedStatus) {
throw new ValidationError(
`El producto ya tiene el estado ${normalizedStatus}`
);
}

const isApproving = normalizedStatus === "ACTIVE";

const updateData = {
approval_status: normalizedStatus,
visible: isApproving,
rejection_reason: isApproving ? null : reason.toString().trim(),
};

const [updated] = await prisma.$transaction([
prisma.products.update({
where: { id_product: resolvedProductId },
data: updateData,
select: productSelect,
}),
// Notificar al vendedor solo al rechazar
...(isApproving
? []
: [
prisma.notifications.create({
data: {
fk_user: product.store.fk_user,
title: "Producto rechazado",
message: `Tu producto "${product.name}" ha sido rechazado. Motivo: ${reason.toString().trim()}`,
},
}),
]),
]);

return mapProductResponse(updated);
}; No newline at end of file

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

Transición REJECTED → ACTIVE: sin notificación y visible forzado.

Al reaprobar un producto previamente rechazado, se limpia rejection_reason, se fuerza visible: true y no se notifica al vendedor (solo hay notificación en el rechazo). Si la reaprobación es un flujo válido, conviene (a) notificar también al vendedor en el camino de aprobación y (b) evaluar si visible debería quedar bajo control del vendedor en lugar de forzarse a true, para no sobrescribir una decisión manual de ocultar el producto.

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

In `@src/modules/admin/products/admin-products.service.js` around lines 102 - 166,
In updateProductApprovalStatusService: when transitioning REJECTED→ACTIVE do not
force visible: true and send a notification to the seller; specifically, change
the updateData construction to preserve the current product.visible on approval
(e.g., visible: isApproving ? product.visible : product.visible) or simply omit
changing visible when isApproving, and add a prisma.notifications.create entry
in the transaction for the approval path (similar to the rejection notification)
with a title like "Producto aprobado" and a message informing the seller their
product was reapproved; update the transaction spread that currently only adds
notifications on rejection to include the approval notification when isApproving
is true and remove the unconditional forcing of visible to true in updateData.

Comment on lines +383 to +415
const report = await prisma.productReports.findFirst({
where: { id_product_report: resolvedReportId, status: true },
select: { id_product_report: true, report_status: true },
});

if (!report) throw new NotFoundError("Reporte no encontrado");

if (report.report_status === "RESOLVED" || report.report_status === "REJECTED") {
throw new ValidationError("Este reporte ya fue cerrado y no puede modificarse");
}

let updated;
try {
updated = await prisma.productReports.update({
where: { id_product_report: resolvedReportId, status: true },
data: {
report_status: normalizedStatus,
notes: trimmedNotes,
resolved_by: resolvedAdminId,
resolved_at: new Date(),
},
select: {
id_product_report: true,
report_status: true,
notes: true,
resolved_at: true,
resolver: { select: { id_user: true, name: true } },
},
});
} catch (e) {
if (e?.code === "P2025") throw new NotFoundError("El reporte ya no está disponible o fue modificado por otro proceso");
throw e;
}

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

TOCTOU: el update no protege contra cierres concurrentes.

Entre el findFirst (línea 383) que valida que el reporte todavía esté abierto y el update (línea 396), otro admin puede cerrar el mismo reporte. El where del update sólo filtra por id_product_report y status: true, sin incluir report_status, así que la segunda request sobrescribe silenciosamente report_status, notes, resolved_by y resolved_at del primer cierre — violando el invariante "no permite cerrar reportes ya cerrados".

updateProductReportService (línea 185) ya usa el patrón correcto incluyendo report_status: currentStatus en el where. Aplicá lo mismo acá y traducí el P2025 como conflicto:

🔒 Fix propuesto
   const report = await prisma.productReports.findFirst({
     where: { id_product_report: resolvedReportId, status: true },
     select: { id_product_report: true, report_status: true },
   });

   if (!report) throw new NotFoundError("Reporte no encontrado");

   if (report.report_status === "RESOLVED" || report.report_status === "REJECTED") {
     throw new ValidationError("Este reporte ya fue cerrado y no puede modificarse");
   }

+  const currentStatus = report.report_status;
+
   let updated;
   try {
     updated = await prisma.productReports.update({
-      where: { id_product_report: resolvedReportId, status: true },
+      where: {
+        id_product_report: resolvedReportId,
+        status: true,
+        report_status: currentStatus,
+      },
       data: {
         report_status: normalizedStatus,
         notes: trimmedNotes,
         resolved_by: resolvedAdminId,
         resolved_at: new Date(),
       },
       select: { ... },
     });
   } catch (e) {
-    if (e?.code === "P2025") throw new NotFoundError("El reporte ya no está disponible o fue modificado por otro proceso");
+    if (e?.code === "P2025") {
+      // Puede ser que otro admin lo cerró en paralelo (lost update evitado) o que ya no existe.
+      throw new ValidationError("El reporte fue modificado por otro proceso. Recargá y volvé a intentar.");
+    }
     throw e;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/global/reports/product/product-report.service.js` around lines
383 - 415, The update is vulnerable to TOCTOU: after fetching the report with
prisma.productReports.findFirst you must include the current report_status in
the prisma.productReports.update where clause to ensure you only update if the
status is still the same (use the fetched report.report_status as the expected
current status), and convert the P2025 catch branch to throw a ConflictError
indicating the report was modified by another process rather than NotFound; keep
the same update data (normalizedStatus, trimmedNotes, resolvedAdminId,
resolved_at) and selected fields but change the where to include
id_product_report: resolvedReportId, status: true, report_status:
report.report_status.

@DavidVillarM DavidVillarM left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Apruebo porque necesito urgentemente todos estos cambios

@DavidVillarM
DavidVillarM merged commit 48488ee into dev Apr 22, 2026
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