[debt] Borrados con dependencias dan error 500; usar baja lógica y errores legibles #11

Open
opened 2026-10-02 18:37:10 +00:00 by Carlos · 0 comments
Member

Qué es la deuda

  • La UI permite borrar productos, clientes, proveedores, inventarios, categorías y usuarios (useRemove() en las páginas de admin/). Si el registro tiene ventas, movimientos o precios asociados, la llave foránea impide el borrado.

  • No hay un manejador global de errores (exception filter) en apps/api: los errores de base de datos (FK, SKU o email duplicado) llegan como 500 genérico.

  • Product.status existe en el contrato con default active, pero no se usa.

  • Ninguna relación del contrato define onDelete, y en Prisma 8 el valor por defecto es Restrict. Al cruzarlo con los botones "Eliminar" de la UI:

    Al borrar… Falla si tiene…
    Categoría algún producto asignado (product_category)
    Producto precio, stock o movimientos (casi siempre: capturar el precio ya crea product_history)
    Proveedor productos asociados
    Cliente alguna venta
    Inventario stock, movimientos o sesiones de caja
    Usuario cualquier actividad (8 relaciones)
  • Crear con IDs inexistentes también da 500 (por ejemplo, POST /inventory-movements con un productId que no existe).

  • inventory-movement.service.ts:64,79 usa .catch(() => null), que se traga cualquier error, incluida una base de datos caída, y continúa como si el registro no existiera.

Qué estamos pagando hoy

  • El administrador ve Error 500 sin saber por qué no se pudo borrar o guardar.
  • Si algún día el borrado sí pasa (por ejemplo, un producto sin ventas pero con precios), se pierde historial.

Arreglo propuesto

  • Exception filter global que traduzca errores de base de datos a 409 Conflict / 400 Bad Request con mensaje en español
  • Usar Product.status (active / inactive) para desactivar productos en vez de borrarlos; ocultar los inactivos en el POS
  • Revisar el mismo criterio para clientes, proveedores, usuarios e inventarios
  • Definir onDelete por relación en el contrato: Category → product_category con Cascade (borrar la categoría solo la desasigna); Supplier → product con SetNull; el resto se queda en Restrict con baja lógica, porque son registros contables o de auditoría
  • Validar que los IDs referenciados existan y responder 404/400 en vez de 500
  • Reemplazar .catch(() => null) por una captura que solo trate NotFoundException como "no existe"
  • Mensaje claro en la UI cuando un borrado no está permitido

Techo de esta ronda

Errores legibles y baja lógica de productos. Fuera: auditoría de quién borró o desactivó qué, y restaurar registros desactivados desde la UI.

Superficie

API (apps/api)

Módulos

API / backend, Productos / precios / categorías, Clientes, Proveedores, Usuarios, Inventario / almacenes

Esfuerzo

M — un PR, un día

Riesgo de no tocarlo

Medio

### Qué es la deuda - La UI permite borrar productos, clientes, proveedores, inventarios, categorías y usuarios (`useRemove()` en las páginas de `admin/`). Si el registro tiene ventas, movimientos o precios asociados, la llave foránea impide el borrado. - No hay un manejador global de errores (exception filter) en `apps/api`: los errores de base de datos (FK, SKU o email duplicado) llegan como `500` genérico. - `Product.status` existe en el contrato con default `active`, pero no se usa. - Ninguna relación del contrato define `onDelete`, y en Prisma 8 el valor por defecto es `Restrict`. Al cruzarlo con los botones "Eliminar" de la UI: | Al borrar… | Falla si tiene… | |---|---| | Categoría | algún producto asignado (`product_category`) | | Producto | precio, stock o movimientos (casi siempre: capturar el precio ya crea `product_history`) | | Proveedor | productos asociados | | Cliente | alguna venta | | Inventario | stock, movimientos o sesiones de caja | | Usuario | cualquier actividad (8 relaciones) | - Crear con IDs inexistentes también da `500` (por ejemplo, `POST /inventory-movements` con un `productId` que no existe). - `inventory-movement.service.ts:64,79` usa `.catch(() => null)`, que se traga cualquier error, incluida una base de datos caída, y continúa como si el registro no existiera. ### Qué estamos pagando hoy - El administrador ve `Error 500` sin saber por qué no se pudo borrar o guardar. - Si algún día el borrado sí pasa (por ejemplo, un producto sin ventas pero con precios), se pierde historial. ### Arreglo propuesto - [ ] Exception filter global que traduzca errores de base de datos a `409 Conflict` / `400 Bad Request` con mensaje en español - [ ] Usar `Product.status` (`active` / `inactive`) para desactivar productos en vez de borrarlos; ocultar los inactivos en el POS - [ ] Revisar el mismo criterio para clientes, proveedores, usuarios e inventarios - [ ] Definir `onDelete` por relación en el contrato: `Category` → `product_category` con `Cascade` (borrar la categoría solo la desasigna); `Supplier` → `product` con `SetNull`; el resto se queda en `Restrict` con baja lógica, porque son registros contables o de auditoría - [ ] Validar que los IDs referenciados existan y responder `404`/`400` en vez de `500` - [ ] Reemplazar `.catch(() => null)` por una captura que solo trate `NotFoundException` como "no existe" - [ ] Mensaje claro en la UI cuando un borrado no está permitido ### Techo de esta ronda Errores legibles y baja lógica de productos. Fuera: auditoría de quién borró o desactivó qué, y restaurar registros desactivados desde la UI. ### Superficie API (apps/api) ### Módulos API / backend, Productos / precios / categorías, Clientes, Proveedores, Usuarios, Inventario / almacenes ### Esfuerzo M — un PR, un día ### Riesgo de no tocarlo Medio
Nolberto was assigned by Carlos 2026-10-02 20:06:12 +00:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RynextTechnologies/PDV#11