Plan de M4 — Producción y cierre del día
Objetivo: implementar M4 según la spec de diseño — el cierre diario como documento único que mueve agua, insumos y stock en una sola transacción.
Dominio: Producción — 21 reglas confirmadas de 24.
Estado: ✅ Terminado (26-ago-2026) — las 7 tasks cerradas.
| Task | Commit | |
|---|---|---|
| T1 — Schema, migración e invariantes | ✅ | 97e127b |
| T2 — La transacción de cuatro escritos | ✅ | 35a7030 |
T3 — Abrir registrarEntrada de M2 |
✅ | 8ca6aca |
| T4 — Balance de agua y reconciliación | ✅ | e77da24 |
| T5 — Endpoints | ✅ | 6ddc068 |
| T6 — Pantalla del cierre y los tanques | ✅ | 3e9ac7c (web) · ef2049e (api) |
| T7 — Bruno, documentación y cierre | ✅ | este commit |
Lo que cambió respecto del plan. T5 creció con un endpoint que el plan no
tenía: GET /produccion/parametros. Apareció al escribir T6 — la vista previa
necesita 3.785 y 0.70 para mostrar cuánta agua se va a procesar, y
copiarlos al componente habría dejado dos fuentes de dos números que están
marcados para cambiar. El día que se mida el caudal de verdad, la pantalla
seguiría prometiendo el número viejo, y quien confirma una operación
irreversible lo haría creyendo otra cosa.
También creció el seed: TAPA_20L y SELLO_BOTELLON ahora se siembran. El
servicio del cierre los busca por código y lanza si no están, así que son
parte de la definición del sistema y no datos de cada instalación. Sin eso, una
base recién migrada tenía el catálogo completo y aun así no podía hacer la
operación central del negocio — y el error aparecía recién en la planta, con
los botellones ya envasados.
La compuerta de M4 no es la concurrencia: es la ATOMICIDAD
Sección titulada «La compuerta de M4 no es la concurrencia: es la ATOMICIDAD»M2 y M3 tuvieron el mismo riesgo de fondo —dos personas moviendo el mismo
saldo— y lo resolvieron con el UPDATE condicional. M4 tiene otro.
El cierre escribe en cuatro tablas: el documento, el libro del agua, el libro de insumos y el lote con su movimiento de stock. Una sola operación de negocio repartida en cuatro escritos.
Si falla el tercero, lo que queda es peor que nada: un cierre que dice que se envasaron 200 botellones, con el agua descontada y las tapas intactas. La planta cree que tiene insumos que ya gastó, y lo descubre el día que no puede envasar.
Un cierre parcial no es un cierre a medias — es una mentira consistente, que es la clase de dato que nadie sospecha hasta que ya causó daño.
Por eso T2 llega antes que todo lo demás, y se verifica al revés: forzando el fallo del último escrito y confirmando que no quedó ninguno.
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 |
|---|---|---|
| 🔴 Transacción parcial | Un cierre que miente de forma consistente. Se descubre contando tapas | T2 |
🔴 caudal_gpm referenciado y no copiado |
Corregir el caudal recalcula un mes de historia | T1 |
| 🔴 Un campo «litros ingresados» | Fabrica precisión que no existe. Sacarlo después obliga a decidir qué hacer con los valores ya cargados | T1 |
| 🟠 Dos tanques procesados en el enum | Cada escritura duplicada para que las dos filas digan lo mismo | T1 |
| 🟠 Saldo negativo de insumos permitido | Devuelve por la puerta de atrás lo que M2 y M3 impidieron | T2 |
| 🟢 Reconciliación visual | Aditiva, se puede afinar después | T4 |
M4 tiene que abrir el servicio de M2 — y eso es una task
Sección titulada «M4 tiene que abrir el servicio de M2 — y eso es una task»descontar() e ingresar() de stock e insumos ya aceptan un ejecutor
externo (Ejecutor = DB | Transaccion). Ese parámetro se agregó pensando en
este momento y no hay que tocarlo.
Pero registrarEntrada() de stock NO lo acepta. Usa db directo, abre su
propia transacción, y adentro hace tres cosas que M4 necesita enteras:
- Calcula el código de lote consultando los del día (
codigoDeLote). - Deriva la fecha de vencimiento (
vencimientoDe, 30 días —RN-STK-08). - Crea el lote en cero y lo sube con un movimiento, para que el libro explique el saldo desde la primera unidad.
M4 no puede reimplementar eso: sería una segunda fuente de códigos de lote y de vencimientos, y el día que la regla de 30 días cambie habría que acordarse de los dos lugares.
T3 abre registrarEntrada para que acepte un ejecutor, igual que ya lo hacen
sus vecinas. Es un cambio chico y de un solo tipo, pero toca código de M2 en
producción, así que va en su propia task con sus tests.
Grafo de dependencias
Sección titulada «Grafo de dependencias»T1 schema ── T2 la transacción ──┬── T3 abrir registrarEntrada ── T5 endpoints ── T6 pantalla │ └── T4 balance y reconciliación ─┘
T7 bruno + docs- T2 es la compuerta: nada se construye encima hasta que la atomicidad esté probada, aunque en esa etapa el lote se cree con SQL directo.
- T3 reemplaza ese SQL directo por la función de M2, ya abierta.
- T7 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.
- Migraciones en SQL explícito, escritas a mano.
REVOKEexplícito en toda tabla nueva (ADR-0006).- Aplicar la migración a las DOS bases:
db:migrateydb:migrate:test. En M3 la pantalla quedó rota con la suite en verde por saltarse la primera. /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/0005_produccion.sql - Crear:
api/src/db/__tests__/produccion-schema.test.ts
Qué se construye
cierres_produccion con UNIQUE(fecha) —un cierre por día, RN-PRD-22— y
movimientos_agua como libro append-only.
Los CHECK
CHECK (pacas_600 >= 0 AND pacas_300 >= 0)CHECK (botellones_llenados >= 0 AND botellones_lavados >= 0)CHECK (minutos_procesando > 0)CHECK (litros >= 0) -- en movimientos_agua: nunca negativoCHECK (litros <> 0)CHECK (tipo <> 'ajuste' OR motivo IS NOT NULL)Lo que este esquema NO tiene, y es lo importante
- No hay
litros_ingresados. El ingreso de la red no se mide (RN-PRD-11). Se registra el hecho contipo = 'ingreso_red'y el saldo sube hasta la banda observada. - No hay
procesado_a/procesado_b. Los tanques se operan en paralelo (RN-PRD-21). - No hay
PATCHposible:REVOKE UPDATE, DELETEsobre las dos tablas.
Verificación
- Cada
CHECKprobado con elINSERTinválido. - El segundo cierre de la misma fecha rechazado por el
UNIQUE. - Un test que verifique que la columna
litros_ingresadosNO existe. Suena raro y es el más valioso: es la única forma de que alguien no la agregue «para completar el formulario».
Task 2 — La transacción, y la prueba de que es atómica
Sección titulada «Task 2 — La transacción, y la prueba de que es atómica»Archivos
- Crear:
api/src/modules/produccion/cierre.ts - Crear:
api/src/modules/produccion/__tests__/atomicidad.test.ts
El mecanismo
await db.transaction(async (tx) => { const cierre = await insertarCierre(tx, datos) await descontarAgua(tx, cierre) // envasado + lavado await descontarInsumos(tx, cierre) // tapa + sello por botellón await ingresarProducto(tx, cierre) // lote + movimiento})Los cuatro reciben tx. Ninguno abre su propia transacción — enTransaccion
de saldo.ts ya lo resuelve: abre solo si el ejecutor no es una.
Los insumos que no alcanzan cortan el cierre
descontar() devuelve { ok: false, disponible }. Acá eso lanza, para que
la transacción revierta. Es el único lugar del sistema donde ese false se
convierte en excepción, y con razón: en M3 «no alcanza» era una respuesta porque
la operación era una sola; acá hay tres escritos más que deshacer.
Verificación — la que hace que esta task exista
- Forzar el fallo del cuarto escrito y verificar que no quedó ninguno: ni cierre, ni movimiento de agua, ni de insumo.
- Ídem forzando el fallo del segundo y del tercero.
- Con insumos insuficientes: se rechaza, con el faltante en el mensaje, y el agua no se descontó.
- Quitar la transacción —dejar los cuatro escritos sueltos— y confirmar que los tests de arriba fallan. Una prueba de atomicidad que pasa sin transacción no está midiendo nada.
Task 3 — Abrir registrarEntrada de M2
Sección titulada «Task 3 — Abrir registrarEntrada de M2»Archivos
- Modificar:
api/src/modules/stock/service.ts - Modificar:
api/src/modules/produccion/cierre.ts
Qué se cambia
registrarEntrada(entrada, contexto) pasa a
registrarEntrada(entrada, contexto, ejecutor = db), y usa enTransaccion como
ya hacen descontar e ingresar. La consulta de códigos del día también tiene
que ir por el ejecutor: leer fuera de la transacción vería un estado viejo.
Por qué es una task y no un renglón de T2
Toca código de M2 que está en producción y lo usan otras dos rutas. El riesgo no es el cambio —es de un solo tipo— sino que nadie note que ahora se puede llamar desde adentro de una transacción ajena y alguien vuelva a abrir una propia.
Verificación
- Los tests de M2 siguen en verde sin tocarlos. Si hay que tocarlos, el cambio no fue compatible.
- Un test nuevo: llamarla dentro de una transacción que después revierte, y verificar que el lote no quedó.
Task 4 — El balance del agua y la reconciliación
Sección titulada «Task 4 — El balance del agua y la reconciliación»Archivos
- Crear:
api/src/modules/produccion/agua.ts - Crear:
api/src/modules/produccion/__tests__/agua.test.ts
Los dos saldos
Procesado (4.000 L) con balance cerrado; crudo (13.000 L) con un término sin
medir. El consumo sale de las cuatro cifras del cierre — RN-PRD-06.
La reconciliación — RN-PRD-14
El operario reporta el nivel en cuartos. El sistema compara contra su saldo: dentro de la banda cuadra; fuera, marca la discrepancia y ofrece un ajuste con motivo.
Verificación
- Las cinco bandas, en sus dos fronteras cada una.
- Un saldo justo en el límite entre dos cuartos: no puede marcar discrepancia.
- El ajuste exige motivo, y lo exige el
CHECKademás del servicio.
Task 5 — Endpoints
Sección titulada «Task 5 — Endpoints»Archivos
- Crear:
api/src/modules/produccion/routes.ts,validation.ts - Crear:
api/src/modules/produccion/__tests__/routes.test.ts
Las rutas de la spec §10. Los permisos ya están declarados en la matriz —
produccion:ver, produccion:registrar_cierre, tanques:ver,
tanques:registrar_reposicion y tanques:ajustar.
Verificación
- El
sellerrecibe 403 en todas, y queda auditado. - El
pospuede reponer pero no ajustar: es la separación que da sentido a tener dos permisos. - No existen
PATCHniDELETEsobre un cierre.
Task 6 — Pantalla en web/
Sección titulada «Task 6 — Pantalla en web/»Archivos
- Crear:
web/src/app/(app)/modulos/produccion/yweb/src/components/produccion/ - Modificar:
web/src/lib/modules.ts
El formulario del cierre es una sola pantalla, y muestra lo que va a pasar antes de confirmar: cuántos litros se van a descontar, cuántas tapas y sellos, y qué lote se va a generar.
Es la misma decisión que la vista previa de la conversión en M3. Un cierre mueve
tres saldos de una vez y no se puede deshacer (RN-PRD-08): confirmarlo a
ciegas es lo que hace que un error se descubra tres días después.
Se reusa todo: <Estado>, <Vacio>, .aq-campo, .aq-boton*, <SelloDeHora>.
Task 7 — Bruno, documentación y cierre
Sección titulada «Task 7 — Bruno, documentación y cierre»- Colección
7-Produccioncon los casos que definen el módulo, no el CRUD. - El roadmap pasa M4 a ✅.
- Las preguntas 4, 5 y 6 quedan marcadas con lo que bloquean: sin caudal no se calcula el procesamiento, sin litros de lavado un término del balance queda en cero.
Definition of Done de M4
Sección titulada «Definition of Done de M4»- Un
posregistra el cierre y los cuatro escritos quedan, o ninguno. - Forzar el fallo de cualquiera de los cuatro no deja rastro de los otros.
- Con insumos insuficientes se rechaza con el faltante, y el agua no se tocó.
- Un segundo cierre de la misma fecha se rechaza.
- Corregir el caudal no altera ningún cierre pasado.
- El sistema nunca pide «litros ingresados».
- Una lectura que cuadra no genera ajuste; una que no, sí.
- Los tests de M2 siguen en verde sin haberlos tocado.
Notas de ejecución
Sección titulada «Notas de ejecución»- A partir de M4, mover stock o insumos a mano vuelve a ser una excepción. El ajuste con motivo sigue existiendo porque la realidad física siempre difiere, pero ya no es el camino normal.
- La autonomía en días (
RN-PRD-13) y «hacen falta más tanques» (RN-PRD-17) son de M12: necesitan la historia que M4 recién empieza a generar.
