Skip to content

Guía de Idempotencia — Ventas (Cliente Java)

Guía de Idempotencia — Ventas (Cliente Java)

Concepto clave

Una idempotency_key es un UUID único por ciclo de vida del carrito. Si envías la misma key dos veces, el servidor retorna HTTP 409 (no duplica).

Enfoque actual: UUID por ciclo de vida del carrito (no SHA-256 determinístico).

Regla de oro: generar la key UNA vez al abrir la pantalla de ventas, reutilizarla en cada clic/reintento, y generar una nueva al limpiar el carrito.


Cómo funciona el ciclo de vida

VENTASVIEWCONTROLLER
├─ initialize() / initCartPersistence()
│ └─ saleUuid = UUID.randomUUID() ← SE GENERA UNA VEZ
│ Ejemplo: "550e8400-e29b-41d4-a716-446655440000"
├─ handleCobrar() ← clic 1
│ ├─ venta.setSaleUuid(saleUuid) ← USA LA MISMA KEY
│ └─ POST /api/ventas/ { idempotency_key: "550e8400-..." }
├─ handleCobrar() ← clic 2 (mientras procesa)
│ └─ isProcessing = true → return ← BLOQUEADO
├─ handleCobrar() ← clic después de timeout
│ └─ venta.setSaleUuid(saleUuid) ← MISMA KEY QUE ANTES
│ └─ POST /api/ventas/ { idempotency_key: "550e8400-..." }
└─ handleCancelar() / ventaExitosa()
└─ inicializarValoresPorDefecto()
└─ saleUuid = UUID.randomUUID() ← NUEVA KEY PARA PRÓXIMA VENTA

Implementación paso a paso

  1. Campo saleUuid en VentaGS.java

    La modelo de datos almacena la key del ciclo de vida.

    public class VentaGS {
    // ... otros campos ...
    private String saleUuid;
    public String getSaleUuid() { return saleUuid; }
    public void setSaleUuid(String saleUuid) { this.saleUuid = saleUuid; }
    }
  2. Generar key al iniciar la pantalla de ventas

    En VentasViewController.java, generar la key una vez al abrir la vista.

    private volatile boolean isProcessing = false;
    private String saleUuid;
    private void initCartPersistence() {
    VentaStateManager state = VentaStateManager.getInstancia();
    if (state.hasItems()) {
    cartItems = FXCollections.observableArrayList(state.getItems());
    } else {
    cartItems = FXCollections.observableArrayList();
    }
    saleUuid = UUID.randomUUID().toString(); // ← UNA VEZ
    }
  3. Reutilizar la key en cada clic de “Cobrar”

    private void handleCobrar() {
    if (isProcessing) {
    log.warn("Clic ignorado: venta ya está en proceso");
    return;
    }
    isProcessing = true;
    // ... validaciones ...
    AsyncTaskManager.getInstance().executeIO(() -> {
    try {
    VentaGS venta = new VentaGS();
    // ... setear campos ...
    venta.setSaleUuid(saleUuid); // ← MISMA KEY
    VentasService.getInstance().procesarVentaLocal(venta, ...);
    } finally {
    isProcessing = false;
    }
    });
    }
  4. Enviar la key en el POST al backend

    En VentaGatewayAPI.java:

    String idempotencyKey = venta.getSaleUuid() != null
    ? venta.getSaleUuid()
    : IdempotencyUtils.generarKeyUnica(); // fallback UUID
    body.put("idempotency_key", idempotencyKey);
  5. Generar key nueva al limpiar el carrito

    private void inicializarValoresPorDefecto() {
    cartItems.clear();
    saleUuid = UUID.randomUUID().toString(); // ← NUEVA KEY
    // ... resetear UI ...
    }
  6. Manejar las respuestas del servidor

    public void procesarRespuestaVenta(Response response, VentaGS venta) {
    int code = response.getStatusCode();
    if (code == 201) {
    // Venta creada exitosamente
    JSONObject data = new JSONObject(response.getBody());
    int idVenta = data.getInt("id");
    log.info("Venta exitosa ID={}", idVenta);
    } else if (code == 409) {
    // La venta ya existe → marcar como sincronizada
    JSONObject data = new JSONObject(response.getBody());
    int idExistente = data.getJSONObject("detail").getInt("venta_id");
    log.info("Venta ya existe ID={}, marcada como sincronizada", idExistente);
    } else if (code == 400) {
    // Datos inválidos → log y NO reintentar
    log.error("Venta rechazada (400): {}", response.getBody());
    } else {
    // 500, 503, timeout → reintentar con la MISMA key
    log.warn("Venta falló ({}), reintentando", code);
    retryQueue.add(venta);
    }
    }

Triple protección contra duplicados

CapaMecanismoCuándo actúa
UIvolatile boolean isProcessingBloquea doble clic instantáneo (nunca llega al backend)
Redidempotency_key (UUID por carrito)Detecta retries con la misma key → HTTP 409
BDUNIQUE (tenant_id, idempotency_key)Última línea de defensa contra race conditions

Matriz de decisiones del cliente

Estado HTTPSignificadoAcción
201 CreatedVenta creada OKMostrar toast éxito, limpiar carrito (genera key nueva)
409 ConflictVenta ya existe (duplicado)Tratar como éxito, mostrar ID existente
400 Bad RequestDatos inválidosLog error, NO reintentar (es bug del cliente)
500 / 503 / timeoutError del servidorReintentar con la MISMA key
Cualquier otroError inesperadoLog, reintentar con misma key

Errores comunes


Compatibilidad

BackendClienteResultado
Nuevo (acepta key del cliente)Sin keySEGURO — backend genera UUID4 fallback
NuevoCon key (esta guía)SEGURO — triple protección
Obligatorio (Fase 4)Sin keyHTTP 400 — fuerza actualización del cliente

Archivos involucrados

ArchivoCambio
VentaGS.javaCampo saleUuid con getter/setter
VentasViewController.javaGenera saleUuid en initCartPersistence() y inicializarValoresPorDefecto(). Flag isProcessing
VentaGatewayAPI.javaUsa venta.getSaleUuid() como idempotency_key
VentaLocalRepository.javaUsa venta.getSaleUuid() en outbox payload
IdempotencyUtils.javaMétodo generarKeyUnica() como fallback