# Idempotencia

> Una clave por reserva para poder reintentar sin crear dos turnos ni dos cobros.

`POST /api/v1/reservas` exige la cabecera `Idempotency-Key`. Si la conexión se corta y no sabés si la reserva se creó, repetí el pedido con **la misma clave y el mismo cuerpo**: vas a recibir la misma reserva, sin otra fila, sin otro mail y sin otro link de pago.

| Caso | Respuesta |
| --- | --- |
| Clave nueva | `201` con la reserva y `operacion.repetida: false`. |
| Misma clave, mismo cuerpo | `200` con la misma reserva y `operacion.repetida: true`. |
| Misma clave, otro cuerpo | `409 CONFLICTO_INTENCION`. No se crea nada. |
| Dos pedidos a la vez con la misma clave | Se crea una sola reserva; los dos reciben el mismo resultado. |
| Sin la cabecera | `400 IDEMPOTENCY_KEY_FALTANTE`, sin escribir nada. |

## Cómo elegir la clave

- Entre 8 y 100 caracteres: letras, números, punto, guion, guion bajo o dos puntos.
- Generala una vez por intento de reserva de la clienta (por ejemplo, al abrir el paso de confirmación) y guardala hasta tener respuesta. Un UUID sirve.
- Si la clienta cambia el horario o los datos, es otra reserva: usá otra clave.
- La clave es del negocio, no de la credencial: si rotás la clave de la API, el reintento con la nueva recupera la reserva hecha con la anterior.

```js
// Una clave por intento de reserva, reutilizada en cada reintento.
const idem = sessionStorage.getItem("reservaIdem") ?? crypto.randomUUID();
sessionStorage.setItem("reservaIdem", idem);

async function reservar(cuerpo, intentos = 3) {
  for (let i = 0; i < intentos; i++) {
    try {
      const res = await fetch("https://turnovapp.com/api/v1/reservas", {
        method: "POST",
        headers: {
          Authorization: "Bearer tnv_pk_TU_CLAVE_PUBLICA",
          "Content-Type": "application/json",
          "Idempotency-Key": idem,
        },
        body: JSON.stringify(cuerpo),
      });
      const r = await res.json();
      if (r.ok || !r.error.reintentable) return r;
      const espera = Number(res.headers.get("Retry-After") ?? 2);
      await new Promise((ok) => setTimeout(ok, espera * 1000));
    } catch {
      // Red caída: reintentar con la MISMA clave es seguro.
    }
  }
  throw new Error("No pudimos confirmar la reserva");
}
```

Las lecturas (`GET`) no llevan clave de idempotencia: se pueden repetir libremente.
