# Подключение календаря LASERWAR CRM: инструкция клиенту и задание для ИИ

Этот документ можно передать разработчику или прикрепить к чату с ИИ, который пишет ваше приложение. Он описывает действующий API свободных дат и времени, включая совместимость с календарём заявок и сборными играми.

## 1. Что должен подготовить владелец клуба

1. Откройте свою CRM: `https://<ваш-клуб>.laserwar.club/settings/calendar-api/index` — раздел **API календаря** в настройках. Настройки доступны владельцу клуба.
2. Проверьте рабочие дни, часы, перерывы и шаг календаря в общих настройках CRM. В API выберите часовой пояс и при необходимости задайте максимальное число гостей для площадок.
3. Включите API и создайте отдельный ключ для вашего приложения. Для первого этапа достаточно ключа **«Только доступность»**; разрешение на удержания и платёжные ссылки требуется только для оплаты через CRM.
4. Сохраните ключ: полностью он показывается один раз. Разместите его в секретных настройках сервера приложения. При утрате создайте новый ключ, старый отзовите.
5. Передайте разработчику этот документ, адрес своего клуба и OpenAPI-файл. Ключ передайте отдельно безопасным способом; пароль входа в CRM для интеграции не нужен.

Открыть документацию после входа владельцем:

- инструкция: `https://<ваш-клуб>.laserwar.club/settings/calendar-api/docs`;
- скачать OpenAPI: `https://<ваш-клуб>.laserwar.club/settings/calendar-api/openapi`.

Эти страницы документации требуют входа в CRM. Если ИИ не может их открыть, скачайте OpenAPI и прикрепите файл к его чату. Сам API работает по интеграционному ключу, без cookies входа в CRM.

Для тестов LASERWAR используется `https://lasertag-test.laserwar.club`. Тестовый ключ создаётся отдельно: ключ вашего клуба не работает на другом поддомене. Временный ключ проверки от 22.10.2026 отозван.

## 2. Что сообщить ИИ вместе с этим документом

Заполните несекретные значения:

```text
Адрес нашего клуба: https://<ваш-клуб>.laserwar.club
Режим: только получение доступности / удержание и оплата через CRM
Где нужно подключение: <наше приложение, сайт или микросервис>
Длительность мероприятий: <например, 60, 90, 120 минут>
Число гостей: <откуда берётся и может ли меняться>
Соответствие услуги и площадки: <наша услуга -> ID площадок из API>
Оплата сейчас принимается: <нашей системой / через CRM / пока не нужна>
OpenAPI-файл: <приложен к сообщению>
API-ключ: будет размещён владельцем в секретах backend, в это сообщение не вставлять.
```

Если вы пока подключаете только выбор свободного времени, выберите **«только получение доступности»**. Модуль сохранит существующий способ оплаты. Если обязательно удерживать место на всё время оплаты, согласуйте переход на платёжную цепочку CRM.

Далее — готовое техническое задание. Его можно целиком передать ИИ вместе с исходным кодом вашего приложения.

## 3. Задание ИИ-разработчику

Ты подключаешь существующее приложение к API календаря LASERWAR CRM. Реализуй работающий модуль в текущем проекте, его интерфейс выбора даты/времени и необходимые тесты. Сначала изучи стек, существующие формы, backend и способ приёма предоплаты. Используй принятые в проекте библиотеки и архитектуру.

Задача: гость должен видеть подходящие свободные дни и интервалы из календаря своего клуба и не выбирать время, которое уже занято. Существующие заявки и сборные игры ведутся в CRM; приложение читает их совокупное влияние на доступность через API.

Если режим интеграции не указан, реализуй сначала только чтение доступности и повторную проверку интервала. Платёжный маршрут существующего приложения автоматически не меняй. Отсутствующие адрес и секрет вынеси в настройки, чтобы владелец мог заполнить их после реализации.

### 3.1. Настройки и архитектура

Добавь конфигурацию backend:

```dotenv
LASERWAR_CALENDAR_API_BASE_URL=https://<ваш-клуб>.laserwar.club/api/v1
LASERWAR_CALENDAR_API_KEY=
```

Пустой ключ в `.env.example` — образец, настоящий ключ туда не записывать. Значения загружаются из существующего хранилища секретов проекта. При отсутствующей конфигурации показывай понятное состояние «Подключение календаря не настроено».

Цепочка запросов: интерфейс гостя → backend нашего приложения → API CRM. Backend добавляет:

```http
Authorization: Bearer <интеграционный ключ>
Accept: application/json
```

Для JSON-запросов также нужен `Content-Type: application/json`. Все обращения — по HTTPS. Ключ нельзя передавать в URL, возвращать браузеру, включать в JavaScript, APK/IPA, Git, открытые логи и сообщения об ошибках. Cookies и пароль пользователя CRM не используются для API. Базовый адрес фиксируется в доверенной конфигурации, а не принимается из формы гостя.

Сделай отдельный API-клиент с методами получения площадок, получения доступности и проверки интервала. Используй конечный тайм-аут запроса, например 10 секунд. Не создавай отдельный сервер, если backend уже существует. Если проект состоит только из публичного frontend, добавь серверную часть или serverless-обработчики в принятом для проекта окружении; секрет в браузер не переносить.

Первый диагностический запрос из окружения backend с уже заданными переменными:

```bash
curl --silent --show-error --max-time 10 \
  "$LASERWAR_CALENDAR_API_BASE_URL/availability/venues" \
  -H "Authorization: Bearer $LASERWAR_CALENDAR_API_KEY" \
  -H "Accept: application/json"
```

Ожидается HTTP 200 и `success: true` с площадками именно нашего клуба. Не используй отладочный вывод заголовков с настоящим ключом в публичных логах.

Не подключайся к БД CRM и не меняй её код для этой интеграции. Не придумывай дополнительные маршруты CRM. Приложенный OpenAPI — машинный контракт; если конкретное поле не описано, проверь контракт и согласуй расширение вместо имитации ответа.

### 3.2. Действующие маршруты

Все пути ниже добавляются к `LASERWAR_CALENDAR_API_BASE_URL`.

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/availability/venues` | Площадки клуба |
| GET | `/availability` | Свободные дни и интервалы |
| POST | `/availability/check` | Повторная проверка выбранного интервала |
| POST | `/prepayment-intents` | Создание удержания, если разрешены предоплаты |
| GET | `/prepayment-intents/{id}` | Состояние своей попытки оплаты |
| DELETE | `/prepayment-intents/{id}` | Отмена удержания до запуска оплаты |
| POST | `/prepayment-intents/{id}/payment` | Создание платёжной ссылки через CRM |
| POST | `/prepayment-intents/{id}/sync` | Проверка состояния платежа CRM в банке |

Для режима «только доступность» используй первые три маршрута.

Успех: `{"success":true,"data":{...}}`.

Ошибка: `{"success":false,"error":{"code":"slot_unavailable","message":"...","details":[]}}`.

Обрабатывай HTTP-статус и стабильный `error.code`; не определяй логику по английскому тексту `message`.

### 3.3. Площадки и соответствие услугам

Сначала вызови `GET /availability/venues`. Ответ содержит `data.timezone` и `data.venues`. У площадки есть `id`, `name_ru`, `name_en`, `parallel_capacity`, `player_limit`.

- `parallel_capacity` — сколько мероприятий можно проводить одновременно: 1 или 2. Это не число игроков.
- `player_limit` — настроенный владельцем максимум гостей одной группы. `null` означает отсутствие заданного лимита, а не отсутствие свободного места.
- ID нужно получать из ответа. Не зашивай тестовые ID и названия площадок в код.
- Соответствие «услуга нашего приложения → допустимые площадки CRM» должно быть явной настройкой владельца. Нельзя самостоятельно решить, что площадка VR подходит для банкета.
- Необязательный `game_type_id` проверяет существование активного типа игры и используется при создании заявки. Сам по себе он не фильтрует площадки по пригодности для услуги.

### 3.4. Запрос свободного времени

`GET /availability` принимает:

| Параметр | Правило |
| --- | --- |
| `from` | Обязательная дата `YYYY-MM-DD` |
| `to` | Последняя дата включительно; без неё запрашивается один день |
| `venue_id` | ID конкретной площадки; 0 или отсутствие — любая активная площадка |
| `duration_minutes` | Полная длительность мероприятия, целое число от 5 до 1440; по умолчанию 60 |
| `players` | Число гостей одной группы, целое от 1 до 10000; по умолчанию 1 |
| `game_type_id` | Необязательный активный тип игры этого клуба |

Передавай фактическую длительность и число гостей из нашей формы, чтобы не проверять час вместо двухчасового мероприятия. Один запрос охватывает 1–31 день в пределах ближайших 365 дней. Большие диапазоны дели на запросы с учётом лимита API.

Пример HTTP-запроса — дата и ID здесь образцы, а не сведения о свободном времени клуба:

```http
GET /api/v1/availability?from=2026-12-20&to=2026-12-20&venue_id=123&duration_minutes=90&players=12
Authorization: Bearer <API_KEY>
Accept: application/json
```

Из `data` используй:

- `timezone` — IANA-имя часового пояса клуба;
- `days[]`: `date`, `available`, `slot_count`;
- `slots[]`: `venue_id`, `date`, `time`, `start`, `end`, `starts_at`, `ends_at`, `remaining_capacity`, `player_limit`;
- `advisory` — список отражает доступность на момент запроса и не удерживает место.

`start`/`end` имеют формат ISO 8601 со смещением UTC; `starts_at`/`ends_at` — Unix timestamp. `date` и `time` передаются в часовом поясе клуба, даже если устройство гостя находится в другом поясе.

`slot_count` — число подходящих строк слотов при заданных фильтрах, а не количество свободных столов, игроков или мероприятий. `remaining_capacity` — оставшаяся одновременная вместимость площадки в мероприятиях.

### 3.5. Поведение интерфейса гостя

1. После выбора услуги, длительности и числа гостей запроси нужный диапазон дат для разрешённых площадок.
2. Дни с `available: false` отключи. Для доступного дня покажи только интервалы из `slots`; не генерируй часы самостоятельно.
3. При выборе сохрани точные `venue_id`, `date`, `time`, длительность и число гостей. Если в выдаче была «любая площадка», сохрани ID конкретного выбранного слота.
4. При изменении услуги, площадки, длительности или числа гостей сбрось прежний выбор и обнови доступность. При смене месяца запрашивай новый диапазон.
5. Отменяй устаревшие запросы или игнорируй их ответы, чтобы медленный ответ старого фильтра не заменил актуальный календарь.
6. Пока загрузка не завершена или CRM недоступна, не показывай неизвестные интервалы как свободные. Дай гостю сообщение и кнопку повторной загрузки.
7. Непосредственно перед оформлением предоплаты выполни `POST /availability/check`. При `available: false` останови переход к оплате, обнови календарь и предложи другое время.

Пример JSON для повторной проверки; дата и ID заменяются реальным выбором:

```json
{
  "date": "2026-12-20",
  "time": "14:00",
  "venue_id": 123,
  "duration_minutes": 90,
  "players": 12
}
```

При успехе прочитай `data.available` и `data.slot`; при недоступности — `data.reason` (`no_capacity` или `outside_working_time`). Этот запрос ничего не создаёт и не удерживает.

CRM уже учитывает рабочие дни/часы, перерывы, полную длительность, занятые заявки, сборные игры, их площадки, отмены и активные удержания оплаты. Параллельная загрузка рассчитывается по одновременной вместимости. Не воспроизводи эти вычисления во втором календаре приложения.

Ответ не гарантирует свободного инструктора, выдачу ещё не назначенного оборудования и соответствие площадки услуге. Эти условия требуют отдельных настроек бизнеса.

### 3.6. Если оплата остаётся в нашей системе

Сохрани действующий эквайринг. Используй API для отображения свободного времени и проверки перед переходом в наш платёжный процесс.

Объясни владельцу ограничение: между последней проверкой и оплатой время может занять другой посетитель. Чтение доступности и `/availability/check` не дают атомарного удержания на всё время внешнего платежа. Не называй такой режим гарантированной защитой от конкурирующих оплат.

Не создавай удержание в CRM с расчётом затем подтвердить его оплатой в стороннем приложении: в текущем API нет маршрута «подтвердить внешний платёж», «забронировать произвольным запросом» или «пометить оплаченным». Такой сценарий требует отдельного расширения API.

### 3.7. Если выбраны удержание и оплата через CRM

Этот раздел реализуй только для соответствующего режима. Нужны ключ с правом предоплат и настроенная онлайн-оплата CRM с банком по умолчанию и кассой услуг.

1. На нашем backend рассчитай и проверь сумму. Не доверяй сумме из браузера гостя.
2. Создай `POST /prepayment-intents` с JSON мероприятия и уникальным заголовком `Idempotency-Key`. В одной транзакции CRM проверит текущую доступность и удержит место.
3. Сохрани `data.id` удержания на backend, свяжи с локальным пользователем/попыткой оформления. Проверяй это соответствие при каждом обращении гостя к состоянию или отмене.
4. При нажатии «Оплатить» вызови `POST /prepayment-intents/{id}/payment`. Если получен `data.payment_url`, перенаправь гостя на него. Если уже возвращено `confirmed`, покажи подтверждение; если ссылки нет и платёж ещё не завершён, обработай состояние ожидания/проверки вместо перехода по пустому адресу. Перед созданием платежа CRM ещё раз проверяет интервал.
5. Показывай статус по `GET /prepayment-intents/{id}`. Успешная оплата — `data.state === "confirmed"`. Сам возврат из банка не доказывает оплату.
6. При задержке уведомления вызывай `/sync` с интервалом 30–60 секунд и ограниченным временем автоматического ожидания; затем предложи проверку оператором. Не опрашивай банк непрерывно.

Пример JSON создания удержания; все значения — образцы:

```json
{
  "date": "2026-12-20",
  "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": "Пожелания гостя"
}
```

`amount_rubles` — сумма этого платежа, `total_rubles` — полная стоимость, не меньше предоплаты; без `total_rubles` полная стоимость равна сумме платежа. Суммы — целые рубли RUB от 1 до 10 000 000. Имя и телефон обязательны, email и комментарий необязательны. До вызова `/payment` CRM-заявка и клиент не создаются.

`Idempotency-Key`: 8–128 символов из латинских букв, цифр, `_`, `.`, `:`, `-`. При тайм-ауте создания удержания повтори тот же запрос с тем же ключом. Неизменённая попытка вернёт прежнее удержание. Изменение параметров с прежним ключом приводит к `409 idempotency_conflict`. Для новой попытки после отмены/истечения создай новый ключ.

Один и тот же intent обслуживается тем ключом интеграции, который его создал. Повторная инициализация оплаты использует сохранённую попытку; при `payment_in_progress` дождись её состояния, не создавай новую попытку вслепую.

| `data.state` | Как отображать и обрабатывать |
| --- | --- |
| `held` | Время временно удерживается до `expires_at`, оплату ещё не запускали |
| `expired` | Удержание истекло; загрузить доступность заново |
| `payment_pending` | Платёж создаётся или ссылка выдана; место удерживается до результата банка |
| `payment_review` | Нужна проверка результата банка; сохранять ID и удержание |
| `confirmed` | Банк подтвердил оплату; место занимает CRM-заявка |
| `cancelled` | Удержание или платёж отменён |

`expires_at` — ISO 8601 в UTC. Таймер до оплаты применим к `held`. После запуска оплаты закрытие вкладки или окончание таймера не освобождают место. `DELETE /prepayment-intents/{id}` отменяет только удержание до запуска оплаты; после запуска возвращает `409 bank_state_required`. Требуются подтверждённая отмена в банке и `/sync`. Возврат денег после подтверждения оплаты — отдельный процесс CRM.

### 3.8. Ошибки, нагрузка и повторные запросы

| Ситуация | Действие модуля |
| --- | --- |
| `401 invalid_api_key` | Остановить запросы с неправильным/отозванным ключом, показать владельцу ошибку конфигурации |
| `403 api_disabled` / `scope_required` / `wrong_club_host` | Проверить включение API, права ключа и адрес клуба; не подменять поддомен автоматически |
| `404 venue_not_found` / `game_type_not_found` | Обновить справочники и настройку услуги |
| `409 slot_unavailable` / `hold_expired` | Обновить календарь, предложить другой интервал |
| `409 payment_in_progress` / `bank_state_required` | Сохранить ID и обработать состояние существующей попытки |
| `400`, `405`, `413`, `422` | Проверить метод/JSON/параметры; не повторять ошибочный запрос циклически |
| `429 rate_limit` | Выдержать `Retry-After`, прекратить лавину запросов |
| Тайм-аут или `5xx` | Показать временную недоступность; неизвестное время не считать свободным |
| Неопределённый результат платежа | Сохранить ID, проверить прежнюю попытку через состояние и `/sync`, не создавать второй платёж |

Лимит — **120 запросов в минуту на ключ**, общий для всех посетителей и процессов нашего приложения. Размер JSON — до 16 KiB. Ответы содержат `Cache-Control: no-store`: длительное кеширование доступности отключи. Объединяй одинаковые одновременные запросы к CRM, ограничивай частоту обновления UI и не загружай весь год при каждом открытии формы.

Не принимай от гостя параметры `account`, `account_id`, исключения существующих заявок или произвольные ID удержаний. Клуб задаётся ключом и поддоменом. В логах оставляй безопасные коды ошибок, длительность запроса и локальный ID попытки; заголовок Authorization и контакты гостя маскируй.

### 3.9. Критерии готовности и результат работы ИИ

Реализуй и проверь:

1. Backend использует конфигурацию и передаёт Bearer-заголовок, секрет не попадает в клиентскую сборку и логи.
2. Календарь корректно обрабатывает пустую выдачу, закрытые дни, смену фильтров, устаревший ответ и часовой пояс клуба.
3. Занятый сборной игрой/заявкой интервал исключается из выбора с учётом параллельной вместимости. Проверка использует всю выбранную длительность.
4. Перед платежом повторяется проверка; при недоступности оплата не запускается. Если режим только читающий, ограничение конкурентной оплаты явно описано владельцу.
5. Ошибки ключа, `429`, тайм-аут и недоступность CRM не превращаются в свободные слоты.
6. Для платёжного режима: повторная отправка/тайм-аут не создают дубли, ID сохраняется, возврат из банка без `confirmed` не считается оплатой, отмена следует состоянию CRM.

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

В конце работы предоставь владельцу:

- перечень изменённых файлов и краткое описание модуля;
- какие переменные секретов заполнить и где настроить соответствие услуг площадкам;
- команды запуска и результаты проверок;
- инструкцию проверки интерфейса гостем;
- что осталось настроить и какой режим защиты от конкурентного выбора реализован.

Не выдавай мок или форму с жёстко заданными часами за работающую интеграцию. Покажи путь запроса от формы до backend и CRM, обработку ответа и отказа при занятом времени.

## 4. Сопроводительное сообщение клиенту

> Мы добавили API календаря CRM. Ваше приложение может получать свободные дни и интервалы с учётом площадки, длительности, заявок и сборных игр. Передайте приложенный файл своему разработчику или ИИ вместе с исходным кодом приложения: в нём есть маршруты, формат запросов, правила обработки ошибок и критерии готовности модуля. Для первого этапа достаточно ключа «Только доступность»; его нужно хранить на сервере приложения. Если требуется удерживать место на всё время оплаты, в документе описан отдельный сценарий предоплаты через CRM.

Полный технический контракт: [OpenAPI](calendar-availability.openapi.json) и [подробная инструкция API](calendar-availability-api.md).
