fix: bind HWID top-ups to subscription periods

This commit is contained in:
3252a8
2026-05-25 19:33:50 +03:00
parent 59fa301344
commit c68cb97964
49 changed files with 2027 additions and 119 deletions
+5 -3
View File
@@ -21,8 +21,9 @@ docker compose logs -f backend worker frontend
- `worker` только после успешных миграций;
- `frontend` как отдельный nginx-образ без Python runtime.
Миграции не запускаются внутри backend. Их выполняет отдельный сервис `migrate`, поэтому старт
приложения не создает гонки на схеме БД.
Основной путь миграций — отдельный сервис `migrate`. `backend` и `worker` также выполняют
безопасную проверку схемы на старте под PostgreSQL advisory lock, поэтому прямой запуск сервиса
без compose тоже применит недостающие миграции и не создаст гонку на схеме БД.
## Готовые папки запуска
@@ -71,7 +72,8 @@ docker compose logs migrate
```
`backend` и `worker` зависят от `migrate` через `service_completed_successfully`; если миграции
падают, приложение не стартует поверх неподготовленной БД.
падают, приложение не стартует поверх неподготовленной БД. При прямом запуске `backend` или
`worker` без compose тот же `init_db` применяет недостающие миграции перед стартом логики сервиса.
## Сервисы
+35 -9
View File
@@ -66,6 +66,17 @@ JSON-каталог может содержать несколько тариф
"rub": [{ "gb": 10, "price": 99 }],
"stars": [{ "gb": 10, "price": 2500 }]
},
"hwid_device_packages": {
"rub": [
{
"count": 1,
"price": 99,
"prices": { "1": 99, "3": 249 },
"min_price": 20
}
],
"stars": [{ "count": 1, "price": 50, "prices": { "1": 50, "3": 130 } }]
},
"enabled": true
}
]
@@ -88,7 +99,7 @@ JSON-каталог может содержать несколько тариф
| `tariffs[].premium_topup_packages` | Пакеты докупки premium-трафика в формате `{ "rub": [{ "gb": 10, "price": 99 }], "stars": [...] }`. Требуют `premium_squad_uuids`. |
| `tariffs[].billing_model` | Модель тарифа: `period` или `traffic`. |
| `tariffs[].hwid_device_limit` | Базовый лимит HWID-устройств. `0` означает безлимит, отсутствие поля использует `USER_HWID_DEVICE_LIMIT`. |
| `tariffs[].hwid_device_packages` | Пакеты докупки устройств: `{ "count": 1, "price": 99 }`. |
| `tariffs[].hwid_device_packages` | Пакеты докупки устройств. `price` — legacy/monthly fallback, `prices` задаёт полную цену пакета для периодов тарифа (`"1"`, `"3"`, `"6"`, `"12"`), `min_price` задаёт минимальную цену prorate-докупки. |
Для `period`-тарифа также используются:
@@ -210,8 +221,15 @@ limit_after = current_used + balance_after
{
"hwid_device_limit": 5,
"hwid_device_packages": {
"rub": [{ "count": 1, "price": 99 }],
"stars": [{ "count": 1, "price": 2500 }]
"rub": [
{
"count": 1,
"price": 99,
"prices": { "1": 99, "3": 249, "6": 449, "12": 799 },
"min_price": 20
}
],
"stars": [{ "count": 1, "price": 50, "prices": { "1": 50, "3": 130 } }]
}
}
```
@@ -219,11 +237,17 @@ limit_after = current_used + balance_after
Правила:
- `hwid_device_limit` хранит базовый лимит тарифа;
- `extra_hwid_devices` хранит количество докупленных устройств;
- эффективный лимит равен `hwid_device_limit + extra_hwid_devices`;
- `extra_hwid_devices` хранит только текущую активную сумму докупленных устройств;
- срок действия каждой докупки хранится в `hwid_device_purchases.valid_from` / `valid_until`;
- эффективный лимит равен `hwid_device_limit + active extra_hwid_devices`;
- базовый лимит `0` означает безлимит, в Remnawave отправляется `hwidDeviceLimit = 0`;
- при безлимитном базовом лимите докупка устройств не применяется;
- при смене тарифа базовый лимит берется из целевого тарифа, а докупленные устройства сохраняются;
- полная цена HWID-пакета берется из `prices[duration_months]`; если периода нет, используется fallback `price * duration_months`;
- фактическая цена докупки считается пропорционально оплачиваемому окну `valid_from -> valid_until` относительно периода подписки и фиксируется в платежe;
- для Telegram Stars цена округляется вверх до целого Stars, для RUB — вверх до копеек; `min_price` защищает от микроплатежей в конце периода;
- при продлении подписки докупленные устройства не продлеваются автоматически: старая докупка действует до прежнего `end_date`, а для нового срока создается отдельная `hwid_devices_renewal`-покупка;
- `traffic`-тарифы не показывают и не принимают докупку HWID-устройств, потому что у них нет срока подписки;
- при смене тарифа базовый лимит берется из целевого тарифа, а неиспользованная RUB-стоимость HWID-докупок конвертируется в дни нового period-тарифа или GB traffic-тарифа; XTR/Stars-докупки не конвертируются без явного курса и продолжают жить по своему `valid_until`;
- история докупок пишется в `hwid_device_purchases`;
- платеж хранит количество устройств в `payments.purchased_hwid_devices`.
@@ -237,9 +261,9 @@ limit_after = current_used + balance_after
| Переход | Поведение |
| --- | --- |
| `period -> period` | Остаток оплаченных дней оценивается по `effective_monthly_price_rub`, затем пересчитывается в дни целевого тарифа через месячную цену целевого тарифа. Количество дней округляется вниз. |
| `period -> period` с доплатой | Если целевой тариф дороже, может быть создан платеж `tariff_upgrade`; после оплаты применяется целевой тариф. |
| `period -> traffic` | Остаток оплаченных дней конвертируется в GB по `conversion_rate_rub_per_gb` или минимальной RUB-цене GB из пакетов целевого тарифа. |
| `period -> period` | Остаток оплаченных дней оценивается по `effective_monthly_price_rub`, затем пересчитывается в дни целевого тарифа через месячную цену целевого тарифа. Неиспользованная RUB-стоимость HWID-докупок добавляется к этому расчету как дополнительные дни. Количество дней округляется вниз. |
| `period -> period` с доплатой | Если целевой тариф дороже, может быть создан платеж `tariff_upgrade`; неиспользованная RUB-стоимость HWID-докупок уменьшает сумму доплаты. После оплаты применяется целевой тариф, а конвертированные HWID-окна закрываются. |
| `period -> traffic` | Остаток оплаченных дней и неиспользованная RUB-стоимость HWID-докупок конвертируются в GB по `conversion_rate_rub_per_gb` или минимальной RUB-цене GB из пакетов целевого тарифа. |
| `traffic -> period` | Пользователь выбирает и оплачивает период целевого тарифа; остаток GB сохраняется как `topup_balance_bytes` поверх лимита period-тарифа. |
При смене тарифа бот меняет:
@@ -262,6 +286,8 @@ limit_after = current_used + balance_after
| `tariff_key` | Ключ тарифа, к которому относится платеж. |
| `purchased_gb` | Купленный объем GB для traffic-пакетов и докупки трафика. |
| `purchased_hwid_devices` | Количество устройств при докупке HWID. |
| `hwid_valid_from`, `hwid_valid_until` | Зафиксированное окно действия HWID-докупки на момент создания платежа. |
| `hwid_pricing_period_months`, `hwid_proration_ratio`, `hwid_full_price` | Метаданные расчета цены HWID-докупки: период тарифа, коэффициент prorate и полная цена пакета для периода. |
| `subscription_duration_months` | Количество месяцев для подписки на срок; также используется платежными обработчиками как числовое поле покупки. |
В callback и metadata платежных провайдеров `sale_mode` может передаваться с суффиксом тарифа, например `subscription@standard` или `topup@standard`. При активации платежа тариф сохраняется отдельно в `tariff_key`.