La venta (apps/web/src/app/pos/page.tsx, handleCheckout) y la devolución (apps/web/src/app/pos/historial/page.tsx, handleReturn) se orquestan desde el navegador como cadenas de entre 4 y 2N+4 requests secuenciales, sin transacción.
En el API, varias reglas se validan leyendo y luego escribiendo, sin atomicidad:
inventory-movement.service.ts:62-76: lee el stock y después lo actualiza.
cash-register-session.service.ts:35 y operating-day.service.ts:27: "ya hay caja/día abierto" se revisa con findAll() y después se crea.
operating-day.service.ts:51-55: cerrar el día crea el siguiente en otra operación.
Además:
Los montos Decimal llegan como string y se operan con Number() y toFixed(2) en el POS, el corte de caja (cash-register-control.tsx:61-63) y los reportes.
Las fechas de los eventos (paidAt, closedAt, etc.) las envía el navegador con new Date().toISOString().
Qué estamos pagando hoy
Si un request falla a mitad de una venta, queda un carrito open con stock descontado y sin pago.
totalPrice y discount los calcula el cliente y el servidor no los verifica.
Dos ventas simultáneas pueden dejar el stock en negativo.
Se pueden abrir dos cajas en el mismo inventario o dos días de operación a la vez.
Si falla la creación del día siguiente, el sistema queda sin día abierto.
Con muchas operaciones, el punto flotante produce diferencias de centavos en el corte de caja.
Si la computadora de la caja tiene mal la hora, los registros quedan con hora incorrecta.
Arreglo propuesto
Crear un SalesModule con POST /sales y POST /sales/:id/refund.
El servidor recibe solo { items: [{ productId, quantity }], discount, paymentMethod, customerId }, calcula precios desde el ProductHistory activo y ejecuta todo en una transacción.
Descontar stock de forma atómica: UPDATE warehouse SET stock = stock - n WHERE ... AND stock >= n.
Hacer transaccional el cierre y apertura del día de operación.
Calcular montos en el servidor con Decimal (o en centavos enteros), no con Number().
Que las fechas de los eventos las ponga el servidor o la base de datos (@default(now())), no el cliente.
Criterios de aceptación:
Una venta o devolución fallida no deja datos parciales
El total lo calcula el servidor
El stock no puede quedar negativo bajo concurrencia
Los montos no tienen errores de redondeo
Las fechas de venta, pago y cierre las asigna el servidor
Techo de esta ronda
Venta, devolución y cierre de día. Fuera: conteo físico de inventario (inventario/[id]/page.tsx, también hace N requests), autenticación (#5) y validación de DTOs (#4).
Superficie
API (apps/api)
Módulos
API / backend, Punto de venta (POS), Caja / sesiones de caja, Inventario / almacenes, Día de operación, Base de datos / contrato Prisma
Esfuerzo
L — partir; este issue solo cubre el primer corte
Riesgo de no tocarlo
Alto (seguridad, datos, o bloquea el próximo corte)
### Qué es la deuda
La venta (`apps/web/src/app/pos/page.tsx`, `handleCheckout`) y la devolución (`apps/web/src/app/pos/historial/page.tsx`, `handleReturn`) se orquestan desde el navegador como cadenas de entre 4 y 2N+4 requests secuenciales, sin transacción.
En el API, varias reglas se validan leyendo y luego escribiendo, sin atomicidad:
- `inventory-movement.service.ts:62-76`: lee el stock y después lo actualiza.
- `cash-register-session.service.ts:35` y `operating-day.service.ts:27`: "ya hay caja/día abierto" se revisa con `findAll()` y después se crea.
- `operating-day.service.ts:51-55`: cerrar el día crea el siguiente en otra operación.
Además:
- Los montos `Decimal` llegan como string y se operan con `Number()` y `toFixed(2)` en el POS, el corte de caja (`cash-register-control.tsx:61-63`) y los reportes.
- Las fechas de los eventos (`paidAt`, `closedAt`, etc.) las envía el navegador con `new Date().toISOString()`.
### Qué estamos pagando hoy
- Si un request falla a mitad de una venta, queda un carrito `open` con stock descontado y sin pago.
- `totalPrice` y `discount` los calcula el cliente y el servidor no los verifica.
- Dos ventas simultáneas pueden dejar el stock en negativo.
- Se pueden abrir dos cajas en el mismo inventario o dos días de operación a la vez.
- Si falla la creación del día siguiente, el sistema queda sin día abierto.
- Con muchas operaciones, el punto flotante produce diferencias de centavos en el corte de caja.
- Si la computadora de la caja tiene mal la hora, los registros quedan con hora incorrecta.
### Arreglo propuesto
- Crear un `SalesModule` con `POST /sales` y `POST /sales/:id/refund`.
- El servidor recibe solo `{ items: [{ productId, quantity }], discount, paymentMethod, customerId }`, calcula precios desde el `ProductHistory` activo y ejecuta todo en una transacción.
- Descontar stock de forma atómica: `UPDATE warehouse SET stock = stock - n WHERE ... AND stock >= n`.
- Hacer transaccional el cierre y apertura del día de operación.
- Calcular montos en el servidor con `Decimal` (o en centavos enteros), no con `Number()`.
- Que las fechas de los eventos las ponga el servidor o la base de datos (`@default(now())`), no el cliente.
Criterios de aceptación:
- [ ] Una venta o devolución fallida no deja datos parciales
- [ ] El total lo calcula el servidor
- [ ] El stock no puede quedar negativo bajo concurrencia
- [ ] Los montos no tienen errores de redondeo
- [ ] Las fechas de venta, pago y cierre las asigna el servidor
### Techo de esta ronda
Venta, devolución y cierre de día. Fuera: conteo físico de inventario (`inventario/[id]/page.tsx`, también hace N requests), autenticación (#5) y validación de DTOs (#4).
### Superficie
API (apps/api)
### Módulos
API / backend, Punto de venta (POS), Caja / sesiones de caja, Inventario / almacenes, Día de operación, Base de datos / contrato Prisma
### Esfuerzo
L — partir; este issue solo cubre el primer corte
### Riesgo de no tocarlo
Alto (seguridad, datos, o bloquea el próximo corte)
Carlos
changed title from Mover venta y devolución al servidor con transacciones (SalesModule) to [debt] Mover venta y devolución al servidor con transacciones (SalesModule)2026-10-02 17:42:56 +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 venta (
apps/web/src/app/pos/page.tsx,handleCheckout) y la devolución (apps/web/src/app/pos/historial/page.tsx,handleReturn) se orquestan desde el navegador como cadenas de entre 4 y 2N+4 requests secuenciales, sin transacción.En el API, varias reglas se validan leyendo y luego escribiendo, sin atomicidad:
inventory-movement.service.ts:62-76: lee el stock y después lo actualiza.cash-register-session.service.ts:35yoperating-day.service.ts:27: "ya hay caja/día abierto" se revisa confindAll()y después se crea.operating-day.service.ts:51-55: cerrar el día crea el siguiente en otra operación.Además:
Decimalllegan como string y se operan conNumber()ytoFixed(2)en el POS, el corte de caja (cash-register-control.tsx:61-63) y los reportes.paidAt,closedAt, etc.) las envía el navegador connew Date().toISOString().Qué estamos pagando hoy
opencon stock descontado y sin pago.totalPriceydiscountlos calcula el cliente y el servidor no los verifica.Arreglo propuesto
SalesModuleconPOST /salesyPOST /sales/:id/refund.{ items: [{ productId, quantity }], discount, paymentMethod, customerId }, calcula precios desde elProductHistoryactivo y ejecuta todo en una transacción.UPDATE warehouse SET stock = stock - n WHERE ... AND stock >= n.Decimal(o en centavos enteros), no conNumber().@default(now())), no el cliente.Criterios de aceptación:
Techo de esta ronda
Venta, devolución y cierre de día. Fuera: conteo físico de inventario (
inventario/[id]/page.tsx, también hace N requests), autenticación (#5) y validación de DTOs (#4).Superficie
API (apps/api)
Módulos
API / backend, Punto de venta (POS), Caja / sesiones de caja, Inventario / almacenes, Día de operación, Base de datos / contrato Prisma
Esfuerzo
L — partir; este issue solo cubre el primer corte
Riesgo de no tocarlo
Alto (seguridad, datos, o bloquea el próximo corte)
Mover venta y devolución al servidor con transacciones (SalesModule)to [debt] Mover venta y devolución al servidor con transacciones (SalesModule)