Ir al contenido

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.

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.

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.
  • 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 tail devuelve el estado de tail.
  • Migraciones en SQL explícito, escritas a mano.
  • Toda tabla nueva necesita REVOKE explícito: el ALTER DEFAULT PRIVILEGES de la migración 0001 concede DELETE sobre todo (ADR-0006).
  • El invariante vive en la base; el servicio explica el error.
  • /bin/rm para artefactos de drizzle-kit: rm está aliaseado a trash.

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 CHECK se prueba intentando el INSERT inválido y esperando el rechazo.
  • El REVOKE se prueba intentando el DELETE con el rol de la app.
  • Quitar el CHECK de 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 - :cantidad
WHERE id = :id AND saldo >= :cantidad
RETURNING saldo

Cero 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

  1. 20 salidas de 10 sobre un saldo de 100: exactamente 10 tienen éxito.
  2. La suma del libro explica el saldo, sin importar el orden.
  3. Nunca deja el saldo negativo, con cantidades desparejas.
  4. Entradas y salidas simultáneas cuadran.
  5. 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.

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).