# Спортзал и фитнес

> Продажа абонементов, продление, ежемесячные счета через подписку, заморозка и возврат, открытие турникета по webhook — разбор сценариев для фитнес-клуба с примерами запросов.

## Коротко

В зале есть три разных платежа: **разовый вход**, **продажа абонемента** и **ежемесячный повторяющийся платёж**. Первые два — обычный счёт: показали QR на ресепшене или отправили счёт на телефон клиента. Для третьего есть [подписки](/kb/ru/subscriptions-api): они выставляют счёт по расписанию. Сразу договоримся о главном — **подписка не снимает деньги с карты сама**, она только выставляет счёт, а каждую оплату клиент подтверждает в приложении Kaspi.

## Продажа абонемента

Клиент пришёл на ресепшен и хочет абонемент на три месяца.

1. Администратор заводит карточку абонемента в CRM или учётной системе.
2. Система выставляет счёт:

```json
POST /api/v1/invoices
{
  "amount": 45000,
  "kind": "qr",
  "description": "Абонемент 3 месяца, взрослый",
  "externalOrderId": "ABN-2026-1187",
  "metadata": { "client_id": "1187", "plan": "3m", "starts": "2026-09-15" }
}
```

3. На экране ресепшена появляется QR, клиент сканирует и платит.
4. Приходит webhook `invoice.paid` — система активирует абонемент и выдаёт карту или браслет.

**Активируйте абонемент только по `invoice.paid`**, а не в момент создания счёта. Иначе в зал будут ходить те, кто так и не заплатил.

Если клиент покупает удалённо — из Instagram, WhatsApp или с сайта — отправляйте `kind: "phone"`, ему придёт push в Kaspi. Телефон в формате `7XXXXXXXXXX`, `description` не длиннее 60 символов.

## Продление

Продление — это новый счёт, отдельного метода API для него нет. Важно другое: **когда отправлять** и **что написать**.

| Что | Как |
|---|---|
| Напоминание | Счёт за 3–5 дней до окончания |
| Тип | `kind: "phone"` — клиента нет в зале |
| Описание | «Продление абонемента, октябрь» — чтобы было понятно, за что платят |
| Связь | В `externalOrderId` номер продления, в `metadata` — `client_id` и новый срок |
| Не оплатили | По окончании срока абонемент закрывается, новый счёт — при следующем визите |

У счёта на продление ограниченный срок: окно QR — около трёх минут, счёт на телефон тоже живёт не вечно. Поэтому «разослать всем в начале месяца» не работает — лучше [подписка](/kb/ru/subscriptions-api), она выставляет каждому клиенту свежий счёт в его собственную дату.

## Ежемесячный платёж через подписку

Подписка — это **автоматическое выставление счёта по расписанию**. Деньги с карты сами не уходят, каждую оплату клиент подтверждает лично. Разница принципиальная: клиент в любой месяц может просто не платить, и заставить его нельзя.

Что настраивается:

- **Интервал**: `month` с кратностью `every: 1` — раз в месяц. Для недельных форматов есть `week`.
- **Шаги повтора** `retryDelaysMin`, по умолчанию `[15, 60, 360]` минут. Если клиент был на работе и не увидел счёт, он выставится снова через 15 минут, затем через час, затем через шесть часов. Максимум 5 значений. Для фитнеса значений по умолчанию достаточно, слишком частые повторы раздражают.
- **Политика пропуска** `misfirePolicy`: `run_once` (по умолчанию) или `skip`, `misfireAfterMin` по умолчанию 1440. Если система несколько часов не работала, пропущенный запуск либо выполнится один раз, либо будет отброшен.
- Когда шаги закончились, запуск этого месяца отбрасывается, а расписание продолжается со следующего. Всё видно в полях `failedRuns`, `lastError`, `lastRunStatus`.

Клиент ушёл в заморозку — ставите подписку на `pause`, вернулся — `resume`. Нужно сразу догнать пропущенный запуск: `POST /subscriptions/{id}/resume` с `{ catchUp: true }`.

Подробнее: [API подписок](/kb/ru/subscriptions-api) и [Бизнес по подписке](/kb/ru/for-subscription-business).

## Заморозка и возврат

**Заморозка.** Клиент уехал на две недели. Срок абонемента сдвигаете в своей системе, подписку ставите на `pause`. На стороне Qut Pay ничего возвращать не нужно, деньги не двигаются.

**Возврат.** Клиент хочет вернуть абонемент целиком:

```json
POST /api/v1/invoices/{id}/refund
{ "amount": 30000, "reason": "Возврат абонемента, использован 1 месяц" }
```

Без поля `amount` вернётся вся сумма. При частичном возврате счёт переходит в статус `partially_refunded`. Расчёт делаете вы: вычитаете использованные дни и возвращаете остаток — пропорцию Qut Pay не считает.

Берегитесь двойного возврата: [Как не вернуть деньги дважды](/kb/ru/double-refund). Если возврат не проходит: [Возврат не проходит](/kb/ru/refund-not-working).

## Турникет

Разовый вход можно автоматизировать: клиент сканирует QR, платит, турникет открывается.

Как это устроено:

1. Экран у турникета (планшет или небольшой монитор) запрашивает счёт у вашего сервера: `amount: 2500`, `metadata: { "gate": "north" }`.
2. На экране показывается `qrImageUrl`.
3. Клиент платит.
4. На ваш сервер приходит webhook `invoice.paid`. По `metadata.gate` он понимает, какой это турникет, и отправляет контроллеру команду на открытие.

Подпись webhook проверяйте обязательно: `HMAC-SHA256(secret, timestamp + "." + rawBody)`, причём тело — **в неизменном байтовом виде**, до разбора JSON. Там, где открывается физическая дверь, это особенно важно: [Безопасность вебхуков](/kb/ru/webhook-security).

Обработчик должен быть идемпотентным: если вы не ответили 2xx, доставка повторится до 11 раз — турникет не должен открыться одиннадцать раз. Храните пару `(invoice.id, status)` и игнорируйте повторы.

Место чувствительно к задержке, поэтому вместе с webhook опрашивайте и статус: [Вебхук или опрос статуса](/kb/ru/polling-vs-webhook). Точно такая же схема используется на парковках: [Парковки и шлагбаумы](/kb/ru/for-parking).

## Вариант без кода

- **Постоянные ссылки на оплату.** По ссылке на каждый тариф: «1 месяц», «3 месяца», «персональный тренер». Их кладут в профиль Instagram, в ответы в WhatsApp и на табличку у ресепшена: [Ссылки на оплату](/kb/ru/payment-links).
- **Счёт вручную из кабинета.** Администратор вводит сумму и описание, получает QR.
- **Telegram-бот.** `/invoice 45000 Абонемент 3 месяца`, `/today` — итог дня, `/last` — последние счета. Бота можно добавить в группу и видеть всю смену.
- **Подписку тоже можно создать из кабинета**, без единой строчки кода.

## Что учесть заранее

- **Не обещайте, что деньги будут сниматься сами.** Подписка только выставляет счёт. Объясните это и клиенту, иначе появится вопрос «почему я снова должен что-то подтверждать».
- **Не заходите в приложение Kaspi Pay с номера кассира** — привязка оборвётся, и на ресепшене перестанут выставляться счета: [Привязка кассира оборвалась](/kb/ru/connection-lost).
- **Не забывайте о поздних оплатах.** Деньги по счёту в статусе `expired` могут прийти позже, и `invoice.paid` придёт с пометкой `late: true`. Либо выдайте абонемент, либо верните деньги: [Поздняя оплата](/kb/ru/late-payment).
- **Тариф считайте по числу счетов, а не клиентов.** 300 подписчиков плюс продления плюс разовые входы легко переваливают за 800 счетов в месяц: [Какой тариф выбрать](/kb/ru/tariff-choose).

## Вопросы и ответы

**При подписке клиенту нужно подтверждать оплату каждый месяц?** Да. Подписка выставляет счёт, а оплату клиент подтверждает сам в приложении Kaspi. Деньги сами не уходят.

**Что будет, если клиент пропустит месяц?** Когда шаги повтора (по умолчанию 15, 60, 360 минут) закончатся, запуск этого месяца отбрасывается, расписание идёт дальше. Закрывать абонемент или нет — решает ваша система.

**Нужно ли возвращать деньги при заморозке?** Нет. Заморозка — это сдвиг срока в вашей учётной системе, подписку просто ставите на паузу.

**Как быстро открывается турникет?** После оплаты webhook обычно приходит в пределах 5 секунд. Это зависит от Kaspi и от сети, гарантировать точное время мы не можем — поэтому параллельно опрашивайте статус.

**У нас два зала, можно разделить отчётность?** Да: каждому залу свой API-ключ, привязанный к своему кассиру: [Раздельная отчётность по точкам](/kb/ru/multi-point-reporting).
