Files
remnawave-minishop/docs/tariffs.md
T

287 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Тарифы
Бот поддерживает два способа описания продаж:
- JSON-каталог тарифов из `TARIFFS_CONFIG_PATH` (по умолчанию `config/tariffs.json`);
- конфигурация через переменные `.env`, если JSON-файл отсутствует.
JSON-каталог может содержать несколько тарифов разных моделей: подписки на срок, пакеты трафика без срока действия, разные наборы Internal Squads, лимиты устройств и пакеты докупки. Пример формата: [config/tariffs.example.json](../config/tariffs.example.json).
## Управление через админку
Каталог тарифов можно настраивать из Web App админки: раздел **Система → Тарифы**. Админка читает и сохраняет файл из `TARIFFS_CONFIG_PATH`, валидирует данные той же моделью `TariffsConfig`, что и бот, и атомарно перезаписывает JSON только после успешной проверки.
В интерфейсе доступны:
- добавление, редактирование и удаление тарифов;
- включение и выключение тарифа на витрине;
- выбор тарифа по умолчанию;
- настройка `period`-тарифов: месячный лимит, периоды, RUB/Stars цены, пакеты докупки трафика;
- настройка `traffic`-тарифов: пакеты GB, RUB/Stars цены, курс конвертации;
- настройка Internal Squads, базового HWID-лимита и пакетов докупки устройств.
После сохранения изменения применяются к новым запросам Web App сразу, потому что конфиг тарифов загружается из JSON при обращении. Уже созданные подписки сохраняют свой `tariff_key`; при удалении или отключении тарифа проверьте, что активные подписки с этим ключом не требуют дальнейшего продления или смены.
## Как выбирается режим
Если файл из `TARIFFS_CONFIG_PATH` существует и проходит валидацию, используется каталог тарифов. В этом режиме `TRAFFIC_PACKAGES` и цены подписок из `.env` не формируют витрину продаж, потому что цены и пакеты берутся из JSON.
Если JSON-файл отсутствует, бот использует значения `.env`:
- `RUB_PRICE_*`, `STARS_PRICE_*` и `*_MONTHS_ENABLED` для подписок на срок;
- `TRAFFIC_PACKAGES` и `STARS_TRAFFIC_PACKAGES` для продажи пакетов трафика;
- `USER_TRAFFIC_LIMIT_GB`, `USER_TRAFFIC_STRATEGY`, `USER_SQUAD_UUIDS`, `USER_HWID_DEVICE_LIMIT` для пользователей Remnawave.
В режиме без JSON-каталога наличие `TRAFFIC_PACKAGES` или `STARS_TRAFFIC_PACKAGES` переключает витрину на продажу трафика вместо подписок на срок.
## Структура JSON-каталога
Минимальная структура:
```json
{
"default_tariff": "standard",
"tariffs": [
{
"key": "standard",
"names": { "ru": "Стандарт", "en": "Standard" },
"descriptions": { "ru": "Базовый набор серверов" },
"squad_uuids": ["uuid-1"],
"billing_model": "period",
"monthly_gb": 500,
"prices_rub": { "1": 150, "3": 400 },
"enabled_periods": [1, 3],
"topup_packages": {
"rub": [{ "gb": 10, "price": 99 }],
"stars": [{ "gb": 10, "price": 2500 }]
},
"enabled": true
}
]
}
```
Основные поля:
| Поле | Назначение |
| --- | --- |
| `default_tariff` | Тариф по умолчанию для первичного выбора и привязки активных подписок без `tariff_key`. |
| `tariffs[].key` | Стабильный ключ тарифа. Используется в платежах, подписках и смене тарифа. |
| `tariffs[].names` | Названия тарифа по языкам. |
| `tariffs[].descriptions` | Описания тарифа по языкам. |
| `tariffs[].enabled` | Доступность тарифа на витрине. |
| `tariffs[].squad_uuids` | Internal Squads Remnawave для пользователей тарифа. |
| `tariffs[].premium_squad_uuids` | Internal Squads с отдельным premium-лимитом. Ноды для учета берутся автоматически из accessible nodes этих сквадов через API панели. |
| `tariffs[].premium_monthly_gb` | Отдельный месячный лимит трафика по premium-сквадам. `0` или отсутствие поля отключает отдельное ограничение. |
| `tariffs[].premium_topup_packages` | Пакеты докупки premium-трафика: `{ "gb": 10, "price": 99 }`. |
| `tariffs[].billing_model` | Модель тарифа: `period` или `traffic`. |
| `tariffs[].hwid_device_limit` | Базовый лимит HWID-устройств. `0` означает безлимит, отсутствие поля использует `USER_HWID_DEVICE_LIMIT`. |
| `tariffs[].hwid_device_packages` | Пакеты докупки устройств: `{ "count": 1, "price": 99 }`. |
Для `period`-тарифа также используются:
| Поле | Назначение |
| --- | --- |
| `monthly_gb` | Базовый месячный лимит трафика тарифа. `0` означает безлимит. |
| `prices_rub` | Цены периодов в рублях, ключ - количество месяцев. |
| `prices_stars` | Цены периодов в Telegram Stars. |
| `enabled_periods` | Периоды, доступные для покупки. |
| `topup_packages` | Пакеты докупки трафика именно для этого тарифа. Если поле не задано или списки пустые, докупка для тарифа не показывается в Web App и Telegram-боте. |
Для `traffic`-тарифа используются:
| Поле | Назначение |
| --- | --- |
| `traffic_packages` | Пакеты трафика в GB для рублей и Telegram Stars. |
| `conversion_rate_rub_per_gb` | Курс для конвертации оставшихся дней period-тарифа в GB при смене на traffic-тариф. |
Если у traffic-тарифа нет RUB-пакетов, `conversion_rate_rub_per_gb` обязателен.
## Period-тарифы
`period` продает доступ на срок с месячным лимитом трафика.
При покупке или продлении:
- дата начала берется от текущей активной подписки, если она еще действует, иначе от текущего времени;
- срок считается календарными месяцами через `add_months`;
- промокод может добавить бонусные дни к рассчитанному сроку;
- `tier_baseline_bytes` получает значение `monthly_gb`;
- `topup_balance_bytes` сохраняется из текущей активной подписки;
- `traffic_limit_bytes` становится `tier_baseline_bytes + topup_balance_bytes`;
- в Remnawave отправляется `trafficLimitStrategy = MONTH`;
- в Remnawave отправляются Internal Squads из тарифа;
- в Remnawave отправляется эффективный HWID-лимит тарифа.
`MONTH` означает, что сброс использованного трафика выполняет Remnawave. Бот не рассчитывает дату сброса самостоятельно и не хранит отдельный период сброса для period-тарифов.
Докупка трафика для period-тарифа увеличивает `topup_balance_bytes` и общий `traffic_limit_bytes`. Этот баланс сохраняется в подписке и учитывается при продлении period-тарифа. В панель отправляется актуальный лимит, а доступ переводится в `ACTIVE`.
## Premium-сквады и отдельный лимит
Тариф может включать дополнительный набор Internal Squads с отдельным лимитом трафика. Это удобно для сценария “обычные серверы без изменений, premium-серверы ограничены отдельно”.
```json
{
"squad_uuids": ["standard-squad-uuid"],
"premium_squad_uuids": ["premium-squad-uuid"],
"premium_monthly_gb": 50,
"premium_topup_packages": {
"rub": [{ "gb": 10, "price": 99 }],
"stars": [{ "gb": 10, "price": 2500 }]
}
}
```
Правила:
- обычный лимит тарифа продолжает работать через `trafficLimitBytes` Remnawave;
- premium-трафик считается отдельно по нодам, доступным из `premium_squad_uuids`;
- список UUID нод не хранится в тарифе: бот запрашивает accessible nodes каждого premium-сквада у Remnawave и кеширует результат;
- пока premium-лимит не исчерпан, пользователь получает `squad_uuids + premium_squad_uuids`;
- при исчерпании premium-лимита бот убирает только premium-сквады, обычный доступ остается;
- после докупки premium-трафика бот возвращает premium-сквады, если новый лимит снова больше использованного premium-трафика.
- докупленный premium-трафик не сгорает в конце месяца: каждый месяц сначала расходуется `premium_monthly_gb`, а докупленный остаток уменьшается только на трафик сверх месячного лимита;
- при новом календарном месяце счетчик premium-трафика и `premium_topup_used_bytes` сбрасываются, но `premium_topup_balance_bytes` переносится дальше.
Состояние хранится в подписке:
- `premium_baseline_bytes` - базовый premium-лимит тарифа;
- `premium_topup_balance_bytes` - оставшийся докупленный premium-трафик;
- `premium_topup_used_bytes` - часть докупленного premium-трафика, уже потраченная в текущем месяце;
- `premium_used_bytes` - использованный premium-трафик за текущий календарный месяц;
- `premium_period_start_at` - месяц, к которому относится `premium_used_bytes`;
- `premium_is_limited` - признак, что premium-сквад временно снят.
В пользовательском Web App premium-лимит показывается отдельной карточкой: использовано, лимит, остаток, докупленный переносимый остаток и список серверов/сквадов, на которые действует отдельное ограничение. В Telegram-разделе “Моя подписка” выводится тот же блок.
Предупреждения по premium-лимиту отправляются отдельно от обычного трафика на тех же процентах `TARIFF_TRAFFIC_WARNING_LEVELS`. Сообщение объясняет, что это именно premium-серверы, перечисляет серверы/сквады и ведет пользователя в докупку premium-трафика.
В Web App админке premium-сквады можно выбрать из выпадающего списка. Список берется из API Remnawave, поэтому UUID не нужно копировать вручную.
## Traffic-тарифы
`traffic` продает объем трафика без пользовательского срока действия.
При покупке:
- `end_date` ставится в дальнюю дату `2099-01-01 UTC`, если у активной подписки нет более поздней даты;
- `duration_months = 0`;
- `period_start_at = NULL`;
- `tier_baseline_bytes = 0`;
- `topup_balance_bytes` хранит доступный остаток трафика;
- в Remnawave отправляется `trafficLimitStrategy = NO_RESET`;
- автопродление и уведомления о скором окончании срока отключаются для такой подписки.
Очередная покупка добавляет GB к фактическому остатку:
```text
remaining = max(0, current_limit - current_used)
balance_after = remaining + purchased
limit_after = current_used + balance_after
```
Так пользователь не теряет уже оплаченный остаток, а Remnawave продолжает считать общий лимит от текущего использованного трафика.
Если докупка трафика вызывается для traffic-тарифа, она обрабатывается как покупка очередного пакета этого же traffic-тарифа.
## HWID-устройства
Тариф может задавать базовый лимит устройств и пакеты докупки:
```json
{
"hwid_device_limit": 5,
"hwid_device_packages": {
"rub": [{ "count": 1, "price": 99 }],
"stars": [{ "count": 1, "price": 2500 }]
}
}
```
Правила:
- `hwid_device_limit` хранит базовый лимит тарифа;
- `extra_hwid_devices` хранит количество докупленных устройств;
- эффективный лимит равен `hwid_device_limit + extra_hwid_devices`;
- базовый лимит `0` означает безлимит, в Remnawave отправляется `hwidDeviceLimit = 0`;
- при безлимитном базовом лимите докупка устройств не применяется;
- при смене тарифа базовый лимит берется из целевого тарифа, а докупленные устройства сохраняются;
- история докупок пишется в `hwid_device_purchases`;
- платеж хранит количество устройств в `payments.purchased_hwid_devices`.
Докупка устройств доступна в Web App через `/api/devices/topup-options` и `/api/payments`, а также в Telegram-боте из раздела устройств.
## Смена тарифа
Смена тарифа доступна для активных подписок с `tariff_key` и записывается в таблицу `tariff_changes`.
Варианты расчета:
| Переход | Поведение |
| --- | --- |
| `period -> period` | Остаток оплаченных дней оценивается по `effective_monthly_price_rub`, затем пересчитывается в дни целевого тарифа через месячную цену целевого тарифа. Количество дней округляется вниз. |
| `period -> period` с доплатой | Если целевой тариф дороже, может быть создан платеж `tariff_upgrade`; после оплаты применяется целевой тариф. |
| `period -> traffic` | Остаток оплаченных дней конвертируется в GB по `conversion_rate_rub_per_gb` или минимальной RUB-цене GB из пакетов целевого тарифа. |
| `traffic -> period` | Пользователь выбирает и оплачивает период целевого тарифа; остаток GB сохраняется как `topup_balance_bytes` поверх лимита period-тарифа. |
При смене тарифа бот меняет:
- `tariff_key`;
- Internal Squads в Remnawave;
- `trafficLimitBytes`;
- `trafficLimitStrategy`;
- базовый HWID-лимит;
- `effective_monthly_price_rub` для period-тарифов;
- `auto_renew_enabled` и уведомления для traffic-тарифов.
## Платежи
В платежах используются поля:
| Поле | Назначение |
| --- | --- |
| `sale_mode` | Тип продажи: `subscription`, `traffic_package`, `topup`, `premium_topup`, `tariff_upgrade`, `hwid_devices`. |
| `tariff_key` | Ключ тарифа, к которому относится платеж. |
| `purchased_gb` | Купленный объем GB для traffic-пакетов и докупки трафика. |
| `purchased_hwid_devices` | Количество устройств при докупке HWID. |
| `subscription_duration_months` | Количество месяцев для подписки на срок; также используется платежными обработчиками как числовое поле покупки. |
В callback и metadata платежных провайдеров `sale_mode` может передаваться с суффиксом тарифа, например `subscription@standard` или `topup@standard`. При активации платежа тариф сохраняется отдельно в `tariff_key`.
## Предупреждения и исчерпание трафика
Remnawave ограничивает доступ при достижении `trafficLimitBytes`, переводя пользователя в статус `LIMITED`. Бот не удаляет пользователя из Internal Squads при 100% использования трафика.
`TariffTrafficWorker` запускается, когда активен JSON-каталог тарифов. Раз в 300 секунд он:
- синхронизирует из панели `status`, `trafficLimitBytes`, `usedTrafficBytes` и `trafficLimitStrategy`;
- для period-тарифов выставляет `trafficLimitStrategy = MONTH`, если панель еще показывает другую стратегию;
- отправляет предупреждения на уровнях из `TARIFF_TRAFFIC_WARNING_LEVELS` (по умолчанию `85,90,95`);
- не отправляет `status=ACTIVE` при простой синхронизации стратегии, чтобы не снять статус `LIMITED`, выставленный Remnawave;
- дедуплицирует предупреждения через `traffic_warnings`.
Для period-тарифов дедупликация предупреждений привязана к началу текущего месяца. Для traffic-тарифов она учитывает текущий `trafficLimitBytes`, чтобы после покупки очередного пакета пользователь мог получить следующий набор предупреждений.
Подписки, которые были ограничены логикой предыдущих запусков бота (`is_throttled=True`), восстанавливаются воркером только когда лимит снова больше использованного трафика.
## Автопродление, пробный период и бонусы
Автопродление через YooKassa применяется к подпискам на срок. Для режима продажи трафика без JSON-каталога автопродление пропускается. Для traffic-тарифов JSON-каталога покупка является пакетом трафика, а не периодической подпиской.
Пробный период использует настройки `TRIAL_DURATION_DAYS`, `TRIAL_TRAFFIC_LIMIT_GB` и `TRIAL_TRAFFIC_STRATEGY`. Он не выбирает тариф из JSON-каталога.
Промокоды с бонусными днями применяются к покупке period-подписки. Реферальные бонусы по периодам также относятся к подпискам на срок; в режиме продажи трафика без JSON-каталога Web App не показывает детализацию бонусов по месяцам.
## Привязка существующих подписок
При запуске с активным JSON-каталогом бот заполняет активные подписки без `tariff_key`:
- `tariff_key` получает `default_tariff`;
- `tier_baseline_bytes` берется из текущего лимита подписки или из `monthly_gb` тарифа по умолчанию;
- `topup_balance_bytes` становится `0`, если значение отсутствовало;
- `period_start_at` очищается;
- `effective_monthly_price_rub` берется из последнего успешного платежа или из цены тарифа по умолчанию.
Это позволяет существующим активным подпискам отображаться и управляться в интерфейсах тарифов.