Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab49f3cd1e | |||
| 732e8f7b8b | |||
| 9273a4e15b | |||
| d89293f76a | |||
| 174462183d | |||
| 783927be37 | |||
| 98c68dd8dc | |||
| 13585eab54 | |||
| 169336da43 | |||
| 9ef875f6bf | |||
| 3f54706a72 | |||
| 835e5e6f7f | |||
| b292442e8d | |||
| 9faaf5c963 | |||
| 117e88fe4a | |||
| 96bf08a07c | |||
| 4523403de5 | |||
| 02682e88c1 | |||
| 323b9a7e18 | |||
| 100dc3730f | |||
| 919047317a | |||
| 6ae5b59ed1 | |||
| b3123e2e85 | |||
| fbe0ca790e | |||
| 785648ba7a | |||
| 06f8728efa | |||
| 99f102a06e | |||
| d759dd3a66 | |||
| 73dc22b0b8 | |||
| 5a082f0eb8 | |||
| 073d5a119b | |||
| 2dab106bef | |||
| ec8c7b7242 | |||
| a4b96a089f | |||
| 269f8f6220 | |||
| f1beda2b89 | |||
| 6a00db32ae | |||
| 276bc5eb40 | |||
| 8678afd055 | |||
| 31340ad4a5 | |||
| 98ce47ce71 | |||
| 3462f5b3b1 |
+6
-10
@@ -37,9 +37,11 @@ TRIAL_SQUAD_UUID=
|
||||
# Сколько устройств доступно по дефолту при покупке платной подписки
|
||||
DEFAULT_DEVICE_LIMIT=3
|
||||
|
||||
# Максимум устройств достопных к покупке (0 = Нет лимита)
|
||||
MAX_DEVICES_LIMIT=15
|
||||
|
||||
# Дефолт параметры для подписок выданных через админку
|
||||
DEFAULT_TRAFFIC_LIMIT_GB=100
|
||||
DEFAULT_DEVICE_LIMIT=1
|
||||
|
||||
# ===== ГЛОБАЛЬНЫЙ ПАРАМЕТР ДЛЯ ВСЕХ ПОДПИСОК =====
|
||||
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
|
||||
@@ -71,16 +73,10 @@ PRICE_90_DAYS=36900
|
||||
PRICE_180_DAYS=69900
|
||||
PRICE_360_DAYS=109900
|
||||
|
||||
# Цены за трафик
|
||||
PRICE_TRAFFIC_5GB=1000
|
||||
PRICE_TRAFFIC_10GB=2000
|
||||
PRICE_TRAFFIC_25GB=3000
|
||||
PRICE_TRAFFIC_50GB=4000
|
||||
PRICE_TRAFFIC_100GB=6000
|
||||
PRICE_TRAFFIC_250GB=10000
|
||||
PRICE_TRAFFIC_UNLIMITED=15000
|
||||
# Выводимые пакеты трафика и их цены в копейках
|
||||
TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,100:15000:true,250:17000:false,500:19000:false,1000:19500:true,0:20000:true"
|
||||
|
||||
# Цена за дополнительное устройство
|
||||
# Цена за дополнительное устройство (DEFAULT_DEVICE_LIMIT идет бесплатно!)
|
||||
PRICE_PER_DEVICE=5000
|
||||
|
||||
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
|
||||
|
||||
@@ -130,9 +130,11 @@ TRIAL_SQUAD_UUID=
|
||||
# Сколько устройств доступно по дефолту при покупке платной подписки
|
||||
DEFAULT_DEVICE_LIMIT=3
|
||||
|
||||
# Максимум устройств достопных к покупке (0 = Нет лимита)
|
||||
MAX_DEVICES_LIMIT=15
|
||||
|
||||
# Дефолт параметры для подписок выданных через админку
|
||||
DEFAULT_TRAFFIC_LIMIT_GB=100
|
||||
DEFAULT_DEVICE_LIMIT=1
|
||||
|
||||
# ===== ГЛОБАЛЬНЫЙ ПАРАМЕТР ДЛЯ ВСЕХ ПОДПИСОК =====
|
||||
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
|
||||
@@ -164,16 +166,10 @@ PRICE_90_DAYS=36900
|
||||
PRICE_180_DAYS=69900
|
||||
PRICE_360_DAYS=109900
|
||||
|
||||
# Цены за трафик
|
||||
PRICE_TRAFFIC_5GB=1000
|
||||
PRICE_TRAFFIC_10GB=2000
|
||||
PRICE_TRAFFIC_25GB=3000
|
||||
PRICE_TRAFFIC_50GB=4000
|
||||
PRICE_TRAFFIC_100GB=6000
|
||||
PRICE_TRAFFIC_250GB=10000
|
||||
PRICE_TRAFFIC_UNLIMITED=15000
|
||||
# Выводимые пакеты трафика и их цены в копейках
|
||||
TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,100:15000:true,250:17000:false,500:19000:false,1000:19500:true,0:20000:true"
|
||||
|
||||
# Цена за дополнительное устройство
|
||||
# Цена за дополнительное устройство (DEFAULT_DEVICE_LIMIT идет бесплатно!)
|
||||
PRICE_PER_DEVICE=5000
|
||||
|
||||
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
|
||||
|
||||
+146
-17
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
from typing import List, Optional, Union
|
||||
from typing import List, Optional, Union, Dict
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import field_validator, Field
|
||||
from pathlib import Path
|
||||
@@ -25,6 +25,7 @@ class Settings(BaseSettings):
|
||||
DEFAULT_DEVICE_LIMIT: int = 1
|
||||
TRIAL_SQUAD_UUID: str
|
||||
DEFAULT_TRAFFIC_RESET_STRATEGY: str = "MONTH"
|
||||
MAX_DEVICES_LIMIT: int = 20
|
||||
|
||||
TRIAL_WARNING_HOURS: int = 2
|
||||
ENABLE_NOTIFICATIONS: bool = True
|
||||
@@ -44,13 +45,17 @@ class Settings(BaseSettings):
|
||||
PRICE_180_DAYS: int = 499000
|
||||
PRICE_360_DAYS: int = 899000
|
||||
|
||||
PRICE_TRAFFIC_5GB: int = 10000
|
||||
PRICE_TRAFFIC_10GB: int = 19000
|
||||
PRICE_TRAFFIC_25GB: int = 45000
|
||||
PRICE_TRAFFIC_50GB: int = 85000
|
||||
PRICE_TRAFFIC_100GB: int = 159000
|
||||
PRICE_TRAFFIC_250GB: int = 369000
|
||||
PRICE_TRAFFIC_UNLIMITED: int = 0
|
||||
PRICE_TRAFFIC_5GB: int = 2000
|
||||
PRICE_TRAFFIC_10GB: int = 3500
|
||||
PRICE_TRAFFIC_25GB: int = 7000
|
||||
PRICE_TRAFFIC_50GB: int = 11000
|
||||
PRICE_TRAFFIC_100GB: int = 15000
|
||||
PRICE_TRAFFIC_250GB: int = 17000
|
||||
PRICE_TRAFFIC_500GB: int = 19000
|
||||
PRICE_TRAFFIC_1000GB: int = 19500
|
||||
PRICE_TRAFFIC_UNLIMITED: int = 20000
|
||||
|
||||
TRAFFIC_PACKAGES_CONFIG: str = ""
|
||||
|
||||
PRICE_PER_DEVICE: int = 5000
|
||||
|
||||
@@ -292,6 +297,128 @@ class Settings(BaseSettings):
|
||||
def rubles_to_stars(self, rubles: float) -> int:
|
||||
return max(1, int(rubles / self.get_stars_rate()))
|
||||
|
||||
def get_traffic_packages(self) -> List[Dict]:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
packages = []
|
||||
config_str = self.TRAFFIC_PACKAGES_CONFIG.strip()
|
||||
|
||||
logger.info(f"CONFIG STRING: '{config_str}'")
|
||||
|
||||
if not config_str:
|
||||
logger.info("CONFIG EMPTY, USING FALLBACK")
|
||||
return self._get_fallback_traffic_packages()
|
||||
|
||||
logger.info("PARSING CONFIG...")
|
||||
|
||||
for package_config in config_str.split(','):
|
||||
package_config = package_config.strip()
|
||||
if not package_config:
|
||||
continue
|
||||
|
||||
parts = package_config.split(':')
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
gb = int(parts[0])
|
||||
price = int(parts[1])
|
||||
enabled = parts[2].lower() == 'true'
|
||||
|
||||
packages.append({
|
||||
"gb": gb,
|
||||
"price": price,
|
||||
"enabled": enabled
|
||||
})
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
logger.info(f"PARSED {len(packages)} packages from config")
|
||||
return packages if packages else self._get_fallback_traffic_packages()
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"ERROR PARSING CONFIG: {e}")
|
||||
return self._get_fallback_traffic_packages()
|
||||
|
||||
def _get_fallback_traffic_packages(self) -> List[Dict]:
|
||||
try:
|
||||
if self.TRAFFIC_PACKAGES_CONFIG.strip():
|
||||
packages = []
|
||||
for package_config in self.TRAFFIC_PACKAGES_CONFIG.split(','):
|
||||
package_config = package_config.strip()
|
||||
if not package_config:
|
||||
continue
|
||||
|
||||
parts = package_config.split(':')
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
gb = int(parts[0])
|
||||
price = int(parts[1])
|
||||
enabled = parts[2].lower() == 'true'
|
||||
|
||||
packages.append({
|
||||
"gb": gb,
|
||||
"price": price,
|
||||
"enabled": enabled
|
||||
})
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if packages:
|
||||
return packages
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return [
|
||||
{"gb": 5, "price": self.PRICE_TRAFFIC_5GB, "enabled": True},
|
||||
{"gb": 10, "price": self.PRICE_TRAFFIC_10GB, "enabled": True},
|
||||
{"gb": 25, "price": self.PRICE_TRAFFIC_25GB, "enabled": True},
|
||||
{"gb": 50, "price": self.PRICE_TRAFFIC_50GB, "enabled": True},
|
||||
{"gb": 100, "price": self.PRICE_TRAFFIC_100GB, "enabled": True},
|
||||
{"gb": 250, "price": self.PRICE_TRAFFIC_250GB, "enabled": True},
|
||||
{"gb": 500, "price": self.PRICE_TRAFFIC_500GB, "enabled": True},
|
||||
{"gb": 1000, "price": self.PRICE_TRAFFIC_1000GB, "enabled": True},
|
||||
{"gb": 0, "price": self.PRICE_TRAFFIC_UNLIMITED, "enabled": True},
|
||||
]
|
||||
|
||||
def get_traffic_price(self, gb: int) -> int:
|
||||
packages = self.get_traffic_packages()
|
||||
|
||||
for package in packages:
|
||||
if package["gb"] == gb and package["enabled"]:
|
||||
return package["price"]
|
||||
|
||||
|
||||
enabled_packages = [pkg for pkg in packages if pkg["enabled"]]
|
||||
if not enabled_packages:
|
||||
return 0
|
||||
|
||||
unlimited_package = next((pkg for pkg in enabled_packages if pkg["gb"] == 0), None)
|
||||
|
||||
finite_packages = [pkg for pkg in enabled_packages if pkg["gb"] > 0]
|
||||
if finite_packages:
|
||||
max_package = max(finite_packages, key=lambda x: x["gb"])
|
||||
|
||||
if gb > max_package["gb"]:
|
||||
if unlimited_package:
|
||||
return unlimited_package["price"]
|
||||
else:
|
||||
return max_package["price"]
|
||||
|
||||
suitable_packages = [pkg for pkg in finite_packages if pkg["gb"] >= gb]
|
||||
if suitable_packages:
|
||||
nearest_package = min(suitable_packages, key=lambda x: x["gb"])
|
||||
return nearest_package["price"]
|
||||
|
||||
if unlimited_package:
|
||||
return unlimited_package["price"]
|
||||
|
||||
return 0
|
||||
|
||||
model_config = {
|
||||
"env_file": ".env",
|
||||
"env_file_encoding": "utf-8"
|
||||
@@ -310,12 +437,14 @@ PERIOD_PRICES = {
|
||||
360: settings.PRICE_360_DAYS,
|
||||
}
|
||||
|
||||
TRAFFIC_PRICES = {
|
||||
5: settings.PRICE_TRAFFIC_5GB,
|
||||
10: settings.PRICE_TRAFFIC_10GB,
|
||||
25: settings.PRICE_TRAFFIC_25GB,
|
||||
50: settings.PRICE_TRAFFIC_50GB,
|
||||
100: settings.PRICE_TRAFFIC_100GB,
|
||||
250: settings.PRICE_TRAFFIC_250GB,
|
||||
0: settings.PRICE_TRAFFIC_UNLIMITED,
|
||||
}
|
||||
def get_traffic_prices() -> Dict[int, int]:
|
||||
packages = settings.get_traffic_packages()
|
||||
return {package["gb"]: package["price"] for package in packages}
|
||||
|
||||
TRAFFIC_PRICES = get_traffic_prices()
|
||||
|
||||
def refresh_traffic_prices():
|
||||
global TRAFFIC_PRICES
|
||||
TRAFFIC_PRICES = get_traffic_prices()
|
||||
|
||||
refresh_traffic_prices()
|
||||
|
||||
+314
-33
@@ -8,7 +8,7 @@ import json
|
||||
import os
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
|
||||
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
|
||||
from app.config import settings, PERIOD_PRICES, get_traffic_prices
|
||||
from app.states import SubscriptionStates
|
||||
from app.database.crud.subscription import (
|
||||
get_subscription_by_user_id, create_trial_subscription,
|
||||
@@ -43,6 +43,7 @@ from app.services.referral_service import process_referral_purchase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TRAFFIC_PRICES = get_traffic_prices()
|
||||
|
||||
async def show_subscription_info(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -80,7 +81,7 @@ async def show_subscription_info(
|
||||
if subscription.is_trial:
|
||||
actual_status = "trial_active"
|
||||
status_display = "Тестовая"
|
||||
status_emoji = "🎁"
|
||||
status_emoji = "🎯"
|
||||
else:
|
||||
actual_status = "paid_active"
|
||||
status_display = "Активна"
|
||||
@@ -116,26 +117,61 @@ async def show_subscription_info(
|
||||
subscription_type = "Триал" if subscription.is_trial else "Платная"
|
||||
|
||||
if subscription.traffic_limit_gb == 0:
|
||||
traffic_used_display = f"∞ (безлимит) / {subscription.traffic_used_gb:.1f} ГБ"
|
||||
traffic_used_display = f"∞ (безлимит) | Использовано: {subscription.traffic_used_gb:.1f} ГБ"
|
||||
else:
|
||||
traffic_used_display = f"{subscription.traffic_used_gb:.1f} / {subscription.traffic_limit_gb} ГБ"
|
||||
|
||||
devices_used = await get_current_devices_count(db_user)
|
||||
|
||||
devices_used_str = "—"
|
||||
devices_list = []
|
||||
devices_count = 0
|
||||
|
||||
try:
|
||||
if db_user.remnawave_uuid:
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
service = RemnaWaveService()
|
||||
|
||||
async with service.api as api:
|
||||
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
|
||||
|
||||
if response and 'response' in response:
|
||||
devices_info = response['response']
|
||||
devices_count = devices_info.get('total', 0)
|
||||
devices_list = devices_info.get('devices', [])
|
||||
devices_used_str = str(devices_count)
|
||||
logger.info(f"Найдено {devices_count} устройств для пользователя {db_user.telegram_id}")
|
||||
else:
|
||||
logger.warning(f"Не удалось получить информацию об устройствах для {db_user.telegram_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения устройств для отображения: {e}")
|
||||
devices_used_str = await get_current_devices_count(db_user)
|
||||
|
||||
servers_names = await get_servers_display_names(subscription.connected_squads)
|
||||
servers_display = servers_names if servers_names else "Нет серверов"
|
||||
|
||||
message = f"""👤 {db_user.full_name}
|
||||
━━━━━━━━━━━━━━━━━
|
||||
💰 Баланс: {settings.format_price(db_user.balance_kopeks)}
|
||||
📱 Подписка: {status_emoji} {status_display}{warning_text}
|
||||
━━━━━━━━━━━━━━━━━
|
||||
|
||||
📱 Информация о подписке
|
||||
🎭 Тип: {subscription_type}
|
||||
📅 Действует до: {subscription.end_date.strftime("%d.%m.%Y %H:%M")}
|
||||
⏰ Осталось: {time_left_text}
|
||||
📈 Трафик: {traffic_used_display}
|
||||
🌍 Серверы: {len(subscription.connected_squads)} стран
|
||||
📱 Устройства: {devices_used} / {subscription.device_limit}"""
|
||||
🌍 Серверы: {servers_display}
|
||||
📱 Устройства: {devices_used_str} / {subscription.device_limit}"""
|
||||
|
||||
if devices_list and len(devices_list) > 0:
|
||||
message += f"\n\n<blockquote>📱 <b>Подключенные устройства:</b>\n"
|
||||
for device in devices_list[:5]:
|
||||
platform = device.get('platform', 'Unknown')
|
||||
device_model = device.get('deviceModel', 'Unknown')
|
||||
device_info = f"{platform} - {device_model}"
|
||||
|
||||
if len(device_info) > 35:
|
||||
device_info = device_info[:32] + "..."
|
||||
message += f"• {device_info}\n"
|
||||
message += "</blockquote>"
|
||||
|
||||
if hasattr(subscription, 'subscription_url') and subscription.subscription_url:
|
||||
if actual_status in ['trial_active', 'paid_active']:
|
||||
@@ -154,6 +190,64 @@ async def show_subscription_info(
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
async def get_current_devices_detailed(db_user: User) -> dict:
|
||||
try:
|
||||
if not db_user.remnawave_uuid:
|
||||
return {"count": 0, "devices": []}
|
||||
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
service = RemnaWaveService()
|
||||
|
||||
async with service.api as api:
|
||||
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
|
||||
|
||||
if response and 'response' in response:
|
||||
devices_info = response['response']
|
||||
total_devices = devices_info.get('total', 0)
|
||||
devices_list = devices_info.get('devices', [])
|
||||
|
||||
return {
|
||||
"count": total_devices,
|
||||
"devices": devices_list[:5]
|
||||
}
|
||||
else:
|
||||
return {"count": 0, "devices": []}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения детальной информации об устройствах: {e}")
|
||||
return {"count": 0, "devices": []}
|
||||
|
||||
async def get_servers_display_names(squad_uuids: List[str]) -> str:
|
||||
"""
|
||||
Получает отображаемые названия серверов по их UUID
|
||||
"""
|
||||
if not squad_uuids:
|
||||
return "Нет серверов"
|
||||
|
||||
try:
|
||||
countries = await _get_available_countries()
|
||||
|
||||
server_names = []
|
||||
for uuid in squad_uuids:
|
||||
for country in countries:
|
||||
if country['uuid'] == uuid:
|
||||
server_names.append(country['name'])
|
||||
break
|
||||
|
||||
if not server_names:
|
||||
return f"{len(squad_uuids)} стран"
|
||||
|
||||
if len(server_names) > 6:
|
||||
displayed = ", ".join(server_names[:6])
|
||||
remaining = len(server_names) - 6
|
||||
return f"{displayed} и ещё {remaining}"
|
||||
else:
|
||||
return ", ".join(server_names)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения названий серверов: {e}")
|
||||
return f"{len(squad_uuids)} стран"
|
||||
|
||||
async def get_current_devices_count(db_user: User) -> str:
|
||||
try:
|
||||
if not db_user.remnawave_uuid:
|
||||
@@ -181,7 +275,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
|
||||
if subscription.is_trial:
|
||||
return 0
|
||||
|
||||
from app.config import TRAFFIC_PRICES, PERIOD_PRICES, settings
|
||||
from app.config import settings
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
@@ -197,7 +291,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
|
||||
subscription.connected_squads, db
|
||||
)
|
||||
|
||||
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
|
||||
traffic_cost = settings.get_traffic_price(subscription.traffic_limit_gb)
|
||||
devices_cost = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
|
||||
total_cost = base_cost + servers_cost + traffic_cost + devices_cost
|
||||
@@ -212,7 +306,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
|
||||
return total_cost
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"⚠ Ошибка расчета стоимости подписки: {e}")
|
||||
logger.error(f"⚠️ Ошибка расчета стоимости подписки: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -785,10 +879,14 @@ async def confirm_add_traffic(
|
||||
texts = get_texts(db_user.language)
|
||||
subscription = db_user.subscription
|
||||
|
||||
price = TRAFFIC_PRICES[traffic_gb]
|
||||
price = settings.get_traffic_price(traffic_gb)
|
||||
|
||||
if price == 0 and traffic_gb != 0:
|
||||
await callback.answer("⚠️ Цена для этого пакета не настроена", show_alert=True)
|
||||
return
|
||||
|
||||
if db_user.balance_kopeks < price:
|
||||
await callback.answer("⌛ Недостаточно средств на балансе", show_alert=True)
|
||||
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -798,7 +896,7 @@ async def confirm_add_traffic(
|
||||
)
|
||||
|
||||
if not success:
|
||||
await callback.answer("⌛ Ошибка списания средств", show_alert=True)
|
||||
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
|
||||
return
|
||||
|
||||
if traffic_gb == 0:
|
||||
@@ -853,6 +951,11 @@ async def confirm_add_traffic(
|
||||
|
||||
await callback.answer()
|
||||
|
||||
def update_traffic_prices():
|
||||
from app.config import refresh_traffic_prices
|
||||
refresh_traffic_prices()
|
||||
logger.info("🔄 TRAFFIC_PRICES обновлены из конфигурации")
|
||||
|
||||
|
||||
async def confirm_add_devices(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -864,10 +967,20 @@ async def confirm_add_devices(
|
||||
texts = get_texts(db_user.language)
|
||||
subscription = db_user.subscription
|
||||
|
||||
new_total_devices = subscription.device_limit + devices_count
|
||||
|
||||
if settings.MAX_DEVICES_LIMIT > 0 and new_total_devices > settings.MAX_DEVICES_LIMIT:
|
||||
await callback.answer(
|
||||
f"⚠️ Превышен максимальный лимит устройств ({settings.MAX_DEVICES_LIMIT}). "
|
||||
f"У вас: {subscription.device_limit}, добавляете: {devices_count}",
|
||||
show_alert=True
|
||||
)
|
||||
return
|
||||
|
||||
price = devices_count * settings.PRICE_PER_DEVICE
|
||||
|
||||
if db_user.balance_kopeks < price:
|
||||
await callback.answer("❌ Недостаточно средств на балансе", show_alert=True)
|
||||
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -877,7 +990,7 @@ async def confirm_add_devices(
|
||||
)
|
||||
|
||||
if not success:
|
||||
await callback.answer("❌ Ошибка списания средств", show_alert=True)
|
||||
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
|
||||
return
|
||||
|
||||
await add_subscription_devices(db, subscription, devices_count)
|
||||
@@ -1146,12 +1259,19 @@ async def select_period(
|
||||
data['total_price'] = PERIOD_PRICES[period_days]
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
fixed_traffic_price = TRAFFIC_PRICES.get(settings.get_fixed_traffic_limit(), 0)
|
||||
fixed_traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
|
||||
data['total_price'] += fixed_traffic_price
|
||||
data['traffic_gb'] = settings.get_fixed_traffic_limit()
|
||||
|
||||
await state.set_data(data)
|
||||
|
||||
if settings.is_traffic_selectable():
|
||||
available_packages = [pkg for pkg in settings.get_traffic_packages() if pkg['enabled']]
|
||||
|
||||
if not available_packages:
|
||||
await callback.answer("⚠️ Пакеты трафика не настроены", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_TRAFFIC,
|
||||
reply_markup=get_traffic_packages_keyboard(db_user.language)
|
||||
@@ -1179,6 +1299,56 @@ async def select_period(
|
||||
|
||||
await callback.answer()
|
||||
|
||||
async def refresh_traffic_config():
|
||||
try:
|
||||
from app.config import refresh_traffic_prices
|
||||
refresh_traffic_prices()
|
||||
|
||||
packages = settings.get_traffic_packages()
|
||||
enabled_count = sum(1 for pkg in packages if pkg['enabled'])
|
||||
|
||||
logger.info(f"🔄 Конфигурация трафика обновлена: {enabled_count} активных пакетов")
|
||||
for pkg in packages:
|
||||
if pkg['enabled']:
|
||||
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
|
||||
logger.info(f" 📦 {gb_text}: {pkg['price']/100}₽")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ Ошибка обновления конфигурации трафика: {e}")
|
||||
return False
|
||||
|
||||
async def get_traffic_packages_info() -> str:
|
||||
try:
|
||||
packages = settings.get_traffic_packages()
|
||||
|
||||
info_lines = ["📦 Настроенные пакеты трафика:"]
|
||||
|
||||
enabled_packages = [pkg for pkg in packages if pkg['enabled']]
|
||||
disabled_packages = [pkg for pkg in packages if not pkg['enabled']]
|
||||
|
||||
if enabled_packages:
|
||||
info_lines.append("\n✅ Активные:")
|
||||
for pkg in enabled_packages:
|
||||
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
|
||||
info_lines.append(f" • {gb_text}: {pkg['price']/100}₽")
|
||||
|
||||
if disabled_packages:
|
||||
info_lines.append("\n❌ Отключенные:")
|
||||
for pkg in disabled_packages:
|
||||
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
|
||||
info_lines.append(f" • {gb_text}: {pkg['price']/100}₽")
|
||||
|
||||
info_lines.append(f"\n📊 Всего пакетов: {len(packages)}")
|
||||
info_lines.append(f"🟢 Активных: {len(enabled_packages)}")
|
||||
info_lines.append(f"🔴 Отключенных: {len(disabled_packages)}")
|
||||
|
||||
return "\n".join(info_lines)
|
||||
|
||||
except Exception as e:
|
||||
return f"⚠️ Ошибка получения информации: {e}"
|
||||
|
||||
async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSession):
|
||||
|
||||
devices_used = await get_current_devices_count(db_user)
|
||||
@@ -1257,7 +1427,10 @@ async def select_traffic(
|
||||
|
||||
data = await state.get_data()
|
||||
data['traffic_gb'] = traffic_gb
|
||||
data['total_price'] += TRAFFIC_PRICES[traffic_gb]
|
||||
|
||||
traffic_price = settings.get_traffic_price(traffic_gb)
|
||||
data['total_price'] += traffic_price
|
||||
|
||||
await state.set_data(data)
|
||||
|
||||
if await _should_show_countries_management():
|
||||
@@ -1288,7 +1461,6 @@ async def select_country(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
|
||||
country_uuid = callback.data.split('_')[1]
|
||||
data = await state.get_data()
|
||||
|
||||
@@ -1300,7 +1472,7 @@ async def select_country(
|
||||
|
||||
countries = await _get_available_countries()
|
||||
|
||||
base_price = PERIOD_PRICES[data['period_days']] + TRAFFIC_PRICES[data['traffic_gb']]
|
||||
base_price = PERIOD_PRICES[data['period_days']] + settings.get_traffic_price(data['traffic_gb'])
|
||||
|
||||
try:
|
||||
subscription_service = SubscriptionService()
|
||||
@@ -1360,7 +1532,7 @@ async def select_devices(
|
||||
|
||||
base_price = (
|
||||
PERIOD_PRICES[data['period_days']] +
|
||||
TRAFFIC_PRICES[data['traffic_gb']]
|
||||
settings.get_traffic_price(data['traffic_gb'])
|
||||
)
|
||||
|
||||
countries = await _get_available_countries()
|
||||
@@ -1387,7 +1559,7 @@ async def devices_continue(
|
||||
db: AsyncSession
|
||||
):
|
||||
if not callback.data == "devices_continue":
|
||||
await callback.answer("⌛ Некорректный запрос", show_alert=True)
|
||||
await callback.answer("⚠️ Некорректный запрос", show_alert=True)
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
@@ -1410,9 +1582,11 @@ async def devices_continue(
|
||||
base_price = PERIOD_PRICES[data['period_days']]
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
|
||||
traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
|
||||
final_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
|
||||
traffic_price = settings.get_traffic_price(data['traffic_gb'])
|
||||
final_traffic_gb = data['traffic_gb']
|
||||
|
||||
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
total_price = base_price + traffic_price + countries_price + devices_price
|
||||
@@ -1421,10 +1595,10 @@ async def devices_continue(
|
||||
await state.set_data(data)
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
if data['traffic_gb'] == 0:
|
||||
if final_traffic_gb == 0:
|
||||
traffic_display = "Безлимитный"
|
||||
else:
|
||||
traffic_display = f"{data['traffic_gb']} ГБ"
|
||||
traffic_display = f"{final_traffic_gb} ГБ"
|
||||
else:
|
||||
if data['traffic_gb'] == 0:
|
||||
traffic_display = "Безлимитный"
|
||||
@@ -1477,10 +1651,10 @@ async def confirm_purchase(
|
||||
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
traffic_price = TRAFFIC_PRICES.get(settings.get_fixed_traffic_limit(), 0)
|
||||
traffic_price = settings.get_traffic_price(settings.get_fixed_traffic_limit())
|
||||
final_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
|
||||
traffic_price = settings.get_traffic_price(data['traffic_gb'])
|
||||
final_traffic_gb = data['traffic_gb']
|
||||
|
||||
final_price = base_price + traffic_price + countries_price + devices_price
|
||||
@@ -1568,7 +1742,7 @@ async def confirm_purchase(
|
||||
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
if not remnawave_user:
|
||||
logger.error(f"❌ Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
|
||||
logger.error(f"⚠️ Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
|
||||
logger.info(f"🔄 Fallback: принудительное создание нового RemnaWave пользователя")
|
||||
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
@@ -1634,6 +1808,91 @@ async def confirm_purchase(
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
|
||||
async def add_traffic(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
if settings.is_traffic_fixed():
|
||||
await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True)
|
||||
return
|
||||
|
||||
traffic_gb = int(callback.data.split('_')[2])
|
||||
texts = get_texts(db_user.language)
|
||||
subscription = db_user.subscription
|
||||
|
||||
price = settings.get_traffic_price(traffic_gb)
|
||||
|
||||
if price == 0 and traffic_gb != 0:
|
||||
await callback.answer("⚠️ Цена для этого пакета не настроена", show_alert=True)
|
||||
return
|
||||
|
||||
if db_user.balance_kopeks < price:
|
||||
await callback.answer("⚠️ Недостаточно средств на балансе", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
success = await subtract_user_balance(
|
||||
db, db_user, price,
|
||||
f"Добавление {traffic_gb} ГБ трафика"
|
||||
)
|
||||
|
||||
if not success:
|
||||
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
|
||||
return
|
||||
|
||||
if traffic_gb == 0:
|
||||
subscription.traffic_limit_gb = 0
|
||||
else:
|
||||
await add_subscription_traffic(db, subscription, traffic_gb)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
await create_transaction(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
type=TransactionType.SUBSCRIPTION_PAYMENT,
|
||||
amount_kopeks=price,
|
||||
description=f"Добавление {traffic_gb} ГБ трафика"
|
||||
)
|
||||
|
||||
try:
|
||||
await process_referral_purchase(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
purchase_amount_kopeks=price,
|
||||
transaction_id=None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки реферальной покупки: {e}")
|
||||
|
||||
await db.refresh(db_user)
|
||||
await db.refresh(subscription)
|
||||
|
||||
success_text = f"✅ Трафик успешно добавлен!\n\n"
|
||||
if traffic_gb == 0:
|
||||
success_text += "🎉 Теперь у вас безлимитный трафик!"
|
||||
else:
|
||||
success_text += f"📈 Добавлено: {traffic_gb} ГБ\n"
|
||||
success_text += f"Новый лимит: {texts.format_traffic(subscription.traffic_limit_gb)}"
|
||||
|
||||
await callback.message.edit_text(
|
||||
success_text,
|
||||
reply_markup=get_back_keyboard(db_user.language)
|
||||
)
|
||||
|
||||
logger.info(f"✅ Пользователь {db_user.telegram_id} добавил {traffic_gb} ГБ трафика")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления трафика: {e}")
|
||||
await callback.message.edit_text(
|
||||
texts.ERROR,
|
||||
reply_markup=get_back_keyboard(db_user.language)
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
async def create_paid_subscription_with_traffic_mode(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
@@ -1666,6 +1925,15 @@ async def create_paid_subscription_with_traffic_mode(
|
||||
|
||||
return subscription
|
||||
|
||||
def validate_traffic_price(gb: int) -> bool:
|
||||
from app.config import settings
|
||||
|
||||
price = settings.get_traffic_price(gb)
|
||||
if gb == 0:
|
||||
return True
|
||||
|
||||
return price > 0
|
||||
|
||||
|
||||
async def handle_subscription_settings(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -2455,6 +2723,16 @@ async def handle_specific_app_guide(
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
async def handle_no_traffic_packages(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User
|
||||
):
|
||||
await callback.answer(
|
||||
"⚠️ В данный момент нет доступных пакетов трафика. "
|
||||
"Обратитесь в техподдержку для получения информации.",
|
||||
show_alert=True
|
||||
)
|
||||
|
||||
|
||||
async def handle_open_subscription_link(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -2566,6 +2844,7 @@ def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMa
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
update_traffic_prices()
|
||||
|
||||
dp.callback_query.register(
|
||||
show_subscription_info,
|
||||
@@ -2587,7 +2866,6 @@ def register_handlers(dp: Dispatcher):
|
||||
F.data.in_(["menu_buy", "subscription_upgrade"])
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_add_countries,
|
||||
F.data == "subscription_add_countries"
|
||||
@@ -2643,7 +2921,6 @@ def register_handlers(dp: Dispatcher):
|
||||
F.data == "confirm_reset_devices"
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
select_period,
|
||||
F.data.startswith("period_"),
|
||||
@@ -2674,7 +2951,6 @@ def register_handlers(dp: Dispatcher):
|
||||
SubscriptionStates.confirming_purchase
|
||||
)
|
||||
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_autopay_menu,
|
||||
F.data == "subscription_autopay"
|
||||
@@ -2756,3 +3032,8 @@ def register_handlers(dp: Dispatcher):
|
||||
handle_subscription_settings,
|
||||
F.data == "subscription_settings"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_no_traffic_packages,
|
||||
F.data == "no_traffic_packages"
|
||||
)
|
||||
|
||||
+88
-29
@@ -242,28 +242,57 @@ def get_subscription_period_keyboard(language: str = "ru") -> InlineKeyboardMark
|
||||
|
||||
|
||||
def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
return get_back_keyboard(language)
|
||||
|
||||
logger.info(f"🔍 RAW CONFIG: '{settings.TRAFFIC_PACKAGES_CONFIG}'")
|
||||
|
||||
all_packages = settings.get_traffic_packages()
|
||||
logger.info(f"🔍 ALL PACKAGES: {all_packages}")
|
||||
|
||||
enabled_packages = [pkg for pkg in all_packages if pkg['enabled']]
|
||||
disabled_packages = [pkg for pkg in all_packages if not pkg['enabled']]
|
||||
|
||||
logger.info(f"🔍 ENABLED: {len(enabled_packages)} packages")
|
||||
logger.info(f"🔍 DISABLED: {len(disabled_packages)} packages")
|
||||
|
||||
for pkg in disabled_packages:
|
||||
logger.info(f"🔍 DISABLED PACKAGE: {pkg['gb']}GB = {pkg['price']} kopeks, enabled={pkg['enabled']}")
|
||||
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
packages = [
|
||||
(5, texts.TRAFFIC_5GB),
|
||||
(10, texts.TRAFFIC_10GB),
|
||||
(25, texts.TRAFFIC_25GB),
|
||||
(50, texts.TRAFFIC_50GB),
|
||||
(100, texts.TRAFFIC_100GB),
|
||||
(250, texts.TRAFFIC_250GB),
|
||||
(0, texts.TRAFFIC_UNLIMITED)
|
||||
]
|
||||
traffic_packages = settings.get_traffic_packages()
|
||||
|
||||
for gb, text in packages:
|
||||
for package in traffic_packages:
|
||||
gb = package["gb"]
|
||||
price = package["price"]
|
||||
enabled = package["enabled"]
|
||||
|
||||
if not enabled:
|
||||
continue
|
||||
|
||||
if gb == 0:
|
||||
text = f"♾️ Безлимит - {settings.format_price(package['price'])}"
|
||||
else:
|
||||
text = f"📊 {gb} ГБ - {settings.format_price(package['price'])}"
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=text, callback_data=f"traffic_{gb}")
|
||||
])
|
||||
|
||||
if not keyboard:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⚠️ Пакеты трафика не настроены",
|
||||
callback_data="no_traffic_packages"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="subscription_config_back")
|
||||
@@ -271,7 +300,6 @@ def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def get_countries_keyboard(countries: List[dict], selected: List[str], language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
@@ -315,21 +343,30 @@ def get_devices_keyboard(current: int, language: str = "ru") -> InlineKeyboardMa
|
||||
keyboard = []
|
||||
|
||||
start_devices = settings.DEFAULT_DEVICE_LIMIT
|
||||
end_devices = max(start_devices + 3, 6)
|
||||
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 50
|
||||
end_devices = min(max_devices + 1, start_devices + 10)
|
||||
|
||||
buttons = []
|
||||
|
||||
for devices in range(start_devices, end_devices):
|
||||
price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
price_text = f" (+{texts.format_price(price)})" if price > 0 else " (включено)"
|
||||
price_text = f" (+{texts.format_price(price)})" if price > 0 else " (вкл.)"
|
||||
emoji = "✅" if devices == current else "⚪"
|
||||
|
||||
device_word = _get_device_declension(devices)
|
||||
button_text = f"{emoji} {devices}{price_text}"
|
||||
|
||||
keyboard.append([
|
||||
buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text=f"{emoji} {devices} {device_word}{price_text}",
|
||||
text=button_text,
|
||||
callback_data=f"devices_{devices}"
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
for i in range(0, len(buttons), 2):
|
||||
if i + 1 < len(buttons):
|
||||
keyboard.append([buttons[i], buttons[i + 1]])
|
||||
else:
|
||||
keyboard.append([buttons[i]])
|
||||
|
||||
keyboard.extend([
|
||||
[InlineKeyboardButton(text="✅ Продолжить", callback_data="devices_continue")],
|
||||
@@ -628,34 +665,48 @@ def get_add_traffic_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
packages = [
|
||||
(5, f"📊 +5 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_5GB)}"),
|
||||
(10, f"📊 +10 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_10GB)}"),
|
||||
(25, f"📊 +25 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_25GB)}"),
|
||||
(50, f"📊 +50 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_50GB)}"),
|
||||
(100, f"📊 +100 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_100GB)}"),
|
||||
(0, f"📊 Безлимит - {settings.format_price(settings.PRICE_TRAFFIC_UNLIMITED)}")
|
||||
]
|
||||
traffic_packages = settings.get_traffic_packages()
|
||||
|
||||
for gb, text in packages:
|
||||
for package in traffic_packages:
|
||||
gb = package["gb"]
|
||||
price = package["price"]
|
||||
enabled = package["enabled"]
|
||||
|
||||
if not enabled:
|
||||
continue
|
||||
|
||||
if gb == 0:
|
||||
text = f"📊 Безлимит - {settings.format_price(package['price'])}"
|
||||
else:
|
||||
text = f"📊 +{gb} ГБ - {settings.format_price(package['price'])}"
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=text, callback_data=f"add_traffic_{gb}")
|
||||
])
|
||||
|
||||
if not keyboard:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⚠️ Пакеты трафика не настроены",
|
||||
callback_data="no_traffic_packages"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def get_add_devices_keyboard(current_devices: int, language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
max_devices = 10
|
||||
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 100
|
||||
|
||||
for add_count in range(1, min(6, max_devices - current_devices + 1)):
|
||||
max_add = min(5, max_devices - current_devices)
|
||||
|
||||
for add_count in range(1, max_add + 1):
|
||||
price = add_count * settings.PRICE_PER_DEVICE
|
||||
total_devices = current_devices + add_count
|
||||
|
||||
@@ -668,6 +719,14 @@ def get_add_devices_keyboard(current_devices: int, language: str = "ru") -> Inli
|
||||
)
|
||||
])
|
||||
|
||||
if max_add == 0:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⚠️ Достигнут максимум устройств",
|
||||
callback_data="max_devices_reached"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
|
||||
])
|
||||
|
||||
@@ -107,9 +107,8 @@ class RussianTexts(Texts):
|
||||
REFERRAL_CODE_SKIP = "⏭️ Пропустить"
|
||||
|
||||
MAIN_MENU = """👤 <b>{user_name}</b>
|
||||
━━━━━━━━━━━━━━━━━
|
||||
|
||||
📱 <b>Подписка:</b> {subscription_status}
|
||||
━━━━━━━━━━━━━━━━━
|
||||
|
||||
Выберите действие:
|
||||
"""
|
||||
|
||||
@@ -229,36 +229,40 @@ class SubscriptionService:
|
||||
devices: int,
|
||||
db: AsyncSession
|
||||
) -> Tuple[int, List[int]]:
|
||||
|
||||
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
|
||||
|
||||
from app.config import PERIOD_PRICES
|
||||
from app.database.crud.server_squad import get_server_squad_by_id
|
||||
|
||||
|
||||
if settings.MAX_DEVICES_LIMIT > 0 and devices > settings.MAX_DEVICES_LIMIT:
|
||||
raise ValueError(f"Превышен максимальный лимит устройств: {settings.MAX_DEVICES_LIMIT}")
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
traffic_price = TRAFFIC_PRICES.get(traffic_gb, 0)
|
||||
|
||||
|
||||
traffic_price = settings.get_traffic_price(traffic_gb)
|
||||
|
||||
server_prices = []
|
||||
total_servers_price = 0
|
||||
|
||||
|
||||
for server_id in server_squad_ids:
|
||||
server = await get_server_squad_by_id(db, server_id)
|
||||
if server and server.is_available and not server.is_full:
|
||||
server_prices.append(server.price_kopeks)
|
||||
total_servers_price += server.price_kopeks
|
||||
logger.debug(f"🏷️ Сервер {server.display_name}: {server.price_kopeks/100}₽")
|
||||
logger.debug(f"Сервер {server.display_name}: {server.price_kopeks/100}₽")
|
||||
else:
|
||||
server_prices.append(0)
|
||||
logger.warning(f"⚠️ Сервер ID {server_id} недоступен")
|
||||
|
||||
logger.warning(f"Сервер ID {server_id} недоступен")
|
||||
|
||||
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
|
||||
total_price = base_price + traffic_price + total_servers_price + devices_price
|
||||
|
||||
logger.info(f"💰 Расчет стоимости новой подписки:")
|
||||
logger.info(f" 📅 Период {period_days} дней: {base_price/100}₽")
|
||||
logger.info(f" 📊 Трафик {traffic_gb} ГБ: {traffic_price/100}₽")
|
||||
logger.info(f" 🌍 Серверы ({len(server_squad_ids)}): {total_servers_price/100}₽")
|
||||
logger.info(f" 📱 Устройства ({devices}): {devices_price/100}₽")
|
||||
logger.info(f" 💎 ИТОГО: {total_price/100}₽")
|
||||
logger.info(f"Расчет стоимости новой подписки:")
|
||||
logger.info(f" Период {period_days} дней: {base_price/100}₽")
|
||||
logger.info(f" Трафик {traffic_gb} ГБ: {traffic_price/100}₽")
|
||||
logger.info(f" Серверы ({len(server_squad_ids)}): {total_servers_price/100}₽")
|
||||
logger.info(f" Устройства ({devices}): {devices_price/100}₽")
|
||||
logger.info(f" ИТОГО: {total_price/100}₽")
|
||||
|
||||
return total_price, server_prices
|
||||
|
||||
@@ -269,7 +273,7 @@ class SubscriptionService:
|
||||
db: AsyncSession
|
||||
) -> int:
|
||||
try:
|
||||
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
|
||||
from app.config import PERIOD_PRICES
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
|
||||
@@ -279,7 +283,7 @@ class SubscriptionService:
|
||||
|
||||
devices_price = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||||
|
||||
traffic_price = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
|
||||
traffic_price = settings.get_traffic_price(subscription.traffic_limit_gb)
|
||||
|
||||
total_price = base_price + servers_price + devices_price + traffic_price
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy import delete, select, update
|
||||
|
||||
from app.database.crud.user import (
|
||||
get_user_by_id, get_user_by_telegram_id, get_users_list,
|
||||
@@ -13,7 +13,7 @@ from app.database.crud.transaction import get_user_transactions_count
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
from app.database.models import (
|
||||
User, UserStatus, Subscription, Transaction, PromoCodeUse,
|
||||
ReferralEarning, SubscriptionServer
|
||||
ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory
|
||||
)
|
||||
from app.config import settings
|
||||
|
||||
@@ -242,9 +242,81 @@ class UserService:
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
|
||||
logger.info(f" ✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка деактивации RemnaWave пользователя: {e}")
|
||||
logger.warning(f" ⚠️ Ошибка деактивации RemnaWave: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import YooKassaPayment
|
||||
from sqlalchemy import select
|
||||
|
||||
yookassa_result = await db.execute(
|
||||
select(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
|
||||
)
|
||||
yookassa_payments = yookassa_result.scalars().all()
|
||||
|
||||
if yookassa_payments:
|
||||
logger.info(f" 🔄 Удаляем {len(yookassa_payments)} YooKassa платежей")
|
||||
await db.execute(
|
||||
delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" ✅ YooKassa платежи удалены")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления YooKassa платежей: {e}")
|
||||
|
||||
try:
|
||||
transactions_result = await db.execute(
|
||||
select(Transaction).where(Transaction.user_id == user_id)
|
||||
)
|
||||
transactions = transactions_result.scalars().all()
|
||||
|
||||
if transactions:
|
||||
logger.info(f" 🔄 Удаляем {len(transactions)} транзакций")
|
||||
await db.execute(
|
||||
delete(Transaction).where(Transaction.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" ✅ Транзакции удалены")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления транзакций: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалены использования промокодов пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления использований промокодов: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалены реферальные доходы пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления реферальных доходов: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.referral_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалены реферальные записи о пользователе {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления реферальных записей: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import BroadcastHistory
|
||||
await db.execute(
|
||||
delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалена история рассылок админа {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления истории рассылок: {e}")
|
||||
|
||||
if user.subscription:
|
||||
try:
|
||||
@@ -253,74 +325,47 @@ class UserService:
|
||||
SubscriptionServer.subscription_id == user.subscription.id
|
||||
)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления SubscriptionServer: {e}")
|
||||
logger.error(f" ❌ Ошибка удаления SubscriptionServer: {e}")
|
||||
|
||||
if user.subscription:
|
||||
try:
|
||||
await db.delete(user.subscription)
|
||||
logger.info(f"🗑️ Удалена подписка пользователя {user_id}")
|
||||
from app.database.models import Subscription
|
||||
await db.execute(
|
||||
delete(Subscription).where(Subscription.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалена подписка пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления подписки: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(PromoCodeUse).where(PromoCodeUse.user_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены использования промокодов пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления использований промокодов: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.user_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены реферальные доходы пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления реферальных доходов: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.referral_id == user.id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены реферальные записи о пользователе {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления реферальных записей: {e}")
|
||||
logger.error(f" ❌ Ошибка удаления подписки: {e}")
|
||||
|
||||
try:
|
||||
from sqlalchemy import update
|
||||
await db.execute(
|
||||
referrals_result = await db.execute(
|
||||
update(User)
|
||||
.where(User.referred_by_id == user.id)
|
||||
.where(User.referred_by_id == user_id)
|
||||
.values(referred_by_id=None)
|
||||
)
|
||||
logger.info(f"🗑️ Очищены реферальные ссылки у рефералов пользователя {user_id}")
|
||||
if referrals_result.rowcount > 0:
|
||||
logger.info(f" 🔗 Очищены реферальные ссылки у {referrals_result.rowcount} рефералов")
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}")
|
||||
logger.error(f" ❌ Ошибка очистки реферальных ссылок: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(Transaction).where(Transaction.user_id == user.id)
|
||||
delete(User).where(User.id == user_id)
|
||||
)
|
||||
logger.info(f"🗑️ Удалены транзакции пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления транзакций: {e}")
|
||||
|
||||
try:
|
||||
user.status = UserStatus.DELETED.value
|
||||
user.balance_kopeks = 0
|
||||
user.remnawave_uuid = None
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"✅ Пользователь {user_id} помечен как удаленный и обнулен")
|
||||
logger.info(f" ✅ Пользователь {user_id} окончательно удален из базы")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления статуса пользователя: {e}")
|
||||
logger.error(f" ❌ Ошибка финального удаления пользователя: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
logger.info(f"✅ Пользователь {user_id} (Telegram ID: {user.telegram_id}) полностью удален админом {admin_id}")
|
||||
logger.info(f"✅ Пользователь {user.telegram_id} (ID: {user_id}) полностью удален администратором {admin_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user