Plan de M3 — Insumos de empaque
Objetivo: implementar M3 según la spec de diseño — catálogo de insumos, saldo que no puede quedar negativo, alerta de mínimo y la conversión kilo → unidad registrada de forma auditable.
Dominio: Insumos — RN-INS-01 a RN-INS-04.
Estado: ✅ Terminado (26-ago-2026) — las 6 tasks cerradas.
| Task | Estado | Commit |
|---|---|---|
| T1 — Schema, migración e invariantes | ✅ | 88b5cc0 |
| T2 — El saldo atómico | ✅ | e9b5542 |
| T3 — Entrada, ajuste, descarte y conversión | ✅ | 6232997 |
| T4 — Endpoints | ✅ | 7bd49a7 |
T5 — Pantalla en web/ |
✅ | f8cfdae |
| T6 — Bruno, documentación y cierre | ✅ | b4dd1c9 |
Suites: api/ 548 → 619, web/ 460. Bruno: 15 requests / 33 aserciones en 6-Insumos.
M3 es más chico que M2, y conviene saber por qué
Sección titulada «M3 es más chico que M2, y conviene saber por qué»Son 6 tasks y no 9. Tres cosas que M2 tuvo que construir ya están:
| Lo que no hay que hacer | Por qué |
|---|---|
| La matriz de permisos | insumos:ver e insumos:ajustar ya están declarados en api/src/modules/authz/matrix.ts, con alcances y tests |
| El sistema de diseño | <Estado>, <Vacio>, esqueletos, mapa de errores, .aq-campo, .aq-boton* y la voz de usted ya existen |
| FIFO y vencimiento | Una tapa no vence y no hay lotes de insumo. M3 no los modela |
Lo que sí hay que resolver de nuevo es la concurrencia, y por la misma razón que en M2: es el único defecto de este módulo que no se manifiesta como una pantalla rota.
El orden es por costo de revertir
Sección titulada «El orden es por costo de revertir»| Riesgo | Qué pasa si se descubre tarde | Task |
|---|---|---|
| 🔴 Concurrencia mal resuelta | Descuenta más de lo que hay. Silencioso e intermitente; se descubre el día que faltan tapas para producir | T2 |
🔴 equivalencia guardada en el insumo |
Actualizarla reescribe la historia: las compras viejas pasan a significar otra cantidad | T1 |
| 🔴 Saldo sin libro, o libro sin saldo | Un saldo que no se puede explicar no se puede arbitrar — no se sabe cuál de los dos está mal | T1 |
| 🟠 Conversión sin los tres campos | Un descuadre queda imposible de reconstruir: no se sabe si se pesó mal o faltaron bolsas | T1 |
| 🟠 Movimientos editables | Para cuando alguien dependa de editarlos, volver atrás rompe código ajeno | T1 |
| 🟢 La equivalencia real de las bolsas | Aditivo. Tapas y sellos funcionan completos sin ella | T3 |
T2 llega antes que todo lo que se apoya en el saldo. Si el modelo no aguanta concurrencia hay que saberlo con dos tablas construidas, no con seis tasks encima. Es la lección de M2, donde la versión ingenua dejó pasar 20 de 20 descuentos concurrentes con el libro en −150 y cero errores.
Grafo de dependencias
Sección titulada «Grafo de dependencias»T1 schema ── T2 saldo atómico ──┬── T3 entrada, ajuste y descarte ── T4 endpoints ── T5 pantalla │ └────────────────────────────────────────────────── T6 bruno + docs- T2 es la compuerta: nada que use el saldo arranca antes de que pase.
- T6 cierra.
Restricciones globales
Sección titulada «Restricciones globales»- Convención del proyecto: 2 espacios, comillas simples, sin punto y coma.
- Conventional commits, sin
Co-Authored-By. Nunca buildear. - Cada commit deja el proyecto en verde: tests + typecheck, verificado por
código de salida — un pipe a
taildevuelve el estado detail. - Migraciones en SQL explícito, escritas a mano.
- Toda tabla nueva necesita
REVOKEexplícito: elALTER DEFAULT PRIVILEGESde la migración0001concedeDELETEsobre todo (ADR-0006). - El invariante vive en la base; el servicio explica el error.
/bin/rmpara artefactos de drizzle-kit:rmestá aliaseado atrash.
Task 1 — Schema, migración e invariantes
Sección titulada «Task 1 — Schema, migración e invariantes»Archivos
- Modificar:
api/src/db/schema.ts,api/src/test/db.ts - Crear:
api/src/db/migrations/0004_insumos.sql - Crear:
api/src/db/__tests__/insumos-schema.test.ts
Qué se construye
insumos con codigo único en mayúsculas, nombre, unidad, minimo,
saldo y activo. movimientos_insumo con el patrón de movimientos_stock:
bigserial, cantidad distinta de cero, tipo, motivo, causa, los tres campos de
conversión, documento y quién lo registró.
Los CHECK que van en la base y no en el servicio
CHECK (minimo > 0)CHECK (saldo >= 0)CHECK (cantidad <> 0)CHECK (tipo <> 'ajuste' OR motivo IS NOT NULL)CHECK (tipo <> 'descarte' OR causa IS NOT NULL)-- Los tres campos de conversión, juntos o ninguno.CHECK ((kilos IS NULL) = (equivalencia IS NULL))El último es el que más se presta a olvidar. Sin él se puede guardar «entraron 12 kilos» sin decir a cuántas unidades se convirtieron, y ese movimiento no se puede auditar después: no hay forma de saber si el descuadre vino de la balanza o de la equivalencia.
equivalencia va en el MOVIMIENTO, no en el insumo
Es el valor que se usó ese día, y va a cambiar. En el insumo, actualizarla
reescribiría la historia — el mismo error que M2 evitó con fecha_vencimiento.
Verificación
- Cada
CHECKse prueba intentando elINSERTinválido y esperando el rechazo. - El
REVOKEse prueba intentando elDELETEcon el rol de la app. - Quitar el
CHECKde la conversión y confirmar que su test falla.
Task 2 — El saldo atómico, y la prueba de que aguanta
Sección titulada «Task 2 — El saldo atómico, y la prueba de que aguanta»Archivos
- Crear:
api/src/modules/insumos/service.ts - Crear:
api/src/modules/insumos/__tests__/concurrencia.test.ts
El mecanismo
UPDATE insumos SET saldo = saldo - :cantidadWHERE id = :id AND saldo >= :cantidadRETURNING saldoCero filas afectadas no es un error: es «no alcanza». El servicio devuelve
ok: false con lo que había y no escribe movimiento — un intento fallido no
pasó, y el libro solo cuenta lo que pasó.
El UPDATE y el INSERT del movimiento van en la misma transacción, en ese
orden. La condición saldo >= :cantidad es la que serializa.
Los cinco tests
- 20 salidas de 10 sobre un saldo de 100: exactamente 10 tienen éxito.
- La suma del libro explica el saldo, sin importar el orden.
- Nunca deja el saldo negativo, con cantidades desparejas.
- Entradas y salidas simultáneas cuadran.
- Un intento fallido no toca el saldo y no deja movimiento.
Task 3 — Entrada, ajuste, descarte y la conversión
Sección titulada «Task 3 — Entrada, ajuste, descarte y la conversión»Archivos
- Modificar:
api/src/modules/insumos/service.ts - Crear:
api/src/modules/insumos/__tests__/service.test.ts
Qué se construye
Las tres escrituras del libro. La entrada acepta unidades o kilos; con kilos, convierte y guarda los tres campos.
Verificación
- Los tres campos de conversión viajan juntos, y el saldo resultante es el esperado.
- Cambiar la equivalencia y verificar que los movimientos viejos no cambian.
- Frontera exacta del mínimo: en el mínimo ya avisa, no un paso después.
- Motivo de al menos diez caracteres, como en stock.
Task 4 — Endpoints
Sección titulada «Task 4 — Endpoints»Archivos
- Crear:
api/src/modules/insumos/routes.ts - Crear:
api/src/modules/insumos/__tests__/routes.test.ts
| Método | Ruta | Permiso |
|---|---|---|
GET |
/insumos |
insumos:ver |
GET |
/insumos/:id/movimientos |
insumos:ver |
POST |
/insumos |
insumos:ajustar (admin) |
PATCH |
/insumos/:id |
insumos:ajustar (admin) |
POST |
/insumos/:id/entrada |
insumos:ajustar |
POST |
/insumos/:id/ajuste |
insumos:ajustar |
POST |
/insumos/:id/descarte |
insumos:ajustar |
Toda escritura audita antes de ejecutar (ADR-0007).
