# Идемпотентность

Как безопасно повторять покупку после таймаута или обрыва связи.

Если ответ на `POST /purchase` не дошёл, вы не знаете, прошла ли покупка. Повтор без защиты — это вторая покупка и второе списание. Поэтому передавайте `idempotencyKey` — свой уникальный идентификатор покупки, например ID заказа в вашей системе.

- Первый запрос с ключом создаёт заказ.
- Повтор с тем же ключом и тем же товаром возвращает исход того же заказа — с тем же кодом ответа и полем `"replayed": true`. Деньги второй раз не списываются, даже если товар уже закончился.
- Тот же ключ с другим товаром — `422 idempotency_key_reused`.
- Если заказ не был создан (например, `402` — не хватило денег), ключ не занят: после пополнения можно повторить с ним же.

## Формат

1–128 печатных ASCII-символов (`!`…`~`), без пробелов и кириллицы. Регистр важен: `ABC` и `abc` — разные покупки. Ключ действует в пределах вашего клиента.

## Повтор с тем же ключом

```javascript
async function buy(productId, idempotencyKey) {
  for (let attempt = 1; attempt <= 5; attempt++) {
    try {
      const res = await fetch(`https://merchant.dbdlobby.com/purchase/${productId}`, {
        method: "POST",
        headers: { "X-Entity-Key": KEY, "Content-Type": "application/json" },
        body: JSON.stringify({ idempotencyKey }), // один и тот же ключ во всех попытках
        signal: AbortSignal.timeout(90_000),
      });
      if (res.status >= 500 && res.status !== 502) throw new Error(`HTTP ${res.status}`);
      return { status: res.status, body: await res.json() };
    } catch (e) {
      // Сеть, таймаут или 5xx: исход неизвестен — повторяем С ТЕМ ЖЕ ключом.
      await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
    }
  }
  throw new Error("API недоступен; повторите позже с тем же idempotencyKey");
}
```

Ключ необязателен, но без него повтор после таймаута создаст вторую покупку. В будущем он может стать обязательным.
