[debt] Mover venta y devolución al servidor con transacciones (SalesModule) #3

Open
opened 2026-10-02 17:35:54 +00:00 by Carlos · 0 comments
Member

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)

### 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
Carlos self-assigned this 2026-10-02 20:07:44 +00:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RynextTechnologies/PDV#3