Saltar a contenido

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.pySincronizarVentasOfflineTask: - 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_admin puede recibir notificaciones de ventas anuladas que requieren acción manual.

Documentos actualizados

  • docs/adr/ADR_INDEX.md
  • docs/WORKFLOW.md — reglas de conflicto agregadas a la sección de Venta
  • docs/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.