Ir al contenido

Plan de M6 — Ventas

Objetivo: implementar M6 según la spec de diseño — la venta como documento inmutable, el piso de precio sostenido por la base, la anulación que revierte, y el cobro como documento aparte.

Dominio: Ventas — 13 reglas, 9 confirmadas.

Estado: ✅ Terminado (26-ago-2026) — las 12 tasks cerradas.

Task Commit
T1 — Schema e invariantes ✅ a249e7a
T2 — El cálculo del precio ✅ a249e7a
T3 — La transacción de la venta ✅ 11d164d
T4 — Anulación ✅ cbaa702
T5 — Crédito ✅ 11d164d — quedó dentro de T3
T6 — Cobros y saldo derivado ✅ 0f150c4
T7 — Devoluciones ✅ 0f150c4
T8 — Códigos de descuento ✅ 0f150c4
T9 — Endpoints ✅ 2efa24b + 2102209
T10 — Pantalla del mostrador ✅ d5aafa5
T11 — La deuda en la ficha ✅ d5aafa5
T12 — Bruno, documentación y cierre ✅ este commit

La venta NO consume tapa ni sello. El plan de T3 decía que sí. El dominio es explícito: «se consumen al llenar, no al entregar», y el cierre de producción ya cuenta botellonesLlenados incluyendo las recargas. Descontarlos otra vez los contaría dos veces — la planta creería tener menos tapas de las que tiene y compraría de más. La venta son tres escritos, no cuatro.

T5 quedó dentro de T3. El chequeo compuesto de crédito necesita la deuda, y la deuda se calcula con dos consultas triviales sobre tablas que T1 ya había creado. Separarlo habría sido una task que solo movía código de un archivo a otro.

Lo que apareció escribiendo, y no se había previsto

Sección titulada «Lo que apareció escribiendo, y no se había previsto»
  • El trigger de inmutabilidad frenó a su propio autor. La primera versión de la transacción insertaba la venta con total en cero y lo completaba al final; ventas_solo_anulacion la rechazó, y con razón. La salida no fue abrirle una excepción sino no necesitar el UPDATE: asignarFifo planifica y bloquea las filas, así que el total se calcula antes de que la venta exista.

  • montoAcreditado era decorativo. Las devoluciones lo guardaban y la deuda derivada no lo restaba. Ahora la fórmula es ventas a crédito confirmadas − cobros − devoluciones acreditadas.

  • Los fixtures de los tests del middleware chocaban con las rutas reales. Se llamaban /ventas y /auditoria a secas; registrar GET /ventas de verdad tiró los 19 tests de ese archivo con un error que apuntaba al lugar equivocado. Todos pasaron a /probar/*.


Los milestones anteriores tuvieron una. Este tiene dos, y son de tipos distintos:

La primera es el cálculo del precio. Como el dígito de verificación en M5, no falla: devuelve un número. Un descuento mal aplicado cobra de menos y nadie lo nota hasta el arqueo; uno que perfora el piso deja una venta bajo costo que solo aparece sumando el mes. Va primero, es una función pura sin base de datos, y el piso se prueba con el caso que lo perfora.

La segunda es la transacción de la venta, y es la de M4 otra vez: escribe la venta, sus líneas, el descuento de stock y el consumo de insumos. O quedan las cuatro, o no queda ninguna. Una venta registrada sin descontar stock genera faltantes fantasma que después nadie puede explicar.

T2 precio (puro) ──┐
├── T3 la transacción ── T4 anulación ──┐
T1 schema ─────────┘ ├── T9 endpoints ── T10 POS ── T11 la deuda en la ficha
├── T5 crédito ─────────────────────────┤
├── T6 cobros y saldo ──────────────────┤
├── T7 devoluciones ────────────────────┤
└── T8 códigos de descuento ────────────┘
T12 bruno + docs
  • T2 no depende de nada: es aritmética.
  • T3 es la compuerta: nada se construye encima hasta que la atomicidad esté probada.
  • T11 es lo que hace que M5 deje de mostrar «sin registrar todavía» en la primera de sus cuatro cuentas.

Las mismas de M4 y M5:

  • 2 espacios, comillas simples, sin punto y coma. Conventional commits sin Co-Authored-By. Nunca buildear.
  • Cada commit deja el proyecto en verde, verificado por código de salida.
  • Migraciones en SQL explícito, escritas a mano.
  • REVOKE explícito en toda tabla nueva (ADR-0006).
  • Aplicar la migración a las DOS bases.
  • Leer la matriz antes de tocar permisos. Los seis de ventas y los dos de cobros ya existen, con alcances propio/todo que codifican RN-VEN-08 mejor de lo que uno lo escribiría de memoria.
  • Correr la colección Bruno contra una instancia sobre aquazaku_test, no contra la base de desarrollo. Y mover BETTER_AUTH_URL con el puerto.

Cuatro tablas: ventas, lineas_de_venta, cobros, codigos_de_descuento.

Los invariantes que van a la base:

  1. CHECK (precio_final >= precio_minimo_aplicado) en la línea — RN-VEN-13. Es intra-fila porque el piso se congela junto al precio.
  2. CHECK: medio_de_pago = 'credito' implica cliente_id NOT NULL. No hay a quién cobrarle una deuda sin dueño.
  3. CHECK: los tres campos de anulación van juntos o los tres nulos.
  4. REVOKE UPDATE, DELETE sobre ventas y lineas_de_venta: una venta confirmada no se edita (RN-VEN-02). Anular cambia el estado, así que ventas conserva un UPDATE acotado — se resuelve con un trigger que solo permite la transición a anulada, igual que el append-only de audit_log.

Función pura. precio_final = max(precio_lista − descuento, precio_minimo).

Devuelve además si el código se aplicó parcialmente, que es lo que la pantalla necesita para avisar sin rechazar la venta.

La compuerta. Cuatro escritos o ninguno, con el descuento de stock atómico de M2 —UPDATE … WHERE cantidad_disponible >= :n— como única fuente de verdad sobre si alcanzaba.

Sin reserva de stock: la spec explica por qué.

Revierte al mismo lote, no a stock genérico: el lote tiene su vencimiento y devolver a otro lo falsearía.

Alcance propio para pos y seller, todo para admin. Comentario obligatorio para todos.

El chequeo compuesto de RN-VEN-05: habilitado y verificado y (sin tope o saldo + venta ≤ tope).

deuda = ventas a crédito − cobros. Derivado, no materializado: no hay descuento atómico que defender y dos cobros suman en vez de competir.

Otro flujo que la anulación (RN-VEN-10): no cancela la venta, ajusta inventario. sano vuelve al lote, vencido va a descarte.

Alta y baja por admin, vigencia, tope de usos. El contador de usos sube en la misma transacción de la venta.

Task 9 — Endpoints · Task 10 — Pantalla del mostrador · Task 11 — La deuda en la ficha

Sección titulada «Task 9 — Endpoints · Task 10 — Pantalla del mostrador · Task 11 — La deuda en la ficha»

Colección 9-Ventas (y Sesion pasa a 10-).


La de la spec, más:

  1. Los tests de M1–M5 siguen en verde sin haberlos tocado.
  2. Forzar el fallo de cualquiera de los cuatro escritos no deja rastro de los otros.
  3. Quitar el CHECK del piso pone un test en rojo.