# Предварительная бронь и оплата через ваш шлюз

Контракт API 1.1.0. Перед подключением проверьте, что обновление и миграция
`m261029_160000_calendar_reservations` установлены на сервере CRM.
Эта инструкция не подтверждает публикацию обновления на конкретном сервере.

Ваш сервер принимает деньги через собственный шлюз. LASERWAR CRM резервирует
площадку и время, показывает заявку в календаре и учитывает подтверждённую вашим
сервером оплату. Подключать банк в разделе онлайн-оплаты CRM для этого режима не нужно.

## Подготовка владельцем клуба

1. Откройте **Настройки → API календаря** на поддомене своего клуба.
2. Проверьте рабочие часы, перерывы, шаг календаря, площадки и часовой пояс.
3. Включите API. Создайте отдельный ключ с правом **«Разрешить бронь,
   подтверждение внешней оплаты и отмену»**. Старые ключи этого права не получают.
4. Для учёта оплаты подготовьте активную кассу услуг. Если в настройках
   онлайн-оплаты выбрана касса, используется она; иначе — первая активная касса услуг.
   Включать онлайн-эквайринг CRM не требуется.
5. Разместите ключ только в защищённых настройках вашего backend. Разработчику
   передайте адрес клуба и [OpenAPI](calendar-availability.openapi.json).

Базовый адрес: `https://<ваш-клуб>.laserwar.club/api/v1`.
Каждый запрос: HTTPS и `Authorization: Bearer <ключ>`.
JSON-запросы: `Content-Type: application/json`. Общий лимит —
120 запросов в минуту на ключ, тело до 16 KiB; при `429` учитывайте `Retry-After`.
API не использует cookies или пароль входа в CRM.
Ключ с этим правом может учитывать оплату: его получает только доверенный сервер,
который проверяет подлинность уведомлений и статус транзакции в вашем шлюзе.

## Последовательность

1. Получите площадки: `GET /availability/venues`.
2. Покажите свободные интервалы: `GET /availability`.
3. До начала оплаты создайте `POST /reservations` и сохраните `data.id` и
   `data.order_id` в своём заказе. Только успешное создание брони занимает место.
4. Создайте платёж в вашем шлюзе. На своём сервере надёжно свяжите его с этой бронью.
5. После окончательной успешной оплаты, проверенной вашим сервером, вызовите
   `POST /reservations/{id}/confirm-payment`.
6. Если гость отказался, убедитесь, что у шлюза нет транзакции, которая ещё может
   успешно завершиться, и вызовите `DELETE /reservations/{id}`.

Проверка доступности сама по себе не занимает слот. При `409 slot_unavailable`
обновите календарь и предложите другое время; оплату для этой попытки не запускайте.
Если площадка допускает две игры одновременно, одна бронь занимает одно место.
Рабочие часы, вся длительность, перерывы и лимиты гостей учитываются общим календарём.
API не обещает свободного инструктора и ещё не назначенное оборудование.

## Создание брони

`POST /reservations`. Обязателен заголовок
`Idempotency-Key: checkout-unique-attempt-001`: 8–128 латинских букв, цифр,
`_`, `.`, `:`, `-`. Один ключ соответствует одной неизменной попытке.

```json
{
  "date": "YYYY-MM-DD",
  "time": "14:00",
  "venue_id": 123,
  "duration_minutes": 90,
  "players": 12,
  "amount_rubles": 1000,
  "total_rubles": 5000,
  "customer": {
    "name": "Имя гостя",
    "phone": "+79990000000",
    "email": "guest@example.test"
  },
  "comment": "День рождения"
}
```

Дата, площадка, контакты и суммы — образцы, замените их своими значениями.
Дата и время — в часовом поясе клуба. Сумму рассчитывает ваш сервер,
не браузер гостя. Поддерживаются целые рубли RUB от 1 до 10 000 000.
`amount_rubles` — один ожидаемый платёж, например предоплата.
`total_rubles` — полная стоимость, не меньше ожидаемой оплаты; по умолчанию равна ей.
Длительность — 5–1440 минут, число гостей — 1–10 000 с учётом лимита площадки.
`venue_id` можно опустить или передать 0: CRM выберет свободную площадку и вернёт её ID.
`game_type_id` необязателен: активный тип игры клуба, без автоматического выбора арены.
Имя и телефон обязательны, email и комментарий необязательны.
Имя — простой текст без HTML; HTML в комментарии удаляется.
Не передавайте `account`, `status`, цвет или срок истечения: неизвестные поля отклоняются.

Успешный ответ HTTP 200:

```json
{
  "success": true,
  "data": {
    "id": "0123456789abcdef0123456789abcdef",
    "state": "pending",
    "order_id": 456,
    "venue_id": 123,
    "start": "2026-11-15T14:00:00+03:00",
    "end": "2026-11-15T15:30:00+03:00",
    "timezone": "Europe/Moscow",
    "amount_rubles": 1000,
    "total_rubles": 5000,
    "expires_at": null,
    "cash_id": null,
    "confirmed_at": null,
    "cancelled_at": null
  }
}
```

Сразу появляется обычная CRM-заявка со статусом **«Предварительная бронь»**.
Цвет по умолчанию — янтарный (`#b45309`), в том числе при окраске календаря
по инструктору или площадке. Владелец может изменить название и цвет статуса
в настройках статусов заявок. Статус должен оставаться включённым и занимать место.
При создании брони деньги в кассу не записываются, новый клиент создаётся
без подписки на маркетинговые рассылки. Письма, SMS и реальные платежи этот метод не отправляет.

До подтверждения или отмены слот, клиент, стоимость и платёжные поля управляются
API. Перенос, ручная смена статуса и удаление ожидающей заявки через обычные формы
блокируются, чтобы внешняя оплата не завершилась для уже изменённой брони.

При потере ответа повторите создание с прежним телом и ключом идемпотентности.
Вернутся прежние ID и текущее состояние. Изменённое тело с тем же ключом —
`409 idempotency_conflict`. Повтор после отмены возвращает отменённую бронь;
для нового бронирования нужен новый ключ.

## Подтверждение оплаты

`POST /reservations/{id}/confirm-payment`:

```json
{
  "gateway": "your-gateway",
  "payment_id": "provider-payment-123",
  "amount_rubles": 1000,
  "currency": "RUB"
}
```

`gateway` — постоянный код вашего провайдера: 1–32 символа `a-z`, цифры,
`_`, `-`. `payment_id` — ID окончательно успешной транзакции в нём:
1–128 латинских букв, цифр, `_`, `.`, `:`, `-`.
Передавайте один и тот же код шлюза для всех его транзакций.
Сумма должна точно совпасть с `amount_rubles` при создании брони.
В этой версии поддерживается одно подтверждение одного согласованного платежа.
Доплата и возврат — отдельные процессы; этот метод не принимает второй платёж.

CRM доверяет вашему авторизованному серверу и **не запрашивает ваш шлюз**.
Ваш backend обязан проверить подпись уведомления, принадлежность платежа своему
заказу, сумму, валюту и окончательный статус оплаты. При необходимости запросите
статус непосредственно у провайдера. Возврат браузера на страницу успеха и
авторизация/холд без окончательного списания не являются подтверждением оплаты.
Открытый callback от гостя не должен иметь возможность вызывать этот метод.

В одной транзакции CRM создаёт ровно один приход по карте в кассе услуг,
заполняет фактическую предоплату и меняет состояние на `confirmed`.
Заявка получает статус **«Предоплата (внешний шлюз)»** либо
**«Оплачено (внешний шлюз)»**, если платёж покрывает всю стоимость.
Время продолжает занимать та же заявка.
Ответ имеет тот же формат, с заполненными `cash_id` и `confirmed_at`.

Повтор с теми же шлюзом, ID и суммой возвращает прежний результат без нового прихода.
Тот же платёж нельзя использовать для другой брони этого клуба, даже другим ключом.
Сохраняйте ID транзакции у себя до первого запроса. При тайм-ауте или `503`
повторяйте **тот же** запрос; не создавайте новый платёж и новую бронь.
Сначала можно прочитать состояние через `GET /reservations/{id}`.

## Отмена и восстановление после сбоя

`DELETE /reservations/{id}` отменяет неоплаченную бронь:
состояние `cancelled`, заявка **«Бронь отменена»**, место свободно.
История заявки сохраняется. Повторная отмена возвращает тот же результат.
Оплаченная бронь возвращает `409 payment_already_confirmed`;
сначала нужен отдельный процесс возврата и сверки, который этим API не выполняется.

Бронь **не истекает автоматически**: `expires_at: null`.
Таймер в приложении сам по себе не отменяет её. Ваш сервер сначала должен получить
окончательную отмену/отсутствие активного платежа в шлюзе и затем отменить бронь в CRM.
Если результат шлюза неизвестен, сохраняйте бронь и передавайте ситуацию оператору.
При одновременной отмене и подтверждении CRM выполняет только один переход.
Поздний платёж для `cancelled` получит `409 reservation_cancelled` и не восстановит
бронь; потребуется ручная сверка и решение о возврате на стороне шлюза.

Читать, подтверждать и отменять бронь может только создавший её ключ.
Отзыв ключа не отменяет брони. Владелец видит ожидающие брони в
**Настройки → API календаря → Предварительные брони** и может отменить их там
после проверки шлюза, включая случай отозванного ключа. Автоматического переноса
броней на новый ключ нет: завершите активные операции до ротации.
Часовой пояс нельзя менять, пока есть ожидающие внешние брони.

## Основные ошибки

Ответ: `{"success":false,"error":{"code":"...","message":"...","details":[]}}`.
Используйте стабильный `code`, не текст сообщения.

| HTTP | Код | Действие |
| --- | --- | --- |
| 401 | `invalid_api_key` | Проверить ключ и отзыв доступа |
| 403 | `scope_required` / `wrong_club_host` / `api_disabled` | Проверить отдельное право, адрес клуба и включение API |
| 404 | `reservation_not_found` | Проверить ID и создавший ключ |
| 409 | `slot_unavailable` | Выбрать другой интервал до оплаты |
| 409 | `idempotency_conflict` | Не переиспользовать ключ для другого тела |
| 409 | `payment_amount_mismatch` / `payment_conflict` / `payment_already_used` | Сверить сумму, транзакцию и связь с бронью |
| 409 | `reservation_cancelled` / `payment_already_confirmed` | Не повторять противоположный переход; провести сверку |
| 409 | `payment_register_required` / `reservation_status_required` / `reservation_changed` | Владелец проверяет кассу, статусы или изменения заявки |
| 422 | `forbidden_parameter` / `invalid_customer` / `invalid_payment_reference` / `payment_currency_mismatch` | Исправить тело запроса |
| 429 | `rate_limit` | Подождать `Retry-After`, затем повторить |
| 503 | `migration_required` / `payment_accounting_required` | Проверить обновление или повторить тот же запрос после восстановления |

Перед запуском проверьте на демо: создание и видимость заявки, занятый слот,
повторные запросы, частичную и полную оплату, отмену, позднее уведомление,
неверную сумму, отсутствие кассы и конкурирующие запросы.
Реальные платежи и сообщения для проверки не нужны.
