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 VENTAImplementación paso a paso
-
Campo
saleUuidenVentaGS.javaLa 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; }} -
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} -
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 KEYVentasService.getInstance().procesarVentaLocal(venta, ...);} finally {isProcessing = false;}});} -
Enviar la key en el POST al backend
En
VentaGatewayAPI.java:String idempotencyKey = venta.getSaleUuid() != null? venta.getSaleUuid(): IdempotencyUtils.generarKeyUnica(); // fallback UUIDbody.put("idempotency_key", idempotencyKey); -
Generar key nueva al limpiar el carrito
private void inicializarValoresPorDefecto() {cartItems.clear();saleUuid = UUID.randomUUID().toString(); // ← NUEVA KEY// ... resetear UI ...} -
Manejar las respuestas del servidor
public void procesarRespuestaVenta(Response response, VentaGS venta) {int code = response.getStatusCode();if (code == 201) {// Venta creada exitosamenteJSONObject 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 sincronizadaJSONObject 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 reintentarlog.error("Venta rechazada (400): {}", response.getBody());} else {// 500, 503, timeout → reintentar con la MISMA keylog.warn("Venta falló ({}), reintentando", code);retryQueue.add(venta);}}
Triple protección contra duplicados
| Capa | Mecanismo | Cuándo actúa |
|---|---|---|
| UI | volatile boolean isProcessing | Bloquea doble clic instantáneo (nunca llega al backend) |
| Red | idempotency_key (UUID por carrito) | Detecta retries con la misma key → HTTP 409 |
| BD | UNIQUE (tenant_id, idempotency_key) | Última línea de defensa contra race conditions |
Matriz de decisiones del cliente
| Estado HTTP | Significado | Acción |
|---|---|---|
201 Created | Venta creada OK | Mostrar toast éxito, limpiar carrito (genera key nueva) |
409 Conflict | Venta ya existe (duplicado) | Tratar como éxito, mostrar ID existente |
400 Bad Request | Datos inválidos | Log error, NO reintentar (es bug del cliente) |
500 / 503 / timeout | Error del servidor | Reintentar con la MISMA key |
| Cualquier otro | Error inesperado | Log, reintentar con misma key |
Errores comunes
Compatibilidad
| Backend | Cliente | Resultado |
|---|---|---|
| Nuevo (acepta key del cliente) | Sin key | SEGURO — backend genera UUID4 fallback |
| Nuevo | Con key (esta guía) | SEGURO — triple protección |
| Obligatorio (Fase 4) | Sin key | HTTP 400 — fuerza actualización del cliente |
Archivos involucrados
| Archivo | Cambio |
|---|---|
VentaGS.java | Campo saleUuid con getter/setter |
VentasViewController.java | Genera saleUuid en initCartPersistence() y inicializarValoresPorDefecto(). Flag isProcessing |
VentaGatewayAPI.java | Usa venta.getSaleUuid() como idempotency_key |
VentaLocalRepository.java | Usa venta.getSaleUuid() en outbox payload |
IdempotencyUtils.java | Método generarKeyUnica() como fallback |