ADR-004: Resolución de conflictos en sincronización offline
Fecha: 2026-06-26
Estado: Aprobado
Responsable: Orchestrator
Contexto
El POS de Venuo opera sin internet (resiliencia offline). Las ventas generadas offline se encolan con estado PENDIENTE_SYNC y Celery las sincroniza al recuperar la conexión. Durante la desconexión, el estado del servidor puede haber cambiado — stock reducido, precios actualizados, caja cerrada — generando conflictos al sincronizar.
Se necesita una estrategia explícita de resolución para cada tipo de conflicto, que refleje la realidad operativa de un comercio de mostrador argentino.
Decisión
Estrategia C — Resolución por tipo de conflicto.
Cada tipo de conflicto tiene su propia regla de resolución definida en el backend. Las reglas son inmutables desde el código — ningún agente IA puede cambiarlas sin ADR de reemplazo.
Reglas de resolución por conflicto
Conflicto 1 — Stock insuficiente al sincronizar
Escenario: la venta offline requiere N unidades pero el stock actual en el servidor es menor a N.
Regla: la venta se acepta. El stock queda en valor negativo. Se genera alerta de stock negativo para el sucursal_admin.
Justificación: el cajero ya entregó el producto y cobró el dinero. Rechazar la venta no devuelve el producto ni deshace la transacción real — solo la deja sin registrar, creando un descuadre de caja silencioso. El negocio prefiere stock negativo visible y auditable sobre una venta perdida.
Estado final de la venta: SINCRONIZADA con flag conflicto_stock=True.
Conflicto 2 — Precio modificado durante la desconexión
Escenario: el administrador actualizó el precio de uno o más productos mientras el dispositivo estaba offline. La venta offline tiene precios distintos a los vigentes en el sistema al momento de sincronizar.
Regla: la venta se acepta con el precio offline. La diferencia de precio se registra como evento auditable visible para el empresa_admin.
Justificación: el cliente pagó ese precio en el mostrador. Modificarlo retroactivamente es incorrecto comercialmente y potencialmente ilegal. El empresa_admin recibe la información y decide si requiere acción (nota de crédito, ajuste, etc.).
Estado final de la venta: SINCRONIZADA con flag conflicto_precio=True y campo diferencia_precio registrado.
Conflicto 3 — Caja cerrada durante la desconexión
Escenario: el administrador cerró la caja manualmente mientras el dispositivo estaba offline. Las ventas intentan sincronizarse contra una caja en estado CERRADA o PENDIENTE_REVISION.
Regla: la venta se rechaza. Estado final: ANULADA. El sucursal_admin recibe notificación con el detalle de todas las ventas afectadas para registro manual.
Justificación: no es posible registrar ventas en una caja que no está ABIERTA. Es el único caso donde el rechazo automático es correcto — la operación de cierre de caja tiene precedencia sobre la sincronización. El admin debe reabrir la caja y registrar las ventas manualmente si corresponde.
Estado final de la venta: ANULADA con motivo CAJA_CERRADA_DURANTE_SYNC.
Alternativas consideradas
| Alternativa | Ventajas | Desventajas |
|---|---|---|
| Last Write Wins | Simple de implementar | Stock puede quedar negativo sin control, precios incorrectos aceptados sin auditoría |
| Server Wins | Simple, consistencia garantizada | Ventas rechazadas cuando el cajero ya entregó el producto — descuadres sin explicación |
| Por tipo de conflicto (elegida) | Refleja la realidad operativa del negocio | Más complejo de implementar — requiere lógica explícita por caso |
Implementación técnica requerida
Capa de servicio
Toda la lógica de resolución vive en app_caja/services.py — método sincronizar_venta_offline(venta_id).
def sincronizar_venta_offline(venta_id: int) -> dict:
"""
Retorna: {"estado": "SINCRONIZADA"|"ANULADA", "conflictos": [...]}
"""
# 1. Verificar estado de la caja
# 2. Verificar stock por cada línea de venta
# 3. Verificar precios vigentes
# 4. Aplicar reglas de resolución en orden
# 5. Persistir resultado y generar eventos auditables
Campos adicionales en el modelo Venta
conflicto_stock = models.BooleanField(default=False)
conflicto_precio = models.BooleanField(default=False)
diferencia_precio_total = models.DecimalField(null=True, blank=True)
motivo_anulacion = models.CharField(max_length=100, null=True, blank=True)
# Valor posible: "CAJA_CERRADA_DURANTE_SYNC"
Tarea Celery
app_caja/tasks.py — SincronizarVentasOfflineTask:
- Procesa la cola de ventas PENDIENTE_SYNC en orden cronológico.
- Llama a sincronizar_venta_offline() por cada venta.
- Registra resultado en log con nivel INFO (éxito) o ERROR (anulada).
- En caso de fallo técnico (no de negocio): reintenta hasta 3 veces con backoff exponencial.
Auditoría
Cada sincronización genera un evento auditable independientemente del resultado:
| Evento | Nivel de log | Visible para |
|---|---|---|
| Venta sincronizada sin conflictos | INFO |
empresa_admin, sucursal_admin |
| Venta sincronizada con conflicto de stock | WARNING |
sucursal_admin (alerta) |
| Venta sincronizada con conflicto de precio | WARNING |
empresa_admin (alerta) |
| Venta anulada por caja cerrada | ERROR |
sucursal_admin (notificación) |
Alcance de esta decisión
Esta ADR cubre únicamente la sincronización de ventas del dispositivo hacia el servidor (unidireccional).
La sincronización bidireccional avanzada — donde cambios del servidor (nuevos productos, cambios de precio) se propagan al dispositivo offline — está registrada en docs/BACKLOG.md como ítem de Fase 3 y requiere ADR propio al momento de implementarse.
Consecuencias
Positivas
- Las reglas están explícitas y documentadas — ningún agente puede interpretarlas de forma diferente.
- El negocio nunca pierde una venta silenciosamente — siempre hay un registro del resultado.
- Los conflictos son auditables y visibles para los roles correctos.
Negativas / trade-offs
- Stock negativo es un estado válido del sistema — requiere que las vistas de inventario lo muestren claramente como alerta.
- El
sucursal_adminpuede recibir notificaciones de ventas anuladas que requieren acción manual.
Documentos actualizados
docs/adr/ADR_INDEX.mddocs/WORKFLOW.md— reglas de conflicto agregadas a la sección de Ventadocs/ARCHITECTURE.md— eliminado de pendientes
Revisión futura
Revisar si el caso de stock negativo genera fricciones operativas recurrentes. Si ocurre frecuentemente, evaluar alertas preventivas que avisen al sucursal_admin cuando el stock baja de un umbral mientras hay dispositivos offline activos.