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.
### 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 Carlos2026-10-02 20:06:12 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Qué es la deuda
La UI permite borrar productos, clientes, proveedores, inventarios, categorías y usuarios (
useRemove()en las páginas deadmin/). 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 como500genérico.Product.statusexiste en el contrato con defaultactive, pero no se usa.Ninguna relación del contrato define
onDelete, y en Prisma 8 el valor por defecto esRestrict. Al cruzarlo con los botones "Eliminar" de la UI:product_category)product_history)Crear con IDs inexistentes también da
500(por ejemplo,POST /inventory-movementscon unproductIdque no existe).inventory-movement.service.ts:64,79usa.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
Error 500sin saber por qué no se pudo borrar o guardar.Arreglo propuesto
409 Conflict/400 Bad Requestcon mensaje en españolProduct.status(active/inactive) para desactivar productos en vez de borrarlos; ocultar los inactivos en el POSonDeletepor relación en el contrato:Category→product_categoryconCascade(borrar la categoría solo la desasigna);Supplier→productconSetNull; el resto se queda enRestrictcon baja lógica, porque son registros contables o de auditoría404/400en vez de500.catch(() => null)por una captura que solo trateNotFoundExceptioncomo "no existe"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