Etapa 0.2-A: Diseño y precheck. No se ejecuta nada en producción. Este documento es la primera entrega: plan + mapa de dependencias + diseño de RPC + rollback. La ejecución de migraciones requiere autorización explícita del propietario.
sesiones.finalizadafinalizada en Sprint 0.2Razones:
useSalas, TVDisplay, EventLive, Reportes, Salas, TarjetaSala, ModalTrasladarSesion, MovimientoDeHoy, TablaSesionesActivas, MonitorSalasActivas.finalizarSesion y anularSesion usan if (sesion.finalizada) return como guard.TVDisplay y EventLive usan .eq('finalizada', false) directamente contra la DB.vw_reporte_diario (en schema legacy) referencia s.finalizada.actualizar_trigger_stats_cliente.sql y crear_tabla_clientes.sql usan IF NEW.estado = 'finalizada'.finalizada (postergado a Sprint 0.3 o posterior)| Fase | Acción | Sprint |
|---|---|---|
| 0.2 | Mantener finalizada tal cual. No tocar. |
Actual |
| 0.3 | Migrar gradualmente filtros JS de !s.finalizada → s.estado !== 'finalizada' && s.estado !== 'cancelada' |
Futuro |
| 0.3 | Migrar queries DB de .eq('finalizada', false) → .in('estado', ['activa']) |
Futuro |
| 0.4 | Una vez verificado que nada lee finalizada, eliminar la columna con migración + rollback |
Futuro |
Conclusión: finalizada queda intacta en Sprint 0.2. No es bloqueante para transaccionalidad.
| # | Archivo | Función | Tipo | Atómica | Riesgo |
|---|---|---|---|---|---|
| V1 | useSalas.js:699 |
_registrarVentaContable |
INSERT ventas | ❌ | Sesión ya finalizada antes. Si falla, sesión sin venta. |
| V2 | ModalTienda.jsx:194 |
procesarVenta (POS) |
INSERT ventas | ❌ | CRÍTICO: stock ya descontado. Si falla, stock perdido. |
| V3 | ModalTienda.jsx:217 |
procesarVenta (POS) |
INSERT venta_items (loop) | ❌ | Items parciales si un item falla. |
| V4 | Ventas.jsx:341 |
eliminar |
DELETE ventas | ❌ | Stock devuelto antes. Si DELETE falla, stock duplicado. |
| V5 | Ventas.jsx:410 |
guardarEdicion |
UPDATE ventas | ❌ | Stock ajustado antes. Sesión sincronizada después. |
| V6 | js/salas.js:470 |
_registrarVentaContable (legacy) |
INSERT ventas | ❌ | Legacy — no usado por build. |
| V7 | js/stock.js:1770 |
registrarVentaTienda (legacy) |
INSERT ventas | ❌ | Legacy — no usado por build. |
| V8 | js/ventas.js:1152 |
anularVenta (legacy) |
UPDATE ventas | ❌ | Legacy — no usado por build. |
Puntos críticos productivos (src/): V1, V2, V3, V4, V5
| # | Archivo | Función | Patrón | Race condition |
|---|---|---|---|---|
| S1 | ModalAjustarStock.jsx:35 |
Ajuste manual | READ-MODIFY-WRITE | 🔴 SÍ |
| S2 | ModalIngresarMercancia.jsx:187 |
Ingreso mercancía | READ-MODIFY-WRITE | 🔴 SÍ |
| S3 | ModalTienda.jsx:165 |
Venta POS | READ-MODIFY-WRITE | 🔴 SÍ |
| S4 | useSalas.js:245 |
agregarProducto |
READ-MODIFY-WRITE | 🔴 SÍ |
| S5 | useSalas.js:310 |
agregarProductos (Promise.all) |
READ-MODIFY-WRITE | 🔴 SÍ |
| S6 | useSalas.js:575 |
editarSesionAdmin (devolución) |
READ-MODIFY-WRITE | 🔴 SÍ |
| S7 | Ventas.jsx:320 |
eliminar (devolución) |
READ-MODIFY-WRITE | 🔴 SÍ |
| S8 | Ventas.jsx:392 |
guardarEdicion (ajuste) |
READ-MODIFY-WRITE | 🔴 SÍ |
Problemas adicionales:
Ventas.jsx:332,404: .catch(() => {}) silencia errores de INSERT movimientos_stock.Stock.jsx:87: DELETE producto → CASCADE elimina historial de movimientos (pérdida de auditoría).ModalProducto.jsx:143: UPDATE stock manual sin registrar movimiento.| # | Archivo | Función | Operaciones | Atómica |
|---|---|---|---|---|
| C1 | CierreTurno.jsx:273 |
calcularYGuardar |
INSERT cierres_turno + INSERT cierre_turno_items | ❌ |
Problemas adicionales:
efectivo_esperado se calcula desde ventas (no sesiones) — correcto según modelo canónico.inventario_esperado_valor usa stock actual de productos — correcto.Todas las RPCs serán:
LANGUAGE plpgsqlSECURITY DEFINER (para operar con privilegios sobre tablas protegidas por RLS)SET search_path = publicRAISE EXCEPTION)FOR UPDATE o WHERE stock >= n)descontar_stock_atomicoObjetivo: Reemplazar el patrón READ-MODIFY-WRITE por una operación atómica.
CREATE OR REPLACE FUNCTION public.descontar_stock_atomico(
p_producto_id UUID,
p_cantidad INTEGER,
p_tipo TEXT DEFAULT 'venta',
p_motivo TEXT DEFAULT NULL,
p_referencia TEXT DEFAULT NULL,
p_usuario_id UUID DEFAULT NULL
)
RETURNS TABLE(stock_nuevo INTEGER, stock_anterior INTEGER)
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_stock_anterior INTEGER;
v_stock_nuevo INTEGER;
v_delta INTEGER;
BEGIN
-- Determinar delta según tipo
IF p_tipo IN ('venta','salida','merma') THEN
v_delta := -ABS(p_cantidad);
ELSIF p_tipo IN ('entrada','devolucion','ajuste') THEN
v_delta := ABS(p_cantidad);
ELSE
RAISE EXCEPTION 'Tipo de movimiento inválido: %', p_tipo;
END IF;
-- Leer stock con bloqueo de fila
SELECT stock INTO v_stock_anterior
FROM public.productos
WHERE id = p_producto_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Producto no encontrado: %', p_producto_id;
END IF;
v_stock_nuevo := v_stock_anterior + v_delta;
-- Validar stock no negativo para salidas
IF v_delta < 0 AND v_stock_nuevo < 0 THEN
RAISE EXCEPTION 'Stock insuficiente para producto %. Stock actual: %, solicitado: %',
p_producto_id, v_stock_anterior, ABS(p_cantidad);
END IF;
-- Actualizar stock
UPDATE public.productos
SET stock = v_stock_nuevo
WHERE id = p_producto_id;
-- Registrar movimiento (atómico con el UPDATE)
INSERT INTO public.movimientos_stock (
producto_id, usuario_id, tipo, cantidad,
stock_anterior, stock_nuevo, motivo, referencia
) VALUES (
p_producto_id, p_usuario_id, p_tipo, ABS(p_cantidad),
v_stock_anterior, v_stock_nuevo, p_motivo, p_referencia
);
RETURN QUERY SELECT v_stock_nuevo, v_stock_anterior;
END;
$$;
Reemplaza: S1, S2, S3, S4, S5, S6, S7, S8 (las 8 ubicaciones vulnerables).
Rollback: DROP FUNCTION public.descontar_stock_atomico(...).
registrar_venta_posObjetivo: Venta POS directa atómica (sin sesión).
CREATE OR REPLACE FUNCTION public.registrar_venta_pos(
p_usuario_id UUID,
p_cliente TEXT DEFAULT 'Cliente tienda',
p_estacion TEXT DEFAULT 'Tienda',
p_metodo_pago TEXT DEFAULT 'efectivo',
p_subtotal_productos NUMERIC DEFAULT 0,
p_descuento NUMERIC DEFAULT 0,
p_total NUMERIC DEFAULT 0,
p_notas TEXT DEFAULT NULL,
p_items JSONB DEFAULT '[]'::jsonb,
-- Pagos divididos (opcionales)
p_monto_efectivo NUMERIC DEFAULT NULL,
p_monto_transferencia NUMERIC DEFAULT NULL,
p_monto_tarjeta NUMERIC DEFAULT NULL,
p_monto_digital NUMERIC DEFAULT NULL
)
RETURNS UUID
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_venta_id UUID;
v_item JSONB;
v_producto_id UUID;
v_cantidad NUMERIC;
v_precio NUMERIC;
v_subtotal NUMERIC;
v_line_no INT := 0;
v_stock_result RECORD;
BEGIN
-- 1. Crear venta
INSERT INTO public.ventas (
sesion_id, sala_id, usuario_id, cliente, estacion,
fecha_inicio, fecha_cierre, metodo_pago, estado,
subtotal_tiempo, subtotal_productos, descuento, total, notas,
monto_efectivo, monto_transferencia, monto_tarjeta, monto_digital
) VALUES (
NULL, NULL, p_usuario_id, p_cliente, p_estacion,
NULL, NOW(), p_metodo_pago, 'cerrada',
0, p_subtotal_productos, p_descuento, p_total, p_notas,
p_monto_efectivo, p_monto_transferencia, p_monto_tarjeta, p_monto_digital
)
RETURNING id INTO v_venta_id;
-- 2. Procesar items + descontar stock atómicamente
FOR v_item IN SELECT * FROM jsonb_array_elements(p_items) LOOP
v_line_no := v_line_no + 1;
v_producto_id := (v_item->>'producto_id')::UUID;
v_cantidad := (v_item->>'cantidad')::NUMERIC;
v_precio := (v_item->>'precio_unitario')::NUMERIC;
v_subtotal := (v_item->>'subtotal')::NUMERIC;
-- Descontar stock atómico
SELECT * INTO v_stock_result FROM public.descontar_stock_atomico(
v_producto_id, v_cantidad::INTEGER, 'venta',
'Venta POS', v_venta_id::TEXT, p_usuario_id
);
-- Insertar venta_item
INSERT INTO public.venta_items (
venta_id, line_no, tipo, producto_id,
descripcion, cantidad, precio_unitario, subtotal
) VALUES (
v_venta_id, v_line_no, 'producto', v_producto_id,
v_item->>'descripcion', v_cantidad, v_precio, v_subtotal
);
END LOOP;
RETURN v_venta_id;
END;
$$;
Reemplaza: V2 + V3 + S3 (ModalTienda.jsx completo).
Rollback: DROP FUNCTION public.registrar_venta_pos(...).
finalizar_sesionObjetivo: Finalizar sesión + registrar venta contable atómicamente.
CREATE OR REPLACE FUNCTION public.finalizar_sesion(
p_sesion_id UUID,
p_metodo_pago TEXT DEFAULT 'efectivo',
p_monto_efectivo NUMERIC DEFAULT NULL,
p_monto_transferencia NUMERIC DEFAULT NULL,
p_monto_tarjeta NUMERIC DEFAULT NULL,
p_monto_digital NUMERIC DEFAULT NULL,
p_descuento NUMERIC DEFAULT 0,
p_notas TEXT DEFAULT NULL
)
RETURNS UUID -- retorna el venta_id
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_sesion RECORD;
v_venta_id UUID;
v_total_general NUMERIC;
v_total_productos NUMERIC;
v_total_tiempo NUMERIC;
BEGIN
-- 1. Leer sesión con bloqueo
SELECT * INTO v_sesion
FROM public.sesiones
WHERE id = p_sesion_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Sesión no encontrada: %', p_sesion_id;
END IF;
IF v_sesion.finalizada = true OR v_sesion.estado IN ('finalizada','cancelada') THEN
RAISE EXCEPTION 'La sesión ya fue finalizada: %', p_sesion_id;
END IF;
-- 2. Calcular totales
v_total_tiempo := COALESCE(v_sesion.total_tiempo, 0);
v_total_productos := COALESCE(v_sesion.total_productos, 0);
v_total_general := v_total_tiempo + v_total_productos - p_descuento;
-- 3. Actualizar sesión
UPDATE public.sesiones
SET
fecha_fin = NOW(),
estado = 'finalizada',
finalizada = true,
metodo_pago = p_metodo_pago,
descuento = p_descuento,
total_general = v_total_general,
monto_efectivo = p_monto_efectivo,
monto_transferencia = p_monto_transferencia,
monto_tarjeta = p_monto_tarjeta,
monto_digital = p_monto_digital,
notas = COALESCE(p_notas, notas)
WHERE id = p_sesion_id;
-- 4. Registrar venta contable (UPSERT por sesion_id UNIQUE)
INSERT INTO public.ventas (
sesion_id, sala_id, usuario_id, cliente, estacion,
fecha_inicio, fecha_cierre, metodo_pago, estado,
subtotal_tiempo, subtotal_productos, descuento, total, notas,
monto_efectivo, monto_transferencia, monto_tarjeta, monto_digital
) VALUES (
v_sesion.id, v_sesion.sala_id, v_sesion.usuario_id,
v_sesion.cliente, v_sesion.estacion,
v_sesion.fecha_inicio, NOW(), p_metodo_pago, 'cerrada',
v_total_tiempo, v_total_productos, p_descuento, v_total_general, p_notas,
p_monto_efectivo, p_monto_transferencia, p_monto_tarjeta, p_monto_digital
)
ON CONFLICT (sesion_id) DO UPDATE SET
metodo_pago = EXCLUDED.metodo_pago,
estado = 'cerrada',
subtotal_tiempo = EXCLUDED.subtotal_tiempo,
subtotal_productos = EXCLUDED.subtotal_productos,
descuento = EXCLUDED.descuento,
total = EXCLUDED.total,
monto_efectivo = EXCLUDED.monto_efectivo,
monto_transferencia = EXCLUDED.monto_transferencia,
monto_tarjeta = EXCLUDED.monto_tarjeta,
monto_digital = EXCLUDED.monto_digital,
fecha_cierre = EXCLUDED.fecha_cierre
RETURNING id INTO v_venta_id;
-- NOTA: El stock de productos ya fue descontado durante la sesión
-- (al agregar productos). No se descuenta aquí.
-- Los venta_items se insertarán en una fase posterior (Sprint 0.3)
-- cuando se unifique la lógica de items entre sesión y venta.
RETURN v_venta_id;
END;
$$;
Reemplaza: V1 + parte de finalizarSesion en useSalas.js.
Rollback: DROP FUNCTION public.finalizar_sesion(...).
anular_sesionObjetivo: Anular sesión + anular venta + devolver stock atómicamente.
CREATE OR REPLACE FUNCTION public.anular_sesion(
p_sesion_id UUID,
p_motivo TEXT DEFAULT NULL
)
RETURNS VOID
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_sesion RECORD;
v_producto JSONB;
v_producto_id UUID;
v_cantidad NUMERIC;
BEGIN
-- 1. Leer sesión con bloqueo
SELECT * INTO v_sesion
FROM public.sesiones
WHERE id = p_sesion_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Sesión no encontrada: %', p_sesion_id;
END IF;
IF v_sesion.finalizada = true THEN
RAISE EXCEPTION 'La sesión ya fue finalizada: %', p_sesion_id;
END IF;
-- 2. Devolver stock de productos consumidos
-- sesiones.productos es un JSONB array: [{producto_id, cantidad, ...}]
IF v_sesion.productos IS NOT NULL AND jsonb_array_length(v_sesion.productos) > 0 THEN
FOR v_producto IN SELECT * FROM jsonb_array_elements(v_sesion.productos) LOOP
v_producto_id := v_producto->>'producto_id';
v_cantidad := (v_producto->>'cantidad')::NUMERIC;
-- Skip bonos (no descuentan stock)
CONTINUE WHEN v_producto->>'categoria' = 'bonos';
CONTINUE WHEN v_producto_id IS NULL;
-- Devolver stock atómico
PERFORM public.descontar_stock_atomico(
v_producto_id::UUID, v_cantidad::INTEGER, 'devolucion',
COALESCE('Anulación sesión ' || p_sesion_id::TEXT, p_motivo),
p_sesion_id::TEXT, v_sesion.usuario_id
);
END LOOP;
END IF;
-- 3. Anular sesión
UPDATE public.sesiones
SET
fecha_fin = NOW(),
estado = 'cancelada',
finalizada = true,
metodo_pago = NULL,
total_general = 0,
total_productos = 0,
total_tiempo = 0,
costo_adicional = 0,
monto_efectivo = NULL,
monto_transferencia = NULL,
monto_tarjeta = NULL,
monto_digital = NULL,
notas = COALESCE(p_motivo, notas)
WHERE id = p_sesion_id;
-- 4. Anular venta si existe
UPDATE public.ventas
SET estado = 'anulada', updated_at = NOW()
WHERE sesion_id = p_sesion_id AND estado = 'cerrada';
END;
$$;
Reemplaza: anularSesion en useSalas.js (que hoy NO devuelve stock).
Rollback: DROP FUNCTION public.anular_sesion(...).
guardar_cierre_turnoObjetivo: Cierre de turno atómico (cabecera + items).
CREATE OR REPLACE FUNCTION public.guardar_cierre_turno(
p_usuario_id UUID,
p_usuario_email TEXT DEFAULT NULL,
p_usuario_nombre TEXT DEFAULT NULL,
p_rol_usuario TEXT DEFAULT NULL,
p_turno_desde TIMESTAMPTZ,
p_turno_hasta TIMESTAMPTZ DEFAULT NOW(),
p_efectivo_contado NUMERIC DEFAULT 0,
p_efectivo_esperado NUMERIC DEFAULT 0,
p_inventario_esperado_valor NUMERIC DEFAULT 0,
p_inventario_contado_valor NUMERIC DEFAULT 0,
p_observaciones TEXT DEFAULT NULL,
p_ticket_resumen TEXT DEFAULT NULL,
p_creado_por JSONB DEFAULT '{}'::jsonb,
p_items JSONB DEFAULT '[]'::jsonb
)
RETURNS UUID
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_cierre_id UUID;
v_efectivo_descuadre NUMERIC;
v_inventario_descuadre NUMERIC;
v_total_descuadre NUMERIC;
v_item JSONB;
v_line INT := 0;
BEGIN
-- 1. Validar que no exista cierre superpuesto
PERFORM 1 FROM public.cierres_turno
WHERE usuario_id = p_usuario_id
AND turno_desde < p_turno_hasta
AND turno_hasta > p_turno_desde
LIMIT 1;
IF FOUND THEN
RAISE EXCEPTION 'Ya existe un cierre de turno para este período';
END IF;
-- 2. Calcular descuadres
v_efectivo_descuadre := p_efectivo_contado - p_efectivo_esperado;
v_inventario_descuadre := p_inventario_contado_valor - p_inventario_esperado_valor;
v_total_descuadre := v_efectivo_descuadre + v_inventario_descuadre;
-- 3. Insertar cierre
INSERT INTO public.cierres_turno (
usuario_id, usuario_email, usuario_nombre, rol_usuario,
turno_desde, turno_hasta,
efectivo_contado, efectivo_esperado, efectivo_descuadre,
inventario_esperado_valor, inventario_contado_valor, inventario_descuadre_valor,
total_descuadre, observaciones, ticket_resumen, creado_por
) VALUES (
p_usuario_id, p_usuario_email, p_usuario_nombre, p_rol_usuario,
p_turno_desde, p_turno_hasta,
p_efectivo_contado, p_efectivo_esperado, v_efectivo_descuadre,
p_inventario_esperado_valor, p_inventario_contado_valor, v_inventario_descuadre,
v_total_descuadre, p_observaciones, p_ticket_resumen, p_creado_por
)
RETURNING id INTO v_cierre_id;
-- 4. Insertar items atómicamente
FOR v_item IN SELECT * FROM jsonb_array_elements(p_items) LOOP
v_line := v_line + 1;
INSERT INTO public.cierre_turno_items (
cierre_turno_id, producto_id, nombre_producto,
stock_sistema, stock_contado, diferencia_unidades,
precio_unitario, valor_descuadre, detalles
) VALUES (
v_cierre_id,
(v_item->>'producto_id')::UUID,
v_item->>'nombre_producto',
(v_item->>'stock_sistema')::NUMERIC,
(v_item->>'stock_contado')::NUMERIC,
(v_item->>'diferencia_unidades')::NUMERIC,
(v_item->>'precio_unitario')::NUMERIC,
(v_item->>'valor_descuadre')::NUMERIC,
v_item->'detalles'
);
END LOOP;
RETURN v_cierre_id;
END;
$$;
Reemplaza: C1 (CierreTurno.jsx completo).
Rollback: DROP FUNCTION public.guardar_cierre_turno(...).
| Paso | Acción | Archivo afectado | Riesgo |
|---|---|---|---|
| B1 | Crear RPC descontar_stock_atomico |
SQL (producción) | BAJO — función nueva, no rompe nada existente |
| B2 | Modificar ModalAjustarStock.jsx para usar RPC |
src/ | MEDIO — cambio en UI |
| B3 | Modificar ModalIngresarMercancia.jsx para usar RPC |
src/ | MEDIO |
| B4 | Modificar ModalTienda.jsx para usar RPC (stock) |
src/ | MEDIO |
| B5 | Modificar useSalas.js agregarProducto para usar RPC |
src/ | ALTO — núcleo |
| B6 | Modificar useSalas.js agregarProductos para usar RPC |
src/ | ALTO — núcleo |
| B7 | Modificar useSalas.js editarSesionAdmin para usar RPC |
src/ | ALTO — núcleo |
| B8 | Modificar Ventas.jsx eliminar para usar RPC |
src/ | MEDIO |
| B9 | Modificar Ventas.jsx guardarEdicion para usar RPC |
src/ | MEDIO |
| B10 | Eliminar .catch(() => {}) en Ventas.jsx |
src/ | BAJO |
| B11 | Build + test | — | — |
| Paso | Acción | Archivo afectado | Riesgo |
|---|---|---|---|
| C1 | Crear RPC registrar_venta_pos |
SQL (producción) | BAJO — función nueva |
| C2 | Modificar ModalTienda.jsx para usar RPC completa |
src/ | ALTO — reemplaza lógica de venta |
| C3 | Build + test | — | — |
| Paso | Acción | Archivo afectado | Riesgo |
|---|---|---|---|
| D1 | Crear RPC finalizar_sesion |
SQL (producción) | BAJO — función nueva |
| D2 | Modificar useSalas.js finalizarSesion para usar RPC |
src/ | ALTO — núcleo |
| D3 | Build + test | — | — |
| Paso | Acción | Archivo afectado | Riesgo |
|---|---|---|---|
| E1 | Crear RPC anular_sesion |
SQL (producción) | BAJO — función nueva |
| E2 | Modificar useSalas.js anularSesion para usar RPC |
src/ | ALTO — núcleo |
| E3 | Build + test | — | — |
| Paso | Acción | Archivo afectado | Riesgo |
|---|---|---|---|
| F1 | Crear RPC guardar_cierre_turno |
SQL (producción) | BAJO — función nueva |
| F2 | Modificar CierreTurno.jsx para usar RPC |
src/ | MEDIO |
| F3 | Build + test | — | — |
| Paso | Acción | Archivo afectado | Riesgo |
|---|---|---|---|
| G1 | Documentar diseño de tabla pagos |
docs/ | — |
| G2 | NO implementar tabla pagos todavía |
— | — |
| Paso | Acción | Sprint |
|---|---|---|
| H1 | Triggers de auditoría | Sprint 0.3+ (después de estabilizar transacciones) |
Cada RPC es una función nueva (CREATE OR REPLACE FUNCTION). No modifica tablas ni datos existentes. El rollback es simplemente DROP FUNCTION.
| RPC | Rollback | Impacto |
|---|---|---|
descontar_stock_atomico |
DROP FUNCTION public.descontar_stock_atomico(UUID, INTEGER, TEXT, TEXT, TEXT, UUID); |
El código que la llama fallará con error. Revertir código a read-modify-write. |
registrar_venta_pos |
DROP FUNCTION public.registrar_venta_pos(...); |
ModalTienda vuelve a lógica anterior. |
finalizar_sesion |
DROP FUNCTION public.finalizar_sesion(...); |
useSalas vuelve a lógica anterior. |
anular_sesion |
DROP FUNCTION public.anular_sesion(...); |
useSalas vuelve a lógica anterior. |
guardar_cierre_turno |
DROP FUNCTION public.guardar_cierre_turno(...); |
CierreTurno vuelve a lógica anterior. |
Cada cambio en src/ se hace con el patrón:
Estrategia: usar git (cuando se inicialice) o mantener backups de archivos modificados.
No hay migración de datos en Sprint 0.2. Las RPCs operan sobre datos existentes sin modificar schema. No se eliminan columnas, no se renombran tablas, no se mueven datos.
Antes de ejecutar cualquier RPC en producción, debe existir un backup verificable.
NO ejecutar las RPCs. Esperar hasta que exista backup.
No se ejecutan probes INSERT/UPDATE/DELETE contra producción. Las pruebas se hacen en staging o base de datos de prueba.
| RPC | Test | Método |
|---|---|---|
descontar_stock_atomico |
Stock suficiente → OK | Staging: llamar con producto real, verificar stock baja |
descontar_stock_atomico |
Stock insuficiente → ERROR | Staging: llamar con cantidad > stock, verificar exception |
descontar_stock_atomico |
Concurrencia → OK | Staging: dos llamadas simultáneas, verificar stock final correcto |
registrar_venta_pos |
Venta normal → OK | Staging: registrar venta, verificar venta + items + stock |
registrar_venta_pos |
Stock insuficiente → ERROR + rollback | Staging: venta con producto sin stock, verificar que NO se crea venta ni se descuenta stock |
finalizar_sesion |
Finalizar → OK | Staging: finalizar sesión, verificar sesión + venta |
finalizar_sesion |
Doble finalización → ERROR | Staging: finalizar dos veces, verificar segunda falla |
anular_sesion |
Anular con productos → OK | Staging: anular sesión con productos, verificar stock devuelto |
anular_sesion |
Anular sin productos → OK | Staging: anular sesión sin productos, verificar no error |
guardar_cierre_turno |
Cierre normal → OK | Staging: guardar cierre, verificar cabecera + items |
guardar_cierre_turno |
Cierre duplicado → ERROR | Staging: guardar dos cierres mismo rango, verificar segundo falla |
| Check | Método |
|---|---|
| RPCs existen | SELECT proname FROM pg_proc WHERE proname IN ('descontar_stock_atomico','registrar_venta_pos','finalizar_sesion','anular_sesion','guardar_cierre_turno'); |
| Build PASS | npm run build |
| Login PASS | Probar login |
| Salas PASS | Abrir /salas, verificar carga |
| Ventas PASS | Abrir /ventas, verificar carga |
| Stock PASS | Abrir /stock, verificar carga |
| Cierre PASS | Abrir /cierre-turno, verificar carga |
| Item | Razón | Sprint |
|---|---|---|
Eliminar sesiones.finalizada |
23 dependencias en src/, requiere migración gradual | 0.3+ |
Crear tabla pagos |
Diseño only, no implementar todavía | 0.3 |
| Triggers de auditoría | Postergar hasta estabilizar transacciones | 0.3+ |
Refactor useSalas (1 suscripción) |
Pertenece al Sprint Motor de Sesiones | Sprint 2 |
Eliminar js/salas.js legacy |
No afecta build, limpieza cosmética | 0.3+ |
Unificar venta_items entre sesión y venta |
Requiere diseño adicional | 0.3 |
| Auth dual | Requiere decisión del propietario | 0.3+ |
Sprint 0.2 sólo puede considerarse APROBADO si:
✅ RPC descontar_stock_atomico creada y probada en staging
✅ RPC registrar_venta_pos creada y probada en staging
✅ RPC finalizar_sesion creada y probada en staging
✅ RPC anular_sesion creada y probada en staging
✅ RPC guardar_cierre_turno creada y probada en staging
✅ ModalAjustarStock usa RPC
✅ ModalIngresarMercancia usa RPC
✅ ModalTienda usa RPC completa
✅ useSalas.agregarProducto usa RPC
✅ useSalas.agregarProductos usa RPC
✅ useSalas.editarSesionAdmin usa RPC
✅ useSalas.finalizarSesion usa RPC
✅ useSalas.anularSesion usa RPC (con devolución de stock)
✅ Ventas.eliminar usa RPC
✅ Ventas.guardarEdicion usa RPC
✅ CierreTurno usa RPC
✅ .catch(() => {}) eliminado
✅ Build PASS
✅ Login PASS
✅ Salas PASS
✅ Ventas PASS
✅ Stock PASS
✅ Cierre PASS
✅ Rollback documentado por RPC
✅ Backup verificado antes de ejecutar
✅ No se eliminó finalizada
✅ No se creó tabla pagos
✅ No se hicieron migraciones destructivas
Detener inmediatamente si:
No iniciar ejecución sin autorización explícita por fase.