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

# Рекуррентные платежи

> Автопродление подписок: сохранённые карты, автоматическое списание и управление

## Обзор

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

1. **Автопродление (autopay)** — автоматическое списание с баланса при приближении окончания подписки
2. **Сохранённые карты** — автоматическое пополнение баланса через YooKassa, если средств на балансе недостаточно

```
Подписка истекает через N дней
        ↓
Баланс достаточен?
   ├─ Да → Списание с баланса → Продление подписки
   └─ Нет → Есть сохранённая карта?
              ├─ Да → Списание с карты → Пополнение баланса → Продление
              └─ Нет → Уведомление «Недостаточно средств»
```

<Warning>
  Для полноценной работы необходимо включить **оба** компонента:

  * `ENABLE_AUTOPAY=true` — автопродление с баланса
  * `YOOKASSA_RECURRENT_ENABLED=true` — сохранение и использование карт

  Без `ENABLE_AUTOPAY` сохранённые карты не будут использоваться автоматически. Без `YOOKASSA_RECURRENT_ENABLED` продление возможно только при достаточном балансе.
</Warning>

***

## Автопродление (с баланса)

### Как работает

Мониторинг-сервис ежедневно проверяет подписки с включённым автопродлением:

1. Находит подписки, где `autopay_enabled=true` и до окончания осталось ≤ `autopay_days_before` дней
2. Рассчитывает стоимость продления (с учётом промо-скидок)
3. Если баланс достаточен — списывает и продлевает подписку
4. Если баланс недостаточен — пытается списать с сохранённой карты (при наличии)
5. Отправляет уведомление о результате

<Note>
  Пробные (trial) и суточные подписки исключены из автопродления. Также обрабатываются подписки, истекшие не более 48 часов назад.
</Note>

### Настройка

```env theme={null}
ENABLE_AUTOPAY=true
DEFAULT_AUTOPAY_ENABLED=true
DEFAULT_AUTOPAY_DAYS_BEFORE=3
MIN_BALANCE_FOR_AUTOPAY_KOPEKS=10000
```

| Переменная                                      | Тип    | По умолчанию | Описание                                                  |
| ----------------------------------------------- | ------ | ------------ | --------------------------------------------------------- |
| `ENABLE_AUTOPAY`                                | `bool` | `false`      | Глобальное включение автопродления                        |
| `DEFAULT_AUTOPAY_ENABLED`                       | `bool` | `false`      | Автопродление включено у новых пользователей по умолчанию |
| `DEFAULT_AUTOPAY_DAYS_BEFORE`                   | `int`  | `3`          | За сколько дней до окончания подписки запускать продление |
| `MIN_BALANCE_FOR_AUTOPAY_KOPEKS`                | `int`  | `10000`      | Минимальный баланс для срабатывания (100 руб)             |
| `SUBSCRIPTION_RENEWAL_BALANCE_THRESHOLD_KOPEKS` | `int`  | `20000`      | Порог баланса для автопродления (200 руб)                 |

### Интерфейс пользователя

#### В боте

Пользователь управляет автопродлением через меню подписки:

| Кнопка             | Действие                                     |
| ------------------ | -------------------------------------------- |
| Включить/Выключить | Переключение `autopay_enabled`               |
| Настроить дни      | Выбор за сколько дней до окончания списывать |

Отображается текущий статус: включено/выключено и количество дней.

#### В Cabinet

```
PATCH /cabinet/subscription/autopay
```

**Запрос:**

```json theme={null}
{
  "enabled": true,
  "days_before": 3
}
```

<Info>
  Автопродление недоступно для суточных тарифов. При попытке включить на суточном тарифе API вернёт ошибку.
</Info>

***

## Сохранённые карты (YooKassa)

### Как сохраняются карты

При оплате через YooKassa карта сохраняется автоматически:

1. Пользователь создаёт платёж через YooKassa
2. Если `YOOKASSA_RECURRENT_REQUIRED=true` — карта сохраняется принудительно
3. Если `YOOKASSA_RECURRENT_REQUIRED=false` — YooKassa показывает чекбокс «Сохранить карту»
4. После успешной оплаты webhook возвращает `payment_method.saved=true`
5. Бот сохраняет данные карты в таблицу `saved_payment_methods`

```
Пользователь              YooKassa                    Бот
    │                        │                          │
    │  1. Оплата             │                          │
    │───────────────────────>│                          │
    │                        │                          │
    │  2. Чекбокс            │                          │
    │  «Сохранить карту»     │                          │
    │  (или принудительно)   │                          │
    │───────────────────────>│                          │
    │                        │                          │
    │                        │  3. Webhook              │
    │                        │  payment_method.saved     │
    │                        │──────────────────────────>│
    │                        │                          │  4. Сохранение
    │                        │                          │  в БД
    │                        │                          │
    │  5. Карта сохранена    │                          │
    │<──────────────────────────────────────────────────│
```

### Что сохраняется

| Поле                         | Описание                                        |
| ---------------------------- | ----------------------------------------------- |
| `yookassa_payment_method_id` | Токен YooKassa для повторных списаний           |
| `method_type`                | Тип: `bank_card`, `yoo_money`, `sberbank` и др. |
| `card_first6`                | Первые 6 цифр (BIN)                             |
| `card_last4`                 | Последние 4 цифры                               |
| `card_type`                  | Платёжная система: Visa, MasterCard, Mir        |
| `card_expiry_month/year`     | Срок действия                                   |
| `title`                      | Отображаемое имя, напр. «Visa \*4444»           |

<Note>
  Дедупликация: если карта с таким `payment_method_id` уже существует, новая запись не создаётся. Если карта была ранее деактивирована — она реактивируется.
</Note>

### Настройка

```env theme={null}
YOOKASSA_RECURRENT_ENABLED=true
YOOKASSA_RECURRENT_REQUIRED=false
```

| Переменная                    | Тип    | По умолчанию | Описание                                        |
| ----------------------------- | ------ | ------------ | ----------------------------------------------- |
| `YOOKASSA_RECURRENT_ENABLED`  | `bool` | `false`      | Включить сохранение карт и рекуррентные платежи |
| `YOOKASSA_RECURRENT_REQUIRED` | `bool` | `false`      | Принудительно сохранять карту при каждой оплате |

### Управление картами (Cabinet API)

#### Список карт

```
GET /cabinet/balance/saved-cards
```

**Ответ:**

```json theme={null}
{
  "cards": [
    {
      "id": 42,
      "method_type": "bank_card",
      "card_last4": "4444",
      "card_type": "Visa",
      "title": "Visa *4444",
      "created_at": "2026-01-15T12:00:00Z"
    }
  ],
  "recurrent_enabled": true
}
```

#### Удаление карты

```
DELETE /cabinet/balance/saved-cards/{card_id}
```

Мягкое удаление — карта помечается как `is_active=false`. При повторной оплате той же картой она реактивируется автоматически.

***

## Автоматическое списание с карты

### Процесс

Если при автопродлении на балансе недостаточно средств и у пользователя есть сохранённые карты:

1. Рассчитывается сумма для продления подписки
2. Бот перебирает все активные карты пользователя
3. Для каждой карты создаётся платёж через YooKassa API (без подтверждения пользователя)
4. При успехе — баланс пополняется, подписка продлевается
5. При неудаче — пробуется следующая карта
6. Если все карты не сработали — отправляется уведомление

```
Мониторинг                 Recurrent Service          YooKassa
    │                           │                        │
    │  1. Баланс недостаточен   │                        │
    │──────────────────────────>│                        │
    │                           │                        │
    │                           │  2. Карта #1           │
    │                           │  create_autopayment()  │
    │                           │───────────────────────>│
    │                           │                        │
    │                           │  3. Результат          │
    │                           │<───────────────────────│
    │                           │                        │
    │                           │  [Если ошибка]         │
    │                           │  4. Карта #2...        │
    │                           │───────────────────────>│
    │                           │                        │
    │  5. Результат             │                        │
    │<──────────────────────────│                        │
```

<Info>
  Каждый платёж использует ключ идемпотентности, что предотвращает повторные списания при сетевых ошибках.
</Info>

### Защита от дублирования

Сервис рекуррентных платежей выполняется не чаще одного раза в сутки (daily guard). Подписки, уже обработанные за текущий день, пропускаются.

***

## Уведомления

### Предупреждения об окончании подписки

Настраиваются через `AUTOPAY_WARNING_DAYS` (по умолчанию `3,1` — за 3 дня и за 1 день):

| Условие                              | Сообщение                                      |
| ------------------------------------ | ---------------------------------------------- |
| Автопродление вкл. + карта сохранена | «Автоматическое списание с карты»              |
| Автопродление вкл. + нет карты       | «Подписка продлится автоматически» (с баланса) |
| Автопродление выкл.                  | «Не забудьте продлить вручную!»                |

### Результат автопродления

| Событие                          | Описание                                           |
| -------------------------------- | -------------------------------------------------- |
| Успешное продление с баланса     | «Подписка продлена на N дней за X руб»             |
| Успешное списание с карты        | «Автоплатёж выполнен, баланс пополнен на X руб»    |
| Недостаточно средств (нет карты) | «Не удалось списать X руб, баланс: Y руб» + кнопки |
| Все карты не сработали           | «Не удалось списать ни с одной сохранённой карты»  |

<Note>
  Уведомление о недостаточном балансе отправляется с cooldown 24 часа — повторное уведомление в течение суток не приходит.
</Note>

***

## Администрирование

### Управление автопродлением пользователя

```
POST /cabinet/admin/users/{user_id}/subscription-action
```

```json theme={null}
{
  "action": "toggle_autopay",
  "autopay_enabled": true
}
```

### Просмотр статистики

Сервис рекуррентных платежей возвращает статистику после каждого запуска:

| Метрика                | Описание                        |
| ---------------------- | ------------------------------- |
| `checked`              | Подписок проверено              |
| `payments_created`     | Успешных автоплатежей           |
| `insufficient_no_card` | Недостаточно средств, нет карты |
| `all_cards_failed`     | Все карты не сработали          |
| `already_processed`    | Уже обработано сегодня          |
| `errors`               | Ошибки при обработке            |

***

## Полная настройка

### Минимальная конфигурация

```env theme={null}
# Автопродление с баланса
ENABLE_AUTOPAY=true

# Рекуррентные платежи через YooKassa
YOOKASSA_ENABLED=true
YOOKASSA_SHOP_ID=123456
YOOKASSA_SECRET_KEY=live_xxxxxxxxxxxxx
YOOKASSA_RECURRENT_ENABLED=true
```

### Расширенная конфигурация

```env theme={null}
# Автопродление
ENABLE_AUTOPAY=true
DEFAULT_AUTOPAY_ENABLED=true
DEFAULT_AUTOPAY_DAYS_BEFORE=3
MIN_BALANCE_FOR_AUTOPAY_KOPEKS=10000
SUBSCRIPTION_RENEWAL_BALANCE_THRESHOLD_KOPEKS=20000
AUTOPAY_WARNING_DAYS=3,1

# Рекуррентные платежи
YOOKASSA_RECURRENT_ENABLED=true
YOOKASSA_RECURRENT_REQUIRED=false
```

| Переменная                                      | Тип    | По умолчанию | Описание                                      |
| ----------------------------------------------- | ------ | ------------ | --------------------------------------------- |
| `ENABLE_AUTOPAY`                                | `bool` | `false`      | Глобальное включение автопродления            |
| `DEFAULT_AUTOPAY_ENABLED`                       | `bool` | `false`      | Автопродление для новых пользователей         |
| `DEFAULT_AUTOPAY_DAYS_BEFORE`                   | `int`  | `3`          | За сколько дней до окончания списывать        |
| `MIN_BALANCE_FOR_AUTOPAY_KOPEKS`                | `int`  | `10000`      | Минимальный баланс для срабатывания (100 руб) |
| `SUBSCRIPTION_RENEWAL_BALANCE_THRESHOLD_KOPEKS` | `int`  | `20000`      | Порог баланса для автопродления (200 руб)     |
| `AUTOPAY_WARNING_DAYS`                          | `str`  | `3,1`        | Дни для предупреждений (через запятую)        |
| `YOOKASSA_RECURRENT_ENABLED`                    | `bool` | `false`      | Включить сохранение карт                      |
| `YOOKASSA_RECURRENT_REQUIRED`                   | `bool` | `false`      | Принудительное сохранение карт                |

***

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

### Автопродление не срабатывает

1. `ENABLE_AUTOPAY=true`?
2. У пользователя включено автопродление? Проверьте `autopay_enabled` на подписке
3. Подписка не trial и не суточная?
4. Баланс достаточен или есть сохранённая карта?
5. Проверьте логи:
   ```bash theme={null}
   docker logs <bot_container> 2>&1 | grep -i "autopay\|recurrent"
   ```

### Карты не сохраняются

1. `YOOKASSA_RECURRENT_ENABLED=true`?
2. Пользователь дал согласие на сохранение (или `YOOKASSA_RECURRENT_REQUIRED=true`)?
3. Webhook от YooKassa приходит? Проверьте:
   ```bash theme={null}
   docker logs <bot_container> 2>&1 | grep -i "payment_method.*saved"
   ```

### Списание с карты не проходит

1. Срок действия карты не истёк?
2. На карте достаточно средств?
3. Банк не блокирует рекуррентные платежи?
4. Проверьте ответ YooKassa в логах:
   ```bash theme={null}
   docker logs <bot_container> 2>&1 | grep -i "recurrent\|autopayment"
   ```

### Пользователь получает повторные уведомления

Cooldown на уведомления о недостаточном балансе — 24 часа. Если Redis недоступен, cooldown не работает и уведомления могут дублироваться. Проверьте подключение к Redis.
