> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bedolagam.ru/llms.txt
> Use this file to discover all available pages before exploring further.

# Купоны на подписку

> Партии одноразовых купонов для оптовых продаж, раздач и конкурсов

## Обзор

Купон — одноразовая ссылка, которая выдаёт подписку по конкретному тарифу на конкретный срок. Купоны создаются **партиями**: администратор генерирует N ссылок, передаёт их партнёру или разыгрывает, а расчёт с партнёром идёт вне бота.

Подсистема появилась в `v3.63.0`, лимит активаций на пользователя и удаление партий — в `v3.67.0`.

<Note>
  Купон — не промокод. Промокод даёт скидку, баланс, дни или трафик к существующей покупке и может быть многоразовым. Купон выдаёт подписку целиком, одноразовый и живёт внутри партии. См. [Промокоды и промопредложения](/bot/promo-system).
</Note>

## Что получает пользователь

Активация купона повторяет семантику подарочной подписки:

* нет подписки — создаётся новая по тарифу партии на `period_days`;
* есть активная подписка — она **продлевается** на `period_days`;
* есть истёкшая подписка — она заменяется новой.

## Ссылка активации

Каждый купон — это deep link:

```
https://t.me/{bot_username}?start=coupon_{token}
```

Токен — 32 hex-символа (128 бит). Полная нагрузка (`coupon_` + токен = 39 символов) укладывается в лимит Telegram в 64 символа, поэтому ссылка всегда ищется точным совпадением и не обрезается.

## Параметры партии

| Параметр                 | Описание                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| `name`                   | Название партии — для себя, видно в списке                                                  |
| `tariff_id`              | Тариф, который выдаёт купон                                                                 |
| `period_days`            | На сколько дней выдаётся или продлевается подписка                                          |
| `coupons_total`          | Сколько купонов сгенерировать                                                               |
| `wholesale_price_kopeks` | Оптовая цена за купон. Только бухгалтерия — деньги через бота не проходят                   |
| `max_per_user`           | Сколько купонов **этой партии** может активировать один пользователь. `0` — без ограничения |
| `valid_until`            | Дата, после которой купоны партии не активируются                                           |

<Tip>
  Для раздач и конкурсов ставьте `max_per_user=1`. Купоны одноразовые, но их в партии много — без лимита один человек может забрать всю партию.
</Tip>

## Управление через кабинет

| Метод    | Эндпоинт                           | Действие                                |
| -------- | ---------------------------------- | --------------------------------------- |
| `GET`    | `/admin/coupons`                   | Список партий                           |
| `POST`   | `/admin/coupons`                   | Создать партию и сгенерировать купоны   |
| `GET`    | `/admin/coupons/{batch_id}`        | Карточка партии                         |
| `GET`    | `/admin/coupons/{batch_id}/links`  | Экспорт ссылок партии                   |
| `POST`   | `/admin/coupons/{batch_id}/revoke` | Отозвать неиспользованные купоны партии |
| `DELETE` | `/admin/coupons/{batch_id}`        | Удалить партию                          |

Партии также создаются и просматриваются из админ-панели бота.

## Статусы купона

| Статус     | Значение                                               |
| ---------- | ------------------------------------------------------ |
| `active`   | Не активирован, готов к использованию                  |
| `redeemed` | Активирован; в записи сохраняются пользователь и время |
| `revoked`  | Отозван администратором                                |

<Note>
  Флаг `is_revoked` у партии — это кэш для списка. Право на активацию всегда определяется статусом конкретного купона, а не флагом партии.
</Note>

## Отказы при активации

| Код                       | Что видит пользователь                                     |
| ------------------------- | ---------------------------------------------------------- |
| `invalid`                 | Купон не найден, отозван или уже активирован кем-то другим |
| `expired`                 | Срок действия партии истёк                                 |
| `already_redeemed_by_you` | Этот купон пользователь уже активировал сам                |
| `per_user_limit`          | Лимит `max_per_user` по этой партии исчерпан               |
| `internal`                | Внутренняя ошибка                                          |

<Warning>
  «Не найден», «отозван» и «активирован другим» намеренно отвечают одним и тем же `invalid`. Иначе перебор ссылок превращался бы в оракул, по которому можно проверять существование токенов.
</Warning>

## Гарантии

<AccordionGroup>
  <Accordion title="Один купон — одна активация">
    Списание идёт под `SELECT ... FOR UPDATE` с повторной проверкой под блокировкой. Два одновременных перехода по одной ссылке не выдадут две подписки.
  </Accordion>

  <Accordion title="Отклонённая ссылка не держит блокировку">
    Вся валидация выполняется на чтении без блокировки — неудачный переход не тормозит остальные активации.
  </Accordion>

  <Accordion title="Деньги через бота не проходят">
    `wholesale_price_kopeks` — только учёт. Расчёт с партнёром идёт вне бота, поэтому купоны не участвуют в статистике продаж как платежи.
  </Accordion>
</AccordionGroup>

## Связанные разделы

<CardGroup cols={2}>
  <Card title="Промокоды" icon="ticket" href="/bot/promo-system">
    Скидки, баланс, дни и трафик
  </Card>

  <Card title="Подарочные подписки" icon="gift" href="/cabinet/gift-subscriptions">
    Похожий сценарий выдачи подписки по ссылке
  </Card>
</CardGroup>
