> ## 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.

# Подарочные подписки

> Покупка VPN-подписок в подарок: по коду, через deep link или по контакту получателя

## Обзор

Подарочные подписки позволяют пользователям Cabinet покупать VPN-подписки и дарить их другим людям. Начиная с **v3.29.0** (бот) и **v1.30.0** (Cabinet) основной сценарий — **покупка по коду**: покупатель получает уникальный подарочный код, который можно передать любым способом. Получатель активирует код в боте или в Cabinet.

Прежний способ — указание получателя при покупке (email / Telegram username) — по-прежнему поддерживается.

### Возможности

* **Код-подарок** — покупка без указания получателя, передача кода любым удобным способом
* Активация кода через **deep link** бота (`t.me/bot?start=GIFT_XXXXXXXXXXXX`) или в **Cabinet** (вкладка «Активировать»)
* Отправка подписки по **email** или **Telegram username** (классический способ)
* Оплата с **баланса** (мгновенно) или через **платёжную систему**
* Выбор тарифа и периода подписки
* Персональное сообщение для получателя (до 1000 символов)
* **3 вкладки** в разделе подарков: Купить, Активировать, Мои подарки
* Защита от активации собственного кода
* Rate limiting: 5 подарков в минуту

<Warning>
  Для работы подарочных подписок необходимо:

  1. `CABINET_ENABLED=true`
  2. Функция включена через админ-панель (переключатель `CABINET_GIFT_ENABLED`)
  3. Хотя бы одна платёжная система подключена
  4. Настроены тарифы (`SALES_MODE=tariffs`)
</Warning>

***

## Включение функции

### Через Admin Cabinet

1. Откройте Cabinet → Админ-панель → **Настройки**
2. Найдите переключатель **Подарочные подписки** (Gift Subscriptions)
3. Включите его

<Note>
  Переключатель сохраняется в базе данных как системная настройка `CABINET_GIFT_ENABLED`. Перезапуск бота не требуется.
</Note>

### Через API

```bash theme={null}
# Включить
curl -X PATCH https://cabinet.example.com/api/cabinet/branding/gift-enabled \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'

# Проверить статус
curl https://cabinet.example.com/api/cabinet/branding/gift-enabled
```

### RBAC права

Для управления настройкой требуется разрешение `settings:edit`. Для чтения — `settings:read`.

***

## Для пользователей

### Интерфейс Cabinet

Раздел подарков в Cabinet содержит **3 вкладки**:

| Вкладка          | Назначение                                                                                                  |
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
| **Купить**       | Выбор тарифа, периода и оплата. Указание получателя **не обязательно** — по умолчанию создаётся код-подарок |
| **Активировать** | Ввод подарочного кода для активации полученной подписки                                                     |
| **Мои подарки**  | Три секции: **Активные** (ожидают активации), **Активированные** (использованные), **Полученные**           |

### Покупка подарка (код-подарок)

Основной сценарий — покупка без указания получателя:

<Steps>
  <Step title="Откройте вкладку «Купить»">
    В Cabinet перейдите в раздел **Подарить подписку** → вкладка **Купить**.
  </Step>

  <Step title="Выберите тариф и период">
    Доступны все активные тарифы с соответствующими периодами подписки.
  </Step>

  <Step title="Добавьте сообщение (опционально)">
    Напишите персональное сообщение — до 1000 символов.
  </Step>

  <Step title="Оплатите">
    * **С баланса** — списание мгновенное, код генерируется сразу
    * **Через платёжную систему** — код генерируется после подтверждения оплаты
  </Step>

  <Step title="Получите код">
    После оплаты отображается **12-символьный код** подарка. Передайте его получателю любым удобным способом.
  </Step>
</Steps>

<Info>
  Код можно передать двумя способами:

  * **Deep link бота:** `t.me/bot?start=GIFT_XXXXXXXXXXXX` — получатель нажимает ссылку и активирует подписку в Telegram
  * **Ссылка на Cabinet:** `cabinet.url/gift?tab=activate&code=TOKEN` — получатель активирует код в веб-интерфейсе
</Info>

### Покупка подарка с указанием получателя (классический способ)

По-прежнему можно указать получателя при покупке:

<Steps>
  <Step title="Откройте вкладку «Купить»">
    В Cabinet перейдите в раздел **Подарить подписку** → вкладка **Купить**.
  </Step>

  <Step title="Выберите тариф и период">
    Доступны все активные тарифы с соответствующими периодами подписки.
  </Step>

  <Step title="Укажите получателя">
    Введите **email** или **Telegram username** (с `@`) получателя подарка.
  </Step>

  <Step title="Добавьте сообщение (опционально)">
    Напишите персональное сообщение для получателя — до 1000 символов.
  </Step>

  <Step title="Выберите способ оплаты">
    * **С баланса** — списание мгновенное, подарок доставляется сразу
    * **Через платёжную систему** — после оплаты подарок доставляется автоматически
  </Step>
</Steps>

### Активация подарочного кода

#### В Cabinet

1. Откройте раздел **Подарить подписку** → вкладка **Активировать**
2. Введите подарочный код (минимум 8 символов)
3. Подписка активируется мгновенно

#### Через deep link бота

* **Новый пользователь:** отправьте ссылку `t.me/bot?start=GIFT_XXXXXXXXXXXX` — после регистрации подписка активируется автоматически
* **Существующий пользователь:** при переходе по ссылке подписка активируется сразу

<Warning>
  Покупатель **не может** активировать собственный подарочный код. При попытке API вернёт ошибку 400, а в боте код будет проигнорирован.
</Warning>

### Статус подарка

После покупки через платёжную систему отображается страница ожидания с автоматическим опросом статуса оплаты. Когда платёж подтверждён:

* **Код-подарок** — отображается подарочный код для передачи получателю
* **С указанием получателя** — подарок доставляется получателю автоматически

***

## Процесс покупки

### Код-подарок: оплата с баланса

```
Покупатель                    Cabinet                     API бота
    │                           │                           │
    │  1. Выбирает тариф,      │                           │
    │  оплату (без получателя) │                           │
    │──────────────────────────>│                           │
    │                           │  2. POST /gift/purchase   │
    │                           │  {payment_mode: balance}  │
    │                           │  (без recipient_*)        │
    │                           │──────────────────────────>│
    │                           │                           │  3. Списание с баланса
    │                           │                           │  4. Генерация кода
    │                           │  {status: "ok",           │
    │                           │   is_code_only: true,     │
    │                           │   purchase_token: "XXXX"} │
    │                           │<──────────────────────────│
    │  5. Отображение кода      │                           │
    │  для передачи             │                           │
    │<──────────────────────────│                           │
```

### Код-подарок: оплата через платёжную систему

```
Покупатель                    Cabinet                     API бота
    │                           │                           │
    │  1. Выбирает тариф,      │                           │
    │  оплату (без получателя) │                           │
    │──────────────────────────>│                           │
    │                           │  2. POST /gift/purchase   │
    │                           │  {payment_mode: gateway}  │
    │                           │──────────────────────────>│
    │                           │  {status: "created",      │
    │                           │   payment_url: "..."}     │
    │                           │<──────────────────────────│
    │  3. Переход на            │                           │
    │  платёжную систему       │                           │
    │──────────────────────────>│                           │
    │                           │                           │
    │  4. Возврат в Cabinet     │  5. Polling статуса       │
    │  (страница ожидания)     │  GET /gift/purchase/{t}   │
    │<──────────────────────────│──────────────────────────>│
    │                           │                           │  6. Webhook: оплата
    │                           │                           │  7. Генерация кода
    │                           │  {status: "paid",         │
    │                           │   is_code_only: true}     │
    │                           │<──────────────────────────│
    │  8. Отображение кода      │                           │
    │<──────────────────────────│                           │
```

### Активация подарочного кода

```
Получатель                    Cabinet / Бот               API бота
    │                           │                           │
    │  1. Вводит код /          │                           │
    │  переходит по deep link  │                           │
    │──────────────────────────>│                           │
    │                           │  2. POST /gift/activate   │
    │                           │  {code: "XXXXXXXXXXXX"}   │
    │                           │──────────────────────────>│
    │                           │                           │  3. Поиск по префиксу
    │                           │                           │  4. SELECT FOR UPDATE
    │                           │                           │  5. Создание подписки
    │                           │  {status: "activated"}    │
    │                           │<──────────────────────────│
    │  6. Подписка активна      │                           │
    │<──────────────────────────│                           │
```

### Классический способ (с указанием получателя)

При указании `recipient_type` и `recipient_value` поведение не изменилось — подписка доставляется получателю после оплаты, как и раньше.

### Резолвинг Telegram-получателя

При указании Telegram username бот пытается найти пользователя:

1. **База данных** — поиск по username среди существующих пользователей
2. **Bot API** — запрос через `getChat` для получения `user_id`
3. Если не удалось найти — покупка создаётся с предупреждением `recipient_warning`

<Note>
  Если получатель не найден в момент покупки, подарок будет ожидать активации. Когда получатель запустит бота через `/start`, подписка активируется автоматически.
</Note>

***

## API эндпоинты

### GET /cabinet/gift/config

Конфигурация подарков. Возвращает доступные тарифы, периоды, платёжные системы и баланс.

**Ответ:**

```json theme={null}
{
  "enabled": true,
  "tariffs": [
    {
      "id": 15,
      "name": "Стандарт",
      "periods": [
        {"days": 30, "price_kopeks": 30000},
        {"days": 90, "price_kopeks": 80000}
      ]
    }
  ],
  "payment_methods": [
    {
      "method_id": "yookassa",
      "display_name": "ЮKassa",
      "min_amount": 100,
      "max_amount": 1000000,
      "sub_options": [
        {"id": "card", "name": "Карта"},
        {"id": "sbp", "name": "СБП"}
      ]
    }
  ],
  "balance_kopeks": 50000,
  "currency_symbol": "₽"
}
```

### POST /cabinet/gift/purchase

Создание подарочной покупки. Поля `recipient_type` и `recipient_value` **опциональны** — если не указаны, создаётся код-подарок.

**Запрос (код-подарок, без получателя):**

```json theme={null}
{
  "tariff_id": 15,
  "period_days": 30,
  "gift_message": "С днём рождения!",
  "payment_mode": "balance"
}
```

**Запрос (с указанием получателя):**

```json theme={null}
{
  "tariff_id": 15,
  "period_days": 30,
  "recipient_type": "telegram",
  "recipient_value": "@username",
  "gift_message": "С днём рождения!",
  "payment_mode": "balance"
}
```

**Ответ (оплата с баланса, код-подарок):**

```json theme={null}
{
  "status": "ok",
  "purchase_token": "XXXXXXXXXXXX",
  "is_code_only": true,
  "warning": null
}
```

**Ответ (оплата через шлюз):**

```json theme={null}
{
  "status": "created",
  "purchase_token": "XXXXXXXXXXXX",
  "payment_url": "https://yookassa.ru/checkout/...",
  "is_code_only": true,
  "warning": null
}
```

<Note>
  Поле `purchase_token` в ответе содержит **усечённый код** (12 символов). Полный 64-символьный токен никогда не покидает сервер.
</Note>

### GET /cabinet/gift/purchase/{token}

Статус подарочной покупки. Поддерживает поиск по **префиксу токена** (минимум 8 символов). Доступно только покупателю.

**Ответ:**

```json theme={null}
{
  "status": "paid",
  "purchase_token": "XXXXXXXXXXXX",
  "is_code_only": true,
  "tariff_name": "Стандарт",
  "period_days": 30
}
```

### POST /cabinet/gift/activate

Активация подарочного кода. Поиск по префиксу (минимум 8 символов).

**Запрос:**

```json theme={null}
{
  "code": "XXXXXXXXXXXX"
}
```

**Ответ (успех):**

```json theme={null}
{
  "status": "activated",
  "tariff_name": "Стандарт",
  "period_days": 30
}
```

**Ошибки:**

| Код | Описание                                                        |
| --- | --------------------------------------------------------------- |
| 400 | Попытка активировать собственный подарок                        |
| 404 | Код не найден (единообразный ответ для предотвращения перебора) |
| 410 | Код уже активирован                                             |
| 429 | Превышен лимит запросов                                         |

### GET /cabinet/gift/pending

Список ожидающих подарков для текущего пользователя (как получателя).

***

## Безопасность

### Защита токенов

* Полный 64-символьный токен **никогда не покидает сервер** — в API-ответах возвращается усечённый 12-символьный код
* Для активации требуется минимум **8 символов** кода (поиск по префиксу)
* **Единообразные 404-ответы** — предотвращают перебор токенов (token enumeration)

### Защита от race conditions

* Активация использует `SELECT FOR UPDATE` — блокировка строки предотвращает двойную активацию при параллельных запросах

### Rate limiting

* Эндпоинт активации имеет отдельный rate limit
* Покупка: 5 подарков в минуту

### Защита от самоактивации

Покупатель не может активировать собственный код:

* API возвращает **400 Bad Request**
* В боте deep link с собственным кодом **игнорируется**

***

## Gateway webhook и код-подарки

<Info>
  Код-подарки (`is_code_only=true`, `is_gift=True` без получателя) обрабатываются иначе при получении webhook от платёжной системы:

  * **С получателем:** webhook → `fulfill_purchase()` → создание подписки → уведомление получателя
  * **Код-подарок:** webhook → генерация кода → **подписка НЕ создаётся** (создаётся при активации кода)

  Retry-сервис также исключает код-подарки из повторной обработки «зависших» оплат со статусом PAID.
</Info>

***

## Deep link активация в боте

### Формат deep link

```
t.me/bot?start=GIFT_XXXXXXXXXXXX
```

Префикс `GIFT_` обязателен. После него — подарочный код (12 символов).

### Сценарии

| Ситуация                           | Поведение                                                                   |
| ---------------------------------- | --------------------------------------------------------------------------- |
| Новый пользователь                 | `/start GIFT_XXXXXXXXXXXX` → регистрация → автоматическая активация подарка |
| Существующий пользователь          | `/start GIFT_XXXXXXXXXXXX` → немедленная активация                          |
| Код принадлежит этому пользователю | Код игнорируется (без ошибки)                                               |
| Код уже использован                | Сообщение об ошибке                                                         |
| Неверный код                       | Сообщение об ошибке                                                         |

***

## Транзакции

Подарочные платежи записываются с типом `GIFT_PAYMENT`:

* Сумма **отрицательная** (списание с баланса покупателя)
* `payment_method` — `None` для оплаты с баланса, идентификатор провайдера для gateway-оплаты
* Описание: формат с указанием тарифа (и получателя, если указан)

<Note>
  Подарочные платежи с баланса (`payment_method=None`) **не учитываются** в статистике доходов, так как `REAL_PAYMENT_METHODS` не включает `NULL`.
</Note>

***

## Устранение проблем

### Раздел подарков не отображается

1. Функция включена? Проверьте `GET /cabinet/branding/gift-enabled`
2. `CABINET_ENABLED=true`?
3. Хотя бы один тариф создан и активен?

### Код подарка не активируется

1. Код введён верно? Минимум 8 символов
2. Это не ваш собственный код? Покупатель не может активировать свой подарок
3. Код уже был использован? Проверьте вкладку «Мои подарки» → «Активированные»
4. Проверьте логи:
   ```bash theme={null}
   docker logs <bot_container> 2>&1 | grep -i "gift.*activate"
   ```

### Получатель не получает подарок (классический способ)

1. Telegram username указан корректно (с `@`)?
2. Получатель запустил бота? Если нет — подарок ожидает активации через `/start`
3. Проверьте логи:
   ```bash theme={null}
   docker logs <bot_container> 2>&1 | grep -i "gift"
   ```

### Ошибка «Нельзя активировать собственный подарок»

Система запрещает активацию собственного подарочного кода. Передайте код другому пользователю.

### Ошибка rate limit

Максимум 5 подарочных покупок за 60 секунд. Подождите и попробуйте снова.

### Deep link не работает

1. Формат ссылки верный? `t.me/bot?start=GIFT_XXXXXXXXXXXX`
2. Префикс `GIFT_` присутствует?
3. Бот обновлён до v3.29.0+?
