diff --git a/app-config.json b/app-config.json
index 4f5a919f..f53cea84 100644
--- a/app-config.json
+++ b/app-config.json
@@ -1,163 +1,233 @@
{
- "ios": [
- {
- "id": "happ",
- "name": "Happ",
- "isFeatured": true,
- "urlScheme": "happ://add/",
- "installationStep": {
- "buttons": [
- {
- "buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215",
- "buttonText": {
- "en": "App Store [EU]",
- "ru": "App Store [EU]"
- }
- },
- {
- "buttonLink": "https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973",
- "buttonText": {
- "en": "App Store [RU]",
- "ru": "App Store [RU]"
+ "config": {},
+ "platforms": {
+ "ios": [
+ {
+ "id": "happ",
+ "name": "Happ",
+ "isFeatured": true,
+ "urlScheme": "happ://add/",
+ "installationStep": {
+ "buttons": [
+ {
+ "buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215",
+ "buttonText": {
+ "en": "App Store [EU]",
+ "fa": "App Store [EU]",
+ "ru": "App Store [EU]",
+ "zh": "App Store [EU]"
+ }
+ },
+ {
+ "buttonLink": "https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973",
+ "buttonText": {
+ "en": "App Store [RU]",
+ "fa": "App Store [RU]",
+ "ru": "App Store [RU]",
+ "zh": "App Store [RU]"
+ }
}
+ ],
+ "description": {
+ "en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
+ "fa": "صفحه را در App Store باز کنید و برنامه را نصب کنید. آن را اجرا کنید، در پنجره مجوز پیکربندی VPN روی Allow کلیک کنید و رمز عبور خود را وارد کنید.",
+ "ru": "Откройте страницу в App Store и установите приложение. Запустите его, в окне разрешения VPN-конфигурации нажмите Allow и введите свой пароль.",
+ "zh": "在 App Store 中打开页面并安装应用。启动应用后,在 VPN 配置权限窗口中点击\"允许\"并输入您的密码。"
+ }
+ },
+ "addSubscriptionStep": {
+ "description": {
+ "en": "Click the button below — the app will open and the subscription will be added automatically",
+ "fa": "برای افزودن خودکار اشتراک روی دکمه زیر کلیک کنید - برنامه باز خواهد شد",
+ "ru": "Нажмите кнопку ниже — приложение откроется, и подписка добавится автоматически.",
+ "zh": "点击下方按钮 — 应用将打开并自动添加订阅"
+ }
+ },
+ "connectAndUseStep": {
+ "description": {
+ "en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
+ "fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
+ "ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
+ "zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
}
- ],
- "description": {
- "en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
- "ru": "Откройте страницу в App Store и установите приложение. Вернись на страницу с подпиской"
- }
- },
- "addSubscriptionStep": {
- "description": {
- "en": "Click the button below — the app will open and the subscription will be added automatically",
- "ru": "Нажмите кнопку выше — (Подключить подписку) приложение откроется, и подписка добавится автоматически."
- }
- },
- "connectAndUseStep": {
- "description": {
- "en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
- "ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов."
}
}
- }
- ],
- "android": [
- {
- "id": "happ",
- "name": "Happ",
- "isFeatured": true,
- "urlScheme": "happ://add/",
- "installationStep": {
- "buttons": [
- {
- "buttonLink": "https://play.google.com/store/apps/details?id=com.happproxy",
- "buttonText": {
- "en": "Google Play",
- "ru": "Google Play"
- }
- },
- {
- "buttonLink": "https://github.com/Happ-proxy/happ-android/releases/latest/download/Happ.apk",
- "buttonText": {
- "en": "Download APK",
- "ru": "Скачать APK"
+ ],
+ "android": [
+ {
+ "id": "happ",
+ "name": "Happ",
+ "isFeatured": true,
+ "urlScheme": "happ://add/",
+ "installationStep": {
+ "buttons": [
+ {
+ "buttonLink": "https://play.google.com/store/apps/details?id=com.happproxy",
+ "buttonText": {
+ "en": "Google Play",
+ "fa": "Google Play",
+ "ru": "Google Play",
+ "zh": "Google Play"
+ }
+ },
+ {
+ "buttonLink": "https://github.com/Happ-proxy/happ-android/releases/latest/download/Happ.apk",
+ "buttonText": {
+ "en": "Download APK",
+ "fa": "دانلود APK",
+ "ru": "Скачать APK",
+ "zh": "下载 APK"
+ }
}
+ ],
+ "description": {
+ "en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
+ "fa": "صفحه را در Google Play باز کنید و برنامه را نصب کنید. یا برنامه را مستقیماً از فایل APK نصب کنید، اگر Google Play کار نمی کند.",
+ "ru": "Откройте страницу в Google Play и установите приложение. Или установите приложение из APK файла напрямую, если Google Play не работает.",
+ "zh": "在 Google Play 中打开页面并安装应用。如果 Google Play 无法使用,也可以直接从 APK 文件安装应用。"
+ }
+ },
+ "addSubscriptionStep": {
+ "description": {
+ "en": "Click the button below to add subscription",
+ "fa": "برای افزودن اشتراک روی دکمه زیر کلیک کنید",
+ "ru": "Нажмите кнопку ниже, чтобы добавить подписку",
+ "zh": "点击下方按钮添加订阅"
+ }
+ },
+ "connectAndUseStep": {
+ "description": {
+ "en": "Open the app and connect to the server",
+ "fa": "برنامه را باز کنید و به سرور متصل شوید",
+ "ru": "Откройте приложение и подключитесь к серверу",
+ "zh": "打开应用并连接到服务器"
}
- ],
- "description": {
- "en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
- "ru": "Откройте страницу в Google Play и установите приложение. Или установите приложение из APK файла напрямую, если Google Play не работает."
- }
- },
- "addSubscriptionStep": {
- "description": {
- "en": "Click the button below to add subscription",
- "ru": "Нажмите кнопку выше — (Подключить подписку) приложение откроется, и подписка добавится автоматически."
- }
- },
- "connectAndUseStep": {
- "description": {
- "en": "Open the app and connect to the server",
- "ru": "Откройте приложение и подключитесь к серверу"
}
}
- }
- ],
- "pc": [
- {
- "id": "happ",
- "name": "Happ",
- "isFeatured": false,
- "urlScheme": "happ://add/",
- "installationStep": {
- "buttons": [
- {
- "buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215?l=ru",
- "buttonText": {
- "en": "Mac Os",
- "ru": "Mac Os"
- }
- },
- {
- "buttonLink": "https://github.com/Happ-proxy/happ-desktop/releases/latest/download/setup-Happ.x86.exe",
- "buttonText": {
- "en": "Windows",
- "ru": "Windows"
+ ],
+ "macos": [
+ {
+ "id": "happ",
+ "name": "Happ",
+ "isFeatured": true,
+ "urlScheme": "happ://add/",
+ "installationStep": {
+ "buttons": [
+ {
+ "buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215",
+ "buttonText": {
+ "en": "App Store [EU]",
+ "fa": "App Store [EU]",
+ "ru": "App Store [EU]",
+ "zh": "App Store [EU]"
+ }
+ },
+ {
+ "buttonLink": "https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973",
+ "buttonText": {
+ "en": "App Store [RU]",
+ "fa": "App Store [RU]",
+ "ru": "App Store [RU]",
+ "zh": "App Store [RU]"
+ }
}
+ ],
+ "description": {
+ "en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
+ "fa": "صفحه را در App Store باز کنید و برنامه را نصب کنید. آن را اجرا کنید، در پنجره مجوز پیکربندی VPN روی Allow کلیک کنید و رمز عبور خود را وارد کنید.",
+ "ru": "Откройте страницу в App Store и установите приложение. Запустите его, в окне разрешения VPN-конфигурации нажмите Allow и введите свой пароль.",
+ "zh": "在 App Store 中打开页面并安装应用。启动应用后,在 VPN 配置权限窗口中点击\"允许\"并输入您的密码。"
+ }
+ },
+ "addSubscriptionStep": {
+ "description": {
+ "en": "Click the button below — the app will open and the subscription will be added automatically",
+ "fa": "برای افزودن خودکار اشتراک روی دکمه زیر کلیک کنید - برنامه باز خواهد شد",
+ "ru": "Нажмите кнопку ниже — приложение откроется, и подписка добавится автоматически.",
+ "zh": "点击下方按钮 — 应用将打开并自动添加订阅"
+ }
+ },
+ "connectAndUseStep": {
+ "description": {
+ "en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
+ "fa": "در بخش اصلی، دکمه بزرگ روشن/خاموش در مرکز را برای اتصال به VPN کلیک کنید. فراموش نکنید که یک سرور را از لیست سرورها انتخاب کنید. در صورت نیاز، سرور دیگری را از لیست سرورها انتخاب کنید.",
+ "ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов.",
+ "zh": "在主界面中,点击中央的大电源按钮连接到 VPN。别忘了从服务器列表中选择一个服务器。如有需要,可从服务器列表中选择其他服务器。"
}
- ],
- "description": {
- "en": "Choose the version for your device, click the button below and install the app.",
- "ru": "Выберите подходящую версию для вашего устройства, нажмите на кнопку ниже и установите приложение"
- }
- },
- "addSubscriptionStep": {
- "description": {
- "en": "Click the button below to add subscription",
- "ru": "Нажмите кнопку выше — (Подключить подписку) приложение откроется, и подписка добавится автоматически."
- }
- },
- "connectAndUseStep": {
- "description": {
- "en": "You can select a server",
- "ru": "Выберете локацию, включить VPN"
}
}
- }
- ],
- "tv": [
- {
- "id": "vpn4tv",
- "name": "VPN4TV",
- "isFeatured": true,
- "urlScheme": "",
- "installationStep": {
- "buttons": [
- {
- "buttonLink": "https://play.google.com/store/apps/details?id=com.vpn4tv.hiddify",
- "buttonText": {
- "en": "Google Play",
- "ru": "Google Play"
+ ],
+ "windows": [
+ {
+ "id": "happ",
+ "name": "Happ",
+ "isFeatured": false,
+ "urlScheme": "happ://add/",
+ "installationStep": {
+ "buttons": [
+ {
+ "buttonLink": "https://github.com/Happ-proxy/happ-desktop/releases/latest/download/setup-Happ.x86.exe",
+ "buttonText": {
+ "en": "Download",
+ "ru": "Скачать"
+ }
}
+ ],
+ "description": {
+ "en": "Download and install Happ.",
+ "ru": "Скачайте и установите Happ"
+ }
+ },
+ "addSubscriptionStep": {
+ "description": {
+ "en": "Click the button below to add subscription",
+ "ru": "Нажмите кнопку выше — (Подключить подписку) приложение откроется, и подписка добавится автоматически."
+ }
+ },
+ "connectAndUseStep": {
+ "description": {
+ "en": "You can select a server",
+ "ru": "Выберете локацию, включить VPN"
}
- ],
- "description": {
- "en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
- "ru": "Откройте страницу в Google Play и установите приложение"
- }
- },
- "addSubscriptionStep": {
- "description": {
- "en": "Click the button below to add subscription",
- "ru": "Нажмите кнопку выше — (Скопировать ссылку подписки) ты скопируешь свою подписку, далее на телевизоре открой VPN4TV, следуя инструкция передай telegram боту ссылку, которую ты скопировал"
- }
- },
- "connectAndUseStep": {
- "description": {
- "en": "Open the app and connect to the server",
- "ru": "Приложение автоматически обновится и загрузит нужные конфиги на твой телевизор, подключай VPN"
}
}
- }
- ]
-}
+ ],
+ "androidTV": [
+ {
+ "id": "vpn4tv",
+ "name": "VPN4TV",
+ "isFeatured": true,
+ "urlScheme": "",
+ "installationStep": {
+ "buttons": [
+ {
+ "buttonLink": "https://play.google.com/store/apps/details?id=com.vpn4tv.hiddify",
+ "buttonText": {
+ "en": "Google Play",
+ "ru": "Google Play"
+ }
+ }
+ ],
+ "description": {
+ "en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
+ "ru": "Откройте страницу в Google Play и установите приложение"
+ }
+ },
+ "addSubscriptionStep": {
+ "description": {
+ "en": "Click the button below to add subscription",
+ "ru": "Нажмите кнопку выше — (Скопировать ссылку подписки) ты скопируешь свою подписку, далее на телевизоре открой VPN4TV, следуя инструкция передай telegram боту ссылку, которую ты скопировал"
+ }
+ },
+ "connectAndUseStep": {
+ "description": {
+ "en": "Open the app and connect to the server",
+ "ru": "Приложение автоматически обновится и загрузит нужные конфиги на твой телевизор, подключай VPN"
+ }
+ }
+ }
+ ],
+ "linux": [],
+ "appleTV": []
+ }
+}
\ No newline at end of file
diff --git a/app/handlers/subscription.py b/app/handlers/subscription.py
index 8c2ae0b4..923a07ca 100644
--- a/app/handlers/subscription.py
+++ b/app/handlers/subscription.py
@@ -1,44 +1,40 @@
+import json
import logging
from datetime import datetime, timedelta
-from aiogram import Dispatcher, types, F
-from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
-from aiogram.fsm.context import FSMContext
-from sqlalchemy.ext.asyncio import AsyncSession
-import json
-import os
from typing import Dict, List, Any, Tuple, Optional
+from aiogram import Dispatcher, types, F
+from aiogram.fsm.context import FSMContext
+from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
+from sqlalchemy.ext.asyncio import AsyncSession
+
from app.config import settings, PERIOD_PRICES, get_traffic_prices
-from app.states import SubscriptionStates
+from app.database.crud.discount_offer import get_offer_by_id, mark_offer_claimed
from app.database.crud.subscription import (
- get_subscription_by_user_id, create_trial_subscription,
- create_paid_subscription, extend_subscription,
- add_subscription_traffic, add_subscription_devices,
- add_subscription_squad, update_subscription_autopay,
- add_subscription_servers
+ create_trial_subscription,
+ create_paid_subscription, add_subscription_traffic, add_subscription_devices,
+ update_subscription_autopay
)
+from app.database.crud.transaction import create_transaction
from app.database.crud.user import subtract_user_balance, add_user_balance
-from app.database.crud.transaction import create_transaction, get_user_transactions
from app.database.models import (
User, TransactionType, SubscriptionStatus,
- SubscriptionServer, Subscription
+ Subscription
)
-from app.database.crud.discount_offer import get_offer_by_id, mark_offer_claimed
from app.keyboards.inline import (
get_subscription_keyboard, get_trial_keyboard,
get_subscription_period_keyboard, get_traffic_packages_keyboard,
get_countries_keyboard, get_devices_keyboard,
get_subscription_confirm_keyboard, get_autopay_keyboard,
get_autopay_days_keyboard, get_back_keyboard,
- get_extend_subscription_keyboard, get_add_traffic_keyboard,
+ get_add_traffic_keyboard,
get_change_devices_keyboard, get_reset_traffic_confirm_keyboard,
get_manage_countries_keyboard,
get_device_selection_keyboard, get_connection_guide_keyboard,
get_app_selection_keyboard, get_specific_app_keyboard,
get_updated_subscription_settings_keyboard, get_insufficient_balance_keyboard,
get_extend_subscription_keyboard_with_prices, get_confirm_change_devices_keyboard,
- get_devices_management_keyboard, get_device_reset_confirm_keyboard,
- get_device_management_help_keyboard,
+ get_devices_management_keyboard, get_device_management_help_keyboard,
get_happ_cryptolink_keyboard,
get_happ_download_platform_keyboard, get_happ_download_link_keyboard,
get_happ_download_button_row,
@@ -47,15 +43,17 @@ from app.keyboards.inline import (
get_insufficient_balance_keyboard_with_cart
)
from app.localization.texts import get_texts
-from app.services.remnawave_service import RemnaWaveService
from app.services.admin_notification_service import AdminNotificationService
-from app.services.subscription_service import SubscriptionService
+from app.services.remnawave_service import RemnaWaveService
from app.services.subscription_checkout_service import (
clear_subscription_checkout_draft,
get_subscription_checkout_draft,
save_subscription_checkout_draft,
should_offer_checkout_resume,
)
+from app.services.subscription_service import SubscriptionService
+from app.states import SubscriptionStates
+from app.utils.pagination import paginate_list
from app.utils.pricing_utils import (
calculate_months_from_days,
get_remaining_months,
@@ -64,7 +62,6 @@ from app.utils.pricing_utils import (
format_period_description,
apply_percentage_discount,
)
-from app.utils.pagination import paginate_list
from app.utils.subscription_utils import (
get_display_subscription_link,
get_happ_cryptolink_redirect_link,
@@ -77,9 +74,9 @@ TRAFFIC_PRICES = get_traffic_prices()
def _get_addon_discount_percent_for_user(
- user: Optional[User],
- category: str,
- period_days_hint: Optional[int] = None,
+ user: Optional[User],
+ category: str,
+ period_days_hint: Optional[int] = None,
) -> int:
if user is None:
return 0
@@ -98,10 +95,10 @@ def _get_addon_discount_percent_for_user(
def _apply_addon_discount(
- user: Optional[User],
- category: str,
- amount: int,
- period_days_hint: Optional[int] = None,
+ user: Optional[User],
+ category: str,
+ amount: int,
+ period_days_hint: Optional[int] = None,
) -> Dict[str, int]:
percent = _get_addon_discount_percent_for_user(user, category, period_days_hint)
discounted_amount, discount_value = apply_percentage_discount(amount, percent)
@@ -125,9 +122,9 @@ def _get_period_hint_from_subscription(subscription: Optional[Subscription]) ->
def _apply_discount_to_monthly_component(
- amount_per_month: int,
- percent: int,
- months: int,
+ amount_per_month: int,
+ percent: int,
+ months: int,
) -> Dict[str, int]:
discounted_per_month, discount_per_month = apply_percentage_discount(amount_per_month, percent)
@@ -142,11 +139,10 @@ def _apply_discount_to_monthly_component(
async def _prepare_subscription_summary(
- db_user: User,
- data: Dict[str, Any],
- texts,
+ db_user: User,
+ data: Dict[str, Any],
+ texts,
) -> Tuple[str, Dict[str, Any]]:
-
summary_data = dict(data)
countries = await _get_available_countries(db_user.promo_group_id)
@@ -234,9 +230,9 @@ async def _prepare_subscription_summary(
total_price = base_price + total_traffic_price + total_countries_price + total_devices_price
discounted_monthly_additions = (
- traffic_component["discounted_per_month"]
- + discounted_servers_price_per_month
- + devices_component["discounted_per_month"]
+ traffic_component["discounted_per_month"]
+ + discounted_servers_price_per_month
+ + devices_component["discounted_per_month"]
)
is_valid = validate_pricing_calculation(
@@ -351,9 +347,9 @@ async def _prepare_subscription_summary(
def _build_promo_group_discount_text(
- db_user: User,
- periods: Optional[List[int]] = None,
- texts=None,
+ db_user: User,
+ periods: Optional[List[int]] = None,
+ texts=None,
) -> str:
promo_group = getattr(db_user, "promo_group", None)
@@ -446,15 +442,15 @@ def _build_subscription_period_prompt(db_user: User, texts) -> str:
async def show_subscription_info(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
await db.refresh(db_user)
-
+
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
if not subscription:
await callback.message.edit_text(
texts.SUBSCRIPTION_NONE,
@@ -462,17 +458,17 @@ async def show_subscription_info(
)
await callback.answer()
return
-
+
from app.database.crud.subscription import check_and_update_subscription_status
subscription = await check_and_update_subscription_status(db, subscription)
-
+
subscription_service = SubscriptionService()
await subscription_service.sync_subscription_usage(db, subscription)
-
+
await db.refresh(subscription)
-
+
current_time = datetime.utcnow()
-
+
if subscription.status == "expired" or subscription.end_date <= current_time:
actual_status = "expired"
status_display = texts.t("SUBSCRIPTION_STATUS_EXPIRED", "Истекла")
@@ -536,7 +532,7 @@ async def show_subscription_info(
"SUBSCRIPTION_TRAFFIC_LIMITED",
"{used} / {limit} ГБ",
).format(used=used_traffic, limit=subscription.traffic_limit_gb)
-
+
devices_used_str = "—"
devices_list = []
devices_count = 0
@@ -545,10 +541,10 @@ async def show_subscription_info(
if db_user.remnawave_uuid:
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
-
+
async with service.get_api_client() 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)
@@ -557,7 +553,7 @@ async def show_subscription_info(
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)
@@ -611,14 +607,14 @@ async def show_subscription_info(
device_info = device_info[:32] + "..."
message += f"• {device_info}\n"
message += texts.t("SUBSCRIPTION_CONNECTED_DEVICES_FOOTER", "")
-
+
subscription_link = get_display_subscription_link(subscription)
hide_subscription_link = settings.should_hide_subscription_link()
if (
- subscription_link
- and actual_status in ["trial_active", "paid_active"]
- and not hide_subscription_link
+ subscription_link
+ and actual_status in ["trial_active", "paid_active"]
+ and not hide_subscription_link
):
message += "\n\n" + texts.t(
"SUBSCRIPTION_CONNECT_LINK_SECTION",
@@ -628,7 +624,7 @@ async def show_subscription_info(
"SUBSCRIPTION_CONNECT_LINK_PROMPT",
"📱 Скопируйте ссылку и добавьте в ваше VPN приложение",
)
-
+
await callback.message.edit_text(
message,
reply_markup=get_subscription_keyboard(
@@ -641,43 +637,45 @@ 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.get_api_client() 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]
+ "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:
if not squad_uuids:
return "Нет серверов"
-
+
try:
from app.database.database import AsyncSessionLocal
from app.database.crud.server_squad import get_server_squad_by_uuid
-
+
server_names = []
-
+
async with AsyncSessionLocal() as db:
for uuid in squad_uuids:
server = await get_server_squad_by_uuid(db, uuid)
@@ -686,7 +684,7 @@ async def get_servers_display_names(squad_uuids: List[str]) -> str:
logger.debug(f"Найден сервер в БД: {uuid} -> {server.display_name}")
else:
logger.warning(f"Сервер с UUID {uuid} не найден в БД")
-
+
if not server_names:
countries = await _get_available_countries()
for uuid in squad_uuids:
@@ -695,42 +693,43 @@ async def get_servers_display_names(squad_uuids: List[str]) -> str:
server_names.append(country['name'])
logger.debug(f"Найден сервер в кэше: {uuid} -> {country['name']}")
break
-
+
if not server_names:
if len(squad_uuids) == 1:
return "🎯 Тестовый сервер"
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}")
if len(squad_uuids) == 1:
return "🎯 Тестовый сервер"
return f"{len(squad_uuids)} стран"
+
async def get_current_devices_count(db_user: User) -> str:
try:
if not db_user.remnawave_uuid:
return "—"
-
+
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
-
+
async with service.get_api_client() as api:
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
-
+
if response and 'response' in response:
total_devices = response['response'].get('total', 0)
return str(total_devices)
else:
return "—"
-
+
except Exception as e:
logger.error(f"Ошибка получения количества устройств: {e}")
return "—"
@@ -740,12 +739,12 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
try:
if subscription.is_trial:
return 0
-
+
from app.config import settings
from app.services.subscription_service import SubscriptionService
-
+
subscription_service = SubscriptionService()
-
+
base_cost_original = PERIOD_PRICES.get(30, 0)
try:
owner = subscription.user
@@ -780,43 +779,43 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
db,
promo_group_id=promo_group_id,
)
-
+
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
-
+
logger.info(f"📊 Месячная стоимость конфигурации подписки {subscription.id}:")
- base_log = f" 📅 Базовый тариф (30 дней): {base_cost_original/100}₽"
+ base_log = f" 📅 Базовый тариф (30 дней): {base_cost_original / 100}₽"
if period_discount_percent > 0:
discount_value = base_cost_original * period_discount_percent // 100
base_log += (
- f" → {base_cost/100}₽"
- f" (скидка {period_discount_percent}%: -{discount_value/100}₽)"
+ f" → {base_cost / 100}₽"
+ f" (скидка {period_discount_percent}%: -{discount_value / 100}₽)"
)
logger.info(base_log)
if servers_cost > 0:
- logger.info(f" 🌍 Серверы: {servers_cost/100}₽")
+ logger.info(f" 🌍 Серверы: {servers_cost / 100}₽")
if traffic_cost > 0:
- logger.info(f" 📊 Трафик: {traffic_cost/100}₽")
+ logger.info(f" 📊 Трафик: {traffic_cost / 100}₽")
if devices_cost > 0:
- logger.info(f" 📱 Устройства: {devices_cost/100}₽")
- logger.info(f" 💎 ИТОГО: {total_cost/100}₽")
-
+ logger.info(f" 📱 Устройства: {devices_cost / 100}₽")
+ logger.info(f" 💎 ИТОГО: {total_cost / 100}₽")
+
return total_cost
-
+
except Exception as e:
logger.error(f"⚠️ Ошибка расчета стоимости подписки: {e}")
return 0
async def show_trial_offer(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
texts = get_texts(db_user.language)
-
+
if db_user.subscription or db_user.has_had_paid_subscription:
await callback.message.edit_text(
texts.TRIAL_ALREADY_USED,
@@ -824,11 +823,11 @@ async def show_trial_offer(
)
await callback.answer()
return
-
- trial_server_name = "🎯 Тестовый сервер"
+
+ trial_server_name = "🎯 Тестовый сервер"
try:
from app.database.crud.server_squad import get_server_squad_by_uuid
-
+
if settings.TRIAL_SQUAD_UUID:
trial_server = await get_server_squad_by_uuid(db, settings.TRIAL_SQUAD_UUID)
if trial_server:
@@ -837,17 +836,17 @@ async def show_trial_offer(
logger.warning(f"Триальный сервер с UUID {settings.TRIAL_SQUAD_UUID} не найден в БД")
else:
logger.warning("TRIAL_SQUAD_UUID не настроен в конфигурации")
-
+
except Exception as e:
logger.error(f"Ошибка получения триального сервера: {e}")
-
+
trial_text = texts.TRIAL_AVAILABLE.format(
days=settings.TRIAL_DURATION_DAYS,
traffic=settings.TRIAL_TRAFFIC_LIMIT_GB,
devices=settings.TRIAL_DEVICE_LIMIT,
server_name=trial_server_name
)
-
+
await callback.message.edit_text(
trial_text,
reply_markup=get_trial_keyboard(db_user.language)
@@ -856,14 +855,14 @@ async def show_trial_offer(
async def activate_trial(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
from app.services.admin_notification_service import AdminNotificationService
-
+
texts = get_texts(db_user.language)
-
+
if db_user.subscription or db_user.has_had_paid_subscription:
await callback.message.edit_text(
texts.TRIAL_ALREADY_USED,
@@ -871,54 +870,54 @@ async def activate_trial(
)
await callback.answer()
return
-
+
try:
subscription = await create_trial_subscription(db, db_user.id)
-
+
await db.refresh(db_user)
-
+
subscription_service = SubscriptionService()
remnawave_user = await subscription_service.create_remnawave_user(
db, subscription
)
-
+
await db.refresh(db_user)
-
+
try:
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_trial_activation_notification(db, db_user, subscription)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о триале: {e}")
-
+
subscription_link = get_display_subscription_link(subscription)
hide_subscription_link = settings.should_hide_subscription_link()
if remnawave_user and subscription_link:
if settings.is_happ_cryptolink_mode():
trial_success_text = (
- f"{texts.TRIAL_ACTIVATED}\n\n"
- + texts.t(
- "SUBSCRIPTION_HAPP_LINK_PROMPT",
- "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.",
- )
- + "\n\n"
- + texts.t(
- "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
- "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
- )
+ f"{texts.TRIAL_ACTIVATED}\n\n"
+ + texts.t(
+ "SUBSCRIPTION_HAPP_LINK_PROMPT",
+ "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.",
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
+ "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
+ )
)
elif hide_subscription_link:
trial_success_text = (
- f"{texts.TRIAL_ACTIVATED}\n\n"
- + texts.t(
- "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
- "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
- )
- + "\n\n"
- + texts.t(
- "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
- "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
- )
+ f"{texts.TRIAL_ACTIVATED}\n\n"
+ + texts.t(
+ "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
+ "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
+ "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
+ )
)
else:
subscription_import_link = texts.t(
@@ -942,7 +941,8 @@ async def activate_trial(
web_app=types.WebAppInfo(url=subscription_link),
)
],
- [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")],
+ [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")],
])
elif connect_mode == "miniapp_custom":
if not settings.MINIAPP_CUSTOM_URL:
@@ -962,7 +962,8 @@ async def activate_trial(
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
)
],
- [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")],
+ [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")],
])
elif connect_mode == "link":
rows = [
@@ -999,10 +1000,12 @@ async def activate_trial(
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
else:
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
- [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), callback_data="subscription_connect")],
- [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")],
+ [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
+ callback_data="subscription_connect")],
+ [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")],
])
-
+
await callback.message.edit_text(
trial_success_text,
reply_markup=connect_keyboard,
@@ -1013,23 +1016,23 @@ async def activate_trial(
f"{texts.TRIAL_ACTIVATED}\n\n⚠️ Ссылка генерируется, попробуйте перейти в раздел 'Моя подписка' через несколько секунд.",
reply_markup=get_back_keyboard(db_user.language)
)
-
+
logger.info(f"✅ Активирована тестовая подписка для пользователя {db_user.telegram_id}")
-
+
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 start_subscription_purchase(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User
):
texts = get_texts(db_user.language)
@@ -1050,27 +1053,26 @@ async def start_subscription_purchase(
'devices': initial_devices,
'total_price': 0
}
-
+
if settings.is_traffic_fixed():
initial_data['traffic_gb'] = settings.get_fixed_traffic_limit()
else:
initial_data['traffic_gb'] = None
-
+
await state.set_data(initial_data)
await state.set_state(SubscriptionStates.selecting_period)
await callback.answer()
+
async def save_cart_and_redirect_to_topup(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User,
- missing_amount: int
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User,
+ missing_amount: int
):
- from app.handlers.balance import show_payment_methods
-
texts = get_texts(db_user.language)
data = await state.get_data()
-
+
await state.set_state(SubscriptionStates.cart_saved_for_topup)
await state.update_data({
**data,
@@ -1078,7 +1080,7 @@ async def save_cart_and_redirect_to_topup(
'missing_amount': missing_amount,
'return_to_cart': True
})
-
+
await callback.message.edit_text(
f"💰 Недостаточно средств для оформления подписки\n\n"
f"Требуется: {texts.format_price(missing_amount)}\n"
@@ -1093,21 +1095,22 @@ async def save_cart_and_redirect_to_topup(
parse_mode="HTML"
)
+
async def return_to_saved_cart(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User,
+ db: AsyncSession
):
data = await state.get_data()
texts = get_texts(db_user.language)
-
+
if not data.get('saved_cart'):
await callback.answer("❌ Сохраненная корзина не найдена", show_alert=True)
return
-
+
total_price = data.get('total_price', 0)
-
+
if db_user.balance_kopeks < total_price:
missing_amount = total_price - db_user.balance_kopeks
await callback.message.edit_text(
@@ -1121,23 +1124,22 @@ async def return_to_saved_cart(
)
)
return
-
-
+
countries = await _get_available_countries(db_user.promo_group_id)
selected_countries_names = []
-
+
months_in_period = calculate_months_from_days(data['period_days'])
period_display = format_period_description(data['period_days'], db_user.language)
-
+
for country in countries:
if country['uuid'] in data['countries']:
selected_countries_names.append(country['name'])
-
+
if settings.is_traffic_fixed():
traffic_display = "Безлимитный" if data['traffic_gb'] == 0 else f"{data['traffic_gb']} ГБ"
else:
traffic_display = "Безлимитный" if data['traffic_gb'] == 0 else f"{data['traffic_gb']} ГБ"
-
+
summary_text = (
"🛒 Восстановленная корзина\n\n"
f"📅 Период: {period_display}\n"
@@ -1147,26 +1149,27 @@ async def return_to_saved_cart(
f"💎 Общая стоимость: {texts.format_price(total_price)}\n\n"
"Подтверждаете покупку?"
)
-
+
await callback.message.edit_text(
summary_text,
reply_markup=get_subscription_confirm_keyboard_with_cart(db_user.language),
parse_mode="HTML"
)
-
+
await state.set_state(SubscriptionStates.confirming_purchase)
await callback.answer("✅ Корзина восстановлена!")
+
async def handle_add_countries(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext
):
if not await _should_show_countries_management(db_user):
await callback.answer("ℹ️ Управление серверами недоступно - доступен только один сервер", show_alert=True)
return
-
+
texts = get_texts(db_user.language)
subscription = db_user.subscription
@@ -1183,28 +1186,28 @@ async def handle_add_countries(
"servers",
period_hint_days,
)
-
+
current_countries_names = []
for country in countries:
if country['uuid'] in current_countries:
current_countries_names.append(country['name'])
-
+
text = "🌍 Управление странами подписки\n\n"
text += f"📋 Текущие страны ({len(current_countries)}):\n"
if current_countries_names:
text += "\n".join(f"• {name}" for name in current_countries_names)
else:
text += "Нет подключенных стран"
-
+
text += "\n\n💡 Инструкция:\n"
text += "✅ - страна подключена\n"
text += "➕ - будет добавлена (платно)\n"
text += "➖ - будет отключена (бесплатно)\n"
text += "⚪ - не выбрана\n\n"
text += "⚠️ Важно: Повторное подключение отключенных стран будет платным!"
-
+
await state.update_data(countries=current_countries.copy())
-
+
await callback.message.edit_text(
text,
reply_markup=get_manage_countries_keyboard(
@@ -1217,20 +1220,21 @@ async def handle_add_countries(
),
parse_mode="HTML"
)
-
+
await callback.answer()
+
async def get_countries_price_by_uuids_fallback(
- country_uuids: List[str],
- db: AsyncSession,
- promo_group_id: Optional[int] = None,
+ country_uuids: List[str],
+ db: AsyncSession,
+ promo_group_id: Optional[int] = None,
) -> Tuple[int, List[int]]:
try:
from app.database.crud.server_squad import get_server_squad_by_uuid
-
+
total_price = 0
prices_list = []
-
+
for country_uuid in country_uuids:
try:
server = await get_server_squad_by_uuid(db, country_uuid)
@@ -1251,24 +1255,25 @@ async def get_countries_price_by_uuids_fallback(
default_price = 0
total_price += default_price
prices_list.append(default_price)
-
+
return total_price, prices_list
-
+
except Exception as e:
logger.error(f"Ошибка fallback функции: {e}")
default_prices = [0] * len(country_uuids)
return sum(default_prices), default_prices
+
async def handle_manage_country(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext
):
logger.info(f"🔍 Управление страной: {callback.data}")
-
- country_uuid = callback.data.split('_')[2]
-
+
+ country_uuid = callback.data.split('_')[2]
+
subscription = db_user.subscription
if not subscription or subscription.is_trial:
await callback.answer("⚠ Только для платных подписок", show_alert=True)
@@ -1314,21 +1319,21 @@ async def handle_manage_country(
)
)
logger.info(f"✅ Клавиатура обновлена")
-
+
except Exception as e:
logger.error(f"⚠ Ошибка обновления клавиатуры: {e}")
-
+
await callback.answer()
+
async def apply_countries_changes(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext
):
-
logger.info(f"🔧 Применение изменений стран")
-
+
data = await state.get_data()
texts = get_texts(db_user.language)
@@ -1339,7 +1344,7 @@ async def apply_countries_changes(
else None
)
subscription = db_user.subscription
-
+
selected_countries = data.get('countries', [])
current_countries = subscription.connected_squads
@@ -1418,7 +1423,7 @@ async def apply_countries_changes(
total_cost / 100,
total_discount / 100,
)
-
+
if total_cost > 0 and db_user.balance_kopeks < total_cost:
missing_kopeks = total_cost - db_user.balance_kopeks
required_text = f"{texts.format_price(total_cost)} (за {charged_months} мес)"
@@ -1448,17 +1453,17 @@ async def apply_countries_changes(
)
await callback.answer()
return
-
+
try:
if added and total_cost > 0:
success = await subtract_user_balance(
- db, db_user, total_cost,
+ db, db_user, total_cost,
f"Добавление стран: {', '.join(added_names)} на {charged_months} мес"
)
if not success:
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
-
+
await create_transaction(
db=db,
user_id=db_user.id,
@@ -1466,28 +1471,29 @@ async def apply_countries_changes(
amount_kopeks=total_cost,
description=f"Добавление стран к подписке: {', '.join(added_names)} на {charged_months} мес"
)
-
+
if added:
from app.database.crud.server_squad import get_server_ids_by_uuids, add_user_to_servers
from app.database.crud.subscription import add_subscription_servers
-
+
added_server_ids = await get_server_ids_by_uuids(db, added)
-
+
if added_server_ids:
await add_subscription_servers(db, subscription, added_server_ids, added_server_prices)
await add_user_to_servers(db, added_server_ids)
-
- logger.info(f"📊 Добавлены серверы с ценами за {charged_months} мес: {list(zip(added_server_ids, added_server_prices))}")
-
+
+ logger.info(
+ f"📊 Добавлены серверы с ценами за {charged_months} мес: {list(zip(added_server_ids, added_server_prices))}")
+
subscription.connected_squads = selected_countries
subscription.updated_at = datetime.utcnow()
await db.commit()
-
+
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
-
+
await db.refresh(subscription)
-
+
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(callback.bot)
@@ -1496,9 +1502,9 @@ async def apply_countries_changes(
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления об изменении серверов: {e}")
-
+
success_text = "✅ Страны успешно обновлены!\n\n"
-
+
if added_names:
success_text += f"➕ Добавлены страны:\n"
success_text += "\n".join(f"• {name}" for name in added_names)
@@ -1510,55 +1516,56 @@ async def apply_countries_changes(
f" -{texts.format_price(total_discount)})"
)
success_text += "\n"
-
+
if removed_names:
success_text += f"\n➖ Отключены страны:\n"
success_text += "\n".join(f"• {name}" for name in removed_names)
success_text += "\nℹ️ Повторное подключение будет платным\n"
-
+
success_text += f"\n🌐 Активных стран: {len(selected_countries)}"
-
+
await callback.message.edit_text(
success_text,
reply_markup=get_back_keyboard(db_user.language),
parse_mode="HTML"
)
-
+
await state.clear()
- logger.info(f"✅ Пользователь {db_user.telegram_id} обновил страны. Добавлено: {len(added)}, удалено: {len(removed)}, заплатил: {total_cost/100}₽")
-
+ logger.info(
+ f"✅ Пользователь {db_user.telegram_id} обновил страны. Добавлено: {len(added)}, удалено: {len(removed)}, заплатил: {total_cost / 100}₽")
+
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 handle_add_traffic(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
from app.config import settings
-
+
if settings.is_traffic_fixed():
await callback.answer("⚠️ В текущем режиме трафик фиксированный и не может быть изменен", show_alert=True)
return
-
+
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
if not subscription or subscription.is_trial:
await callback.answer("⚠ Эта функция доступна только для платных подписок", show_alert=True)
return
-
+
if subscription.traffic_limit_gb == 0:
await callback.answer("⚠ У вас уже безлимитный трафик", show_alert=True)
return
-
+
current_traffic = subscription.traffic_limit_gb
period_hint_days = _get_period_hint_from_subscription(subscription)
traffic_discount_percent = _get_addon_discount_percent_for_user(
@@ -1578,22 +1585,22 @@ async def handle_add_traffic(
),
parse_mode="HTML"
)
-
+
await callback.answer()
-
+
async def handle_change_devices(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
if not subscription or subscription.is_trial:
await callback.answer("⚠️ Эта функция доступна только для платных подписок", show_alert=True)
return
-
+
current_devices = subscription.device_limit
period_hint_days = _get_period_hint_from_subscription(subscription)
@@ -1618,43 +1625,43 @@ async def handle_change_devices(
),
parse_mode="HTML"
)
-
+
await callback.answer()
+
async def confirm_change_devices(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
new_devices_count = int(callback.data.split('_')[2])
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
current_devices = subscription.device_limit
-
+
if new_devices_count == current_devices:
await callback.answer("ℹ️ Количество устройств не изменилось", show_alert=True)
return
-
+
if settings.MAX_DEVICES_LIMIT > 0 and new_devices_count > settings.MAX_DEVICES_LIMIT:
await callback.answer(
f"⚠️ Превышен максимальный лимит устройств ({settings.MAX_DEVICES_LIMIT})",
show_alert=True
)
return
-
+
devices_difference = new_devices_count - current_devices
-
- if devices_difference > 0:
+
+ if devices_difference > 0:
additional_devices = devices_difference
-
+
if current_devices < settings.DEFAULT_DEVICE_LIMIT:
free_devices = settings.DEFAULT_DEVICE_LIMIT - current_devices
chargeable_devices = max(0, additional_devices - free_devices)
else:
chargeable_devices = additional_devices
-
+
devices_price_per_month = chargeable_devices * settings.PRICE_PER_DEVICE
months_hint = get_remaining_months(subscription.end_date)
period_hint_days = months_hint * 30 if months_hint > 0 else None
@@ -1672,7 +1679,7 @@ async def confirm_change_devices(
subscription.end_date,
)
total_discount = discount_per_month * charged_months
-
+
if price > 0 and db_user.balance_kopeks < price:
missing_kopeks = price - db_user.balance_kopeks
required_text = f"{texts.format_price(price)} (за {charged_months} мес)"
@@ -1701,7 +1708,7 @@ async def confirm_change_devices(
)
await callback.answer()
return
-
+
action_text = f"увеличить до {new_devices_count}"
if price > 0:
cost_text = f"Доплата: {texts.format_price(price)} (за {charged_months} мес)"
@@ -1712,53 +1719,52 @@ async def confirm_change_devices(
)
else:
cost_text = "Бесплатно"
-
- else:
+
+ else:
price = 0
action_text = f"уменьшить до {new_devices_count}"
cost_text = "Возврат средств не производится"
-
+
confirm_text = f"📱 Подтверждение изменения\n\n"
confirm_text += f"Текущее количество: {current_devices} устройств\n"
confirm_text += f"Новое количество: {new_devices_count} устройств\n\n"
confirm_text += f"Действие: {action_text}\n"
confirm_text += f"💰 {cost_text}\n\n"
confirm_text += "Подтвердить изменение?"
-
+
await callback.message.edit_text(
confirm_text,
reply_markup=get_confirm_change_devices_keyboard(new_devices_count, price, db_user.language),
parse_mode="HTML"
)
-
+
await callback.answer()
async def execute_change_devices(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
callback_parts = callback.data.split('_')
new_devices_count = int(callback_parts[3])
price = int(callback_parts[4])
-
+
texts = get_texts(db_user.language)
subscription = db_user.subscription
current_devices = subscription.device_limit
-
+
try:
if price > 0:
success = await subtract_user_balance(
db, db_user, price,
f"Изменение количества устройств с {current_devices} до {new_devices_count}"
)
-
+
if not success:
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
-
+
charged_months = get_remaining_months(subscription.end_date)
await create_transaction(
db=db,
@@ -1767,18 +1773,18 @@ async def execute_change_devices(
amount_kopeks=price,
description=f"Изменение устройств с {current_devices} до {new_devices_count} на {charged_months} мес"
)
-
+
subscription.device_limit = new_devices_count
subscription.updated_at = datetime.utcnow()
-
+
await db.commit()
-
+
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
-
+
await db.refresh(db_user)
await db.refresh(subscription)
-
+
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(callback.bot)
@@ -1787,7 +1793,7 @@ async def execute_change_devices(
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления об изменении устройств: {e}")
-
+
if new_devices_count > current_devices:
success_text = f"✅ Количество устройств увеличено!\n\n"
success_text += f"📱 Было: {current_devices} → Стало: {new_devices_count}\n"
@@ -1797,52 +1803,53 @@ async def execute_change_devices(
success_text = f"✅ Количество устройств уменьшено!\n\n"
success_text += f"📱 Было: {current_devices} → Стало: {new_devices_count}\n"
success_text += f"ℹ️ Возврат средств не производится"
-
+
await callback.message.edit_text(
success_text,
reply_markup=get_back_keyboard(db_user.language)
)
-
- logger.info(f"✅ Пользователь {db_user.telegram_id} изменил количество устройств с {current_devices} на {new_devices_count}, доплата: {price/100}₽")
-
+
+ logger.info(
+ f"✅ Пользователь {db_user.telegram_id} изменил количество устройств с {current_devices} на {new_devices_count}, доплата: {price / 100}₽")
+
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 handle_device_management(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
if not subscription or subscription.is_trial:
await callback.answer("⚠️ Эта функция доступна только для платных подписок", show_alert=True)
return
-
+
if not db_user.remnawave_uuid:
await callback.answer("❌ UUID пользователя не найден", show_alert=True)
return
-
+
try:
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
-
+
async with service.get_api_client() 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', [])
-
+
if total_devices == 0:
await callback.message.edit_text(
"ℹ️ У вас нет подключенных устройств",
@@ -1850,56 +1857,54 @@ async def handle_device_management(
)
await callback.answer()
return
-
+
await show_devices_page(callback, db_user, devices_list, page=1)
else:
await callback.answer("❌ Ошибка получения информации об устройствах", show_alert=True)
-
+
except Exception as e:
logger.error(f"Ошибка получения списка устройств: {e}")
await callback.answer("❌ Ошибка получения информации об устройствах", show_alert=True)
-
+
await callback.answer()
async def show_devices_page(
- callback: types.CallbackQuery,
- db_user: User,
- devices_list: List[dict],
- page: int = 1
+ callback: types.CallbackQuery,
+ db_user: User,
+ devices_list: List[dict],
+ page: int = 1
):
-
-
texts = get_texts(db_user.language)
devices_per_page = 5
-
+
pagination = paginate_list(devices_list, page=page, per_page=devices_per_page)
-
+
devices_text = f"🔄 Управление устройствами\n\n"
devices_text += f"📊 Всего подключено: {len(devices_list)} устройств\n"
devices_text += f"📄 Страница {pagination.page} из {pagination.total_pages}\n\n"
-
+
if pagination.items:
devices_text += "Подключенные устройства:\n"
for i, device in enumerate(pagination.items, 1):
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] + "..."
-
+
devices_text += f"• {device_info}\n"
-
+
devices_text += "\n💡 Действия:\n"
devices_text += "• Выберите устройство для сброса\n"
devices_text += "• Или сбросьте все устройства сразу"
-
+
await callback.message.edit_text(
devices_text,
reply_markup=get_devices_management_keyboard(
- pagination.items,
- pagination,
+ pagination.items,
+ pagination,
db_user.language
),
parse_mode="HTML"
@@ -1907,103 +1912,102 @@ async def show_devices_page(
async def handle_devices_page(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
page = int(callback.data.split('_')[2])
-
+
try:
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
-
+
async with service.get_api_client() as api:
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
-
+
if response and 'response' in response:
devices_list = response['response'].get('devices', [])
await show_devices_page(callback, db_user, devices_list, page=page)
else:
await callback.answer("❌ Ошибка получения устройств", show_alert=True)
-
+
except Exception as e:
logger.error(f"Ошибка перехода на страницу устройств: {e}")
await callback.answer("❌ Ошибка загрузки страницы", show_alert=True)
async def handle_single_device_reset(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
try:
callback_parts = callback.data.split('_')
if len(callback_parts) < 4:
logger.error(f"Некорректный формат callback_data: {callback.data}")
await callback.answer("❌ Ошибка: некорректный запрос", show_alert=True)
return
-
+
device_index = int(callback_parts[2])
page = int(callback_parts[3])
-
+
logger.info(f"🔧 Сброс устройства: index={device_index}, page={page}")
-
+
except (ValueError, IndexError) as e:
logger.error(f"❌ Ошибка парсинга callback_data {callback.data}: {e}")
await callback.answer("❌ Ошибка обработки запроса", show_alert=True)
return
-
+
texts = get_texts(db_user.language)
-
+
try:
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
-
+
async with service.get_api_client() as api:
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
-
+
if response and 'response' in response:
devices_list = response['response'].get('devices', [])
-
+
devices_per_page = 5
pagination = paginate_list(devices_list, page=page, per_page=devices_per_page)
-
+
if device_index < len(pagination.items):
device = pagination.items[device_index]
device_hwid = device.get('hwid')
-
+
if device_hwid:
delete_data = {
"userUuid": db_user.remnawave_uuid,
"hwid": device_hwid
}
-
+
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
-
+
platform = device.get('platform', 'Unknown')
device_model = device.get('deviceModel', 'Unknown')
device_info = f"{platform} - {device_model}"
-
+
await callback.answer(f"✅ Устройство {device_info} успешно сброшено!", show_alert=True)
-
+
updated_response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
if updated_response and 'response' in updated_response:
updated_devices = updated_response['response'].get('devices', [])
-
+
if updated_devices:
- updated_pagination = paginate_list(updated_devices, page=page, per_page=devices_per_page)
+ updated_pagination = paginate_list(updated_devices, page=page,
+ per_page=devices_per_page)
if not updated_pagination.items and page > 1:
page = page - 1
-
+
await show_devices_page(callback, db_user, updated_devices, page=page)
else:
await callback.message.edit_text(
"ℹ️ Все устройства сброшены",
reply_markup=get_back_keyboard(db_user.language)
)
-
+
logger.info(f"✅ Пользователь {db_user.telegram_id} сбросил устройство {device_info}")
else:
await callback.answer("❌ Не удалось получить ID устройства", show_alert=True)
@@ -2011,46 +2015,45 @@ async def handle_single_device_reset(
await callback.answer("❌ Устройство не найдено", show_alert=True)
else:
await callback.answer("❌ Ошибка получения устройств", show_alert=True)
-
+
except Exception as e:
logger.error(f"Ошибка сброса устройства: {e}")
await callback.answer("❌ Ошибка сброса устройства", show_alert=True)
async def handle_all_devices_reset_from_management(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
texts = get_texts(db_user.language)
-
+
if not db_user.remnawave_uuid:
await callback.answer("❌ UUID пользователя не найден", show_alert=True)
return
-
+
try:
from app.services.remnawave_service import RemnaWaveService
service = RemnaWaveService()
-
+
async with service.get_api_client() as api:
devices_response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
-
+
if not devices_response or 'response' not in devices_response:
await callback.answer("❌ Ошибка получения списка устройств", show_alert=True)
return
-
+
devices_list = devices_response['response'].get('devices', [])
-
+
if not devices_list:
await callback.answer("ℹ️ У вас нет подключенных устройств", show_alert=True)
return
-
+
logger.info(f"🔧 Найдено {len(devices_list)} устройств для сброса")
-
+
success_count = 0
failed_count = 0
-
+
for device in devices_list:
device_hwid = device.get('hwid')
if device_hwid:
@@ -2059,18 +2062,18 @@ async def handle_all_devices_reset_from_management(
"userUuid": db_user.remnawave_uuid,
"hwid": device_hwid
}
-
+
await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
success_count += 1
logger.info(f"✅ Устройство {device_hwid} удалено")
-
+
except Exception as device_error:
failed_count += 1
logger.error(f"❌ Ошибка удаления устройства {device_hwid}: {device_error}")
else:
failed_count += 1
logger.warning(f"⚠️ У устройства нет HWID: {device}")
-
+
if success_count > 0:
if failed_count == 0:
await callback.message.edit_text(
@@ -2091,7 +2094,8 @@ async def handle_all_devices_reset_from_management(
reply_markup=get_back_keyboard(db_user.language),
parse_mode="HTML"
)
- logger.warning(f"⚠️ Частичный сброс у пользователя {db_user.telegram_id}: {success_count}/{len(devices_list)}")
+ logger.warning(
+ f"⚠️ Частичный сброс у пользователя {db_user.telegram_id}: {success_count}/{len(devices_list)}")
else:
await callback.message.edit_text(
f"❌ Не удалось сбросить устройства\n\n"
@@ -2101,38 +2105,38 @@ async def handle_all_devices_reset_from_management(
parse_mode="HTML"
)
logger.error(f"❌ Не удалось сбросить ни одного устройства у пользователя {db_user.telegram_id}")
-
+
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 handle_extend_subscription(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
if not subscription or subscription.is_trial:
await callback.answer("⚠ Продление доступно только для платных подписок", show_alert=True)
return
-
+
subscription_service = SubscriptionService()
-
+
available_periods = settings.get_available_renewal_periods()
renewal_prices = {}
-
+
for days in available_periods:
try:
months_in_period = calculate_months_from_days(days)
-
+
from app.config import PERIOD_PRICES
from app.utils.pricing_utils import apply_percentage_discount
@@ -2142,7 +2146,7 @@ async def handle_extend_subscription(
base_price_original,
period_discount_percent,
)
-
+
servers_price_per_month, _ = await subscription_service.get_countries_price_by_uuids(
subscription.connected_squads,
db,
@@ -2174,15 +2178,15 @@ async def handle_extend_subscription(
price = base_price + total_servers_price + total_devices_price + total_traffic_price
renewal_prices[days] = price
-
+
except Exception as e:
logger.error(f"Ошибка расчета цены для периода {days}: {e}")
continue
-
+
if not renewal_prices:
await callback.answer("⚠ Нет доступных периодов для продления", show_alert=True)
return
-
+
prices_text = ""
for days in available_periods:
@@ -2217,38 +2221,38 @@ async def handle_extend_subscription(
reply_markup=get_extend_subscription_keyboard_with_prices(db_user.language, renewal_prices),
parse_mode="HTML"
)
-
+
await callback.answer()
async def handle_reset_traffic(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
from app.config import settings
-
+
if settings.is_traffic_fixed():
await callback.answer("⚠️ В текущем режиме трафик фиксированный и не может быть сброшен", show_alert=True)
return
-
+
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
if not subscription or subscription.is_trial:
await callback.answer("⌛ Эта функция доступна только для платных подписок", show_alert=True)
return
-
+
if subscription.traffic_limit_gb == 0:
await callback.answer("⌛ У вас безлимитный трафик", show_alert=True)
return
-
+
reset_price = PERIOD_PRICES[30]
-
+
if db_user.balance_kopeks < reset_price:
await callback.answer("⌛ Недостаточно средств на балансе", show_alert=True)
return
-
+
await callback.message.edit_text(
f"🔄 Сброс трафика\n\n"
f"Использовано: {texts.format_traffic(subscription.traffic_used_gb)}\n"
@@ -2257,7 +2261,7 @@ async def handle_reset_traffic(
"После сброса счетчик использованного трафика станет равным 0.",
reply_markup=get_reset_traffic_confirm_keyboard(reset_price, db_user.language)
)
-
+
await callback.answer()
@@ -2268,19 +2272,18 @@ def update_traffic_prices():
async def confirm_add_devices(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
devices_count = int(callback.data.split('_')[2])
texts = get_texts(db_user.language)
subscription = db_user.subscription
resume_callback = None
-
+
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}). "
@@ -2288,7 +2291,7 @@ async def confirm_add_devices(
show_alert=True
)
return
-
+
devices_price_per_month = devices_count * settings.PRICE_PER_DEVICE
months_hint = get_remaining_months(subscription.end_date)
period_hint_days = months_hint * 30 if months_hint > 0 else None
@@ -2315,7 +2318,7 @@ async def confirm_add_devices(
price / 100,
total_discount / 100,
)
-
+
if db_user.balance_kopeks < price:
missing_kopeks = price - db_user.balance_kopeks
required_text = f"{texts.format_price(price)} (за {charged_months} мес)"
@@ -2345,22 +2348,22 @@ async def confirm_add_devices(
)
await callback.answer()
return
-
+
try:
success = await subtract_user_balance(
db, db_user, price,
f"Добавление {devices_count} устройств на {charged_months} мес"
)
-
+
if not success:
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
-
+
await add_subscription_devices(db, subscription, devices_count)
-
+
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
-
+
await create_transaction(
db=db,
user_id=db_user.id,
@@ -2368,11 +2371,10 @@ async def confirm_add_devices(
amount_kopeks=price,
description=f"Добавление {devices_count} устройств на {charged_months} мес"
)
-
-
+
await db.refresh(db_user)
await db.refresh(subscription)
-
+
success_text = (
"✅ Устройства успешно добавлены!\n\n"
f"📱 Добавлено: {devices_count} устройств\n"
@@ -2389,23 +2391,23 @@ async def confirm_add_devices(
success_text,
reply_markup=get_back_keyboard(db_user.language)
)
-
- logger.info(f"✅ Пользователь {db_user.telegram_id} добавил {devices_count} устройств за {price/100}₽")
-
+
+ logger.info(f"✅ Пользователь {db_user.telegram_id} добавил {devices_count} устройств за {price / 100}₽")
+
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 confirm_extend_subscription(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
from app.services.admin_notification_service import AdminNotificationService
@@ -2453,7 +2455,7 @@ async def confirm_extend_subscription(
server_uuid_prices[squad_uuid] = discounted_per_month * months_in_period
discounted_servers_price_per_month = servers_price_per_month - (
- servers_price_per_month * servers_discount_percent // 100
+ servers_price_per_month * servers_discount_percent // 100
)
additional_devices = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT)
@@ -2478,9 +2480,9 @@ async def confirm_extend_subscription(
price = base_price + total_servers_price + total_devices_price + total_traffic_price
monthly_additions = (
- discounted_servers_price_per_month
- + discounted_devices_price_per_month
- + discounted_traffic_price_per_month
+ discounted_servers_price_per_month
+ + discounted_devices_price_per_month
+ + discounted_traffic_price_per_month
)
is_valid = validate_pricing_calculation(base_price, monthly_additions, months_in_period, price)
@@ -2490,47 +2492,47 @@ async def confirm_extend_subscription(
return
logger.info(f"💰 Расчет продления подписки {subscription.id} на {days} дней ({months_in_period} мес):")
- base_log = f" 📅 Период {days} дней: {base_price_original/100}₽"
+ base_log = f" 📅 Период {days} дней: {base_price_original / 100}₽"
if base_discount_total > 0:
base_log += (
- f" → {base_price/100}₽"
- f" (скидка {period_discount_percent}%: -{base_discount_total/100}₽)"
+ f" → {base_price / 100}₽"
+ f" (скидка {period_discount_percent}%: -{base_discount_total / 100}₽)"
)
logger.info(base_log)
if total_servers_price > 0:
logger.info(
- f" 🌐 Серверы: {servers_price_per_month/100}₽/мес × {months_in_period}"
- f" = {total_servers_price/100}₽"
+ f" 🌐 Серверы: {servers_price_per_month / 100}₽/мес × {months_in_period}"
+ f" = {total_servers_price / 100}₽"
+ (
f" (скидка {servers_discount_percent}%:"
- f" -{total_servers_discount/100}₽)"
+ f" -{total_servers_discount / 100}₽)"
if total_servers_discount > 0
else ""
)
)
if total_devices_price > 0:
logger.info(
- f" 📱 Устройства: {devices_price_per_month/100}₽/мес × {months_in_period}"
- f" = {total_devices_price/100}₽"
+ f" 📱 Устройства: {devices_price_per_month / 100}₽/мес × {months_in_period}"
+ f" = {total_devices_price / 100}₽"
+ (
f" (скидка {devices_discount_percent}%:"
- f" -{devices_discount_per_month * months_in_period/100}₽)"
+ f" -{devices_discount_per_month * months_in_period / 100}₽)"
if devices_discount_percent > 0 and devices_discount_per_month > 0
else ""
)
)
if total_traffic_price > 0:
logger.info(
- f" 📊 Трафик: {traffic_price_per_month/100}₽/мес × {months_in_period}"
- f" = {total_traffic_price/100}₽"
+ f" 📊 Трафик: {traffic_price_per_month / 100}₽/мес × {months_in_period}"
+ f" = {total_traffic_price / 100}₽"
+ (
f" (скидка {traffic_discount_percent}%:"
- f" -{traffic_discount_per_month * months_in_period/100}₽)"
+ f" -{traffic_discount_per_month * months_in_period / 100}₽)"
if traffic_discount_percent > 0 and traffic_discount_per_month > 0
else ""
)
)
- logger.info(f" 💎 ИТОГО: {price/100}₽")
+ logger.info(f" 💎 ИТОГО: {price / 100}₽")
except Exception as e:
logger.error(f"⚠ ОШИБКА РАСЧЕТА ЦЕНЫ: {e}")
@@ -2665,7 +2667,7 @@ async def confirm_extend_subscription(
reply_markup=get_back_keyboard(db_user.language)
)
- logger.info(f"✅ Пользователь {db_user.telegram_id} продлил подписку на {days} дней за {price/100}₽")
+ logger.info(f"✅ Пользователь {db_user.telegram_id} продлил подписку на {days} дней за {price / 100}₽")
except Exception as e:
logger.error(f"⚠ КРИТИЧЕСКАЯ ОШИБКА ПРОДЛЕНИЯ: {e}")
@@ -2681,19 +2683,19 @@ async def confirm_extend_subscription(
async def confirm_reset_traffic(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
from app.config import settings
-
+
if settings.is_traffic_fixed():
await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True)
return
-
+
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
reset_price = PERIOD_PRICES[30]
if db_user.balance_kopeks < reset_price:
@@ -2723,29 +2725,29 @@ async def confirm_reset_traffic(
)
await callback.answer()
return
-
+
try:
success = await subtract_user_balance(
db, db_user, reset_price,
"Сброс трафика"
)
-
+
if not success:
await callback.answer("⌛ Ошибка списания средств", show_alert=True)
return
-
+
subscription.traffic_used_gb = 0.0
subscription.updated_at = datetime.utcnow()
await db.commit()
-
+
subscription_service = SubscriptionService()
remnawave_service = RemnaWaveService()
-
+
user = db_user
if user.remnawave_uuid:
async with remnawave_service.get_api_client() as api:
await api.reset_user_traffic(user.remnawave_uuid)
-
+
await create_transaction(
db=db,
user_id=db_user.id,
@@ -2753,56 +2755,55 @@ async def confirm_reset_traffic(
amount_kopeks=reset_price,
description="Сброс трафика"
)
-
+
await db.refresh(db_user)
await db.refresh(subscription)
-
+
await callback.message.edit_text(
f"✅ Трафик успешно сброшен!\n\n"
f"🔄 Использованный трафик обнулен\n"
f"📊 Лимит: {texts.format_traffic(subscription.traffic_limit_gb)}",
reply_markup=get_back_keyboard(db_user.language)
)
-
+
logger.info(f"✅ Пользователь {db_user.telegram_id} сбросил трафик")
-
+
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 select_period(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User
):
period_days = int(callback.data.split('_')[1])
texts = get_texts(db_user.language)
-
+
data = await state.get_data()
data['period_days'] = period_days
data['total_price'] = PERIOD_PRICES[period_days]
-
+
if settings.is_traffic_fixed():
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)
@@ -2821,7 +2822,7 @@ async def select_period(
available_countries = [c for c in countries if c.get('is_available', True)]
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
await state.set_data(data)
-
+
selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
await callback.message.edit_text(
@@ -2829,67 +2830,69 @@ async def select_period(
reply_markup=get_devices_keyboard(selected_devices, db_user.language)
)
await state.set_state(SubscriptionStates.selecting_devices)
-
+
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}₽")
-
+ 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}₽")
-
+ 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" • {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)
countries_info = await _get_countries_info(subscription.connected_squads)
countries_text = ", ".join([c['name'] for c in countries_info]) if countries_info else "Нет"
-
+
subscription_url = getattr(subscription, 'subscription_url', None) or "Генерируется..."
-
+
if subscription.is_trial:
status_text = "🎁 Тестовая"
type_text = "Триал"
@@ -2899,7 +2902,7 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess
else:
status_text = "⌛ Истекла"
type_text = "Платная подписка"
-
+
if subscription.traffic_limit_gb == 0:
if settings.is_traffic_fixed():
traffic_text = "∞ Безлимитный"
@@ -2910,9 +2913,9 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess
traffic_text = f"{subscription.traffic_limit_gb} ГБ"
else:
traffic_text = f"{subscription.traffic_limit_gb} ГБ"
-
+
subscription_cost = await get_subscription_cost(subscription, db)
-
+
info_text = texts.SUBSCRIPTION_INFO.format(
status=status_text,
type=type_text,
@@ -2925,23 +2928,24 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess
devices_limit=subscription.device_limit,
autopay_status="✅ Включен" if subscription.autopay_enabled else "⌛ Выключен"
)
-
+
if subscription_cost > 0:
info_text += f"\n💰 Стоимость подписки в месяц: {texts.format_price(subscription_cost)}"
-
+
if (
- subscription_url
- and subscription_url != "Генерируется..."
- and not settings.should_hide_subscription_link()
+ subscription_url
+ and subscription_url != "Генерируется..."
+ and not settings.should_hide_subscription_link()
):
info_text += f"\n\n🔗 Ваша ссылка для импорта в VPN приложениe:\n{subscription_url}"
return info_text
+
def format_traffic_display(traffic_gb: int, is_fixed_mode: bool = None) -> str:
if is_fixed_mode is None:
is_fixed_mode = settings.is_traffic_fixed()
-
+
if traffic_gb == 0:
if is_fixed_mode:
return "Безлимитный"
@@ -2953,22 +2957,23 @@ def format_traffic_display(traffic_gb: int, is_fixed_mode: bool = None) -> str:
else:
return f"{traffic_gb} ГБ"
+
async def select_traffic(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User
):
traffic_gb = int(callback.data.split('_')[1])
texts = get_texts(db_user.language)
-
+
data = await state.get_data()
data['traffic_gb'] = 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(db_user):
countries = await _get_available_countries(db_user.promo_group_id)
await callback.message.edit_text(
@@ -2981,7 +2986,7 @@ async def select_traffic(
available_countries = [c for c in countries if c.get('is_available', True)]
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
await state.set_data(data)
-
+
selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
await callback.message.edit_text(
@@ -2989,32 +2994,32 @@ async def select_traffic(
reply_markup=get_devices_keyboard(selected_devices, db_user.language)
)
await state.set_state(SubscriptionStates.selecting_devices)
-
+
await callback.answer()
async def select_country(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User,
+ db: AsyncSession
):
country_uuid = callback.data.split('_')[1]
data = await state.get_data()
-
+
selected_countries = data.get('countries', [])
if country_uuid in selected_countries:
selected_countries.remove(country_uuid)
else:
selected_countries.append(country_uuid)
-
+
countries = await _get_available_countries(db_user.promo_group_id)
allowed_country_ids = {country['uuid'] for country in countries}
if country_uuid not in allowed_country_ids and country_uuid not in selected_countries:
await callback.answer("❌ Сервер недоступен для вашей промогруппы", show_alert=True)
return
-
+
period_base_price = PERIOD_PRICES[data['period_days']]
discounted_base_price, _ = apply_percentage_discount(
@@ -3023,7 +3028,7 @@ async def select_country(
)
base_price = discounted_base_price + settings.get_traffic_price(data['traffic_gb'])
-
+
try:
subscription_service = SubscriptionService()
countries_price, _ = await subscription_service.get_countries_price_by_uuids(
@@ -3038,11 +3043,11 @@ async def select_country(
db,
promo_group_id=db_user.promo_group_id,
)
-
+
data['countries'] = selected_countries
data['total_price'] = base_price + countries_price
await state.set_data(data)
-
+
await callback.message.edit_reply_markup(
reply_markup=get_countries_keyboard(countries, selected_countries, db_user.language)
)
@@ -3050,73 +3055,73 @@ async def select_country(
async def countries_continue(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User
):
-
data = await state.get_data()
texts = get_texts(db_user.language)
-
+
if not data.get('countries'):
await callback.answer("⚠️ Выберите хотя бы одну страну!", show_alert=True)
return
-
+
selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
await callback.message.edit_text(
texts.SELECT_DEVICES,
reply_markup=get_devices_keyboard(selected_devices, db_user.language)
)
-
+
await state.set_state(SubscriptionStates.selecting_devices)
await callback.answer()
async def select_devices(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User
):
if not callback.data.startswith("devices_") or callback.data == "devices_continue":
await callback.answer("❌ Некорректный запрос", show_alert=True)
return
-
+
try:
devices = int(callback.data.split('_')[1])
except (ValueError, IndexError):
await callback.answer("❌ Некорректное количество устройств", show_alert=True)
return
-
+
data = await state.get_data()
-
+
base_price = (
- PERIOD_PRICES[data['period_days']] +
- settings.get_traffic_price(data['traffic_gb'])
+ PERIOD_PRICES[data['period_days']] +
+ settings.get_traffic_price(data['traffic_gb'])
)
-
+
countries = await _get_available_countries(db_user.promo_group_id)
countries_price = sum(
- c['price_kopeks'] for c in countries
+ c['price_kopeks'] for c in countries
if c['uuid'] in data['countries']
)
-
+
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
-
+
data['devices'] = devices
data['total_price'] = base_price + countries_price + devices_price
await state.set_data(data)
-
+
await callback.message.edit_reply_markup(
reply_markup=get_devices_keyboard(devices, db_user.language)
)
await callback.answer()
+
async def devices_continue(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User,
+ db: AsyncSession
):
if not callback.data == "devices_continue":
await callback.answer("⚠️ Некорректный запрос", show_alert=True)
@@ -3146,13 +3151,13 @@ async def devices_continue(
async def confirm_purchase(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User,
+ db: AsyncSession
):
from app.services.admin_notification_service import AdminNotificationService
-
+
data = await state.get_data()
texts = get_texts(db_user.language)
@@ -3164,7 +3169,7 @@ async def confirm_purchase(
)
countries = await _get_available_countries(db_user.promo_group_id)
-
+
months_in_period = data.get(
'months_in_period', calculate_months_from_days(data['period_days'])
)
@@ -3318,55 +3323,55 @@ async def confirm_purchase(
months_in_period,
final_price,
)
-
+
if not is_valid:
logger.error(f"Ошибка в расчете цены подписки для пользователя {db_user.telegram_id}")
await callback.answer("Ошибка расчета цены. Обратитесь в поддержку.", show_alert=True)
return
-
+
logger.info(f"Расчет покупки подписки на {data['period_days']} дней ({months_in_period} мес):")
- base_log = f" Период: {base_price_original/100}₽"
+ base_log = f" Период: {base_price_original / 100}₽"
if base_discount_total and base_discount_total > 0:
base_log += (
- f" → {base_price/100}₽"
- f" (скидка {base_discount_percent}%: -{base_discount_total/100}₽)"
+ f" → {base_price / 100}₽"
+ f" (скидка {base_discount_percent}%: -{base_discount_total / 100}₽)"
)
logger.info(base_log)
if total_traffic_price > 0:
message = (
- f" Трафик: {traffic_price_per_month/100}₽/мес × {months_in_period}"
- f" = {total_traffic_price/100}₽"
+ f" Трафик: {traffic_price_per_month / 100}₽/мес × {months_in_period}"
+ f" = {total_traffic_price / 100}₽"
)
if traffic_discount_total > 0:
message += (
f" (скидка {traffic_discount_percent}%:"
- f" -{traffic_discount_total/100}₽)"
+ f" -{traffic_discount_total / 100}₽)"
)
logger.info(message)
if total_servers_price > 0:
message = (
- f" Серверы: {countries_price_per_month/100}₽/мес × {months_in_period}"
- f" = {total_servers_price/100}₽"
+ f" Серверы: {countries_price_per_month / 100}₽/мес × {months_in_period}"
+ f" = {total_servers_price / 100}₽"
)
if total_servers_discount > 0:
message += (
f" (скидка {servers_discount_percent}%:"
- f" -{total_servers_discount/100}₽)"
+ f" -{total_servers_discount / 100}₽)"
)
logger.info(message)
if total_devices_price > 0:
message = (
- f" Устройства: {devices_price_per_month/100}₽/мес × {months_in_period}"
- f" = {total_devices_price/100}₽"
+ f" Устройства: {devices_price_per_month / 100}₽/мес × {months_in_period}"
+ f" = {total_devices_price / 100}₽"
)
if devices_discount_total > 0:
message += (
f" (скидка {devices_discount_percent}%:"
- f" -{devices_discount_total/100}₽)"
+ f" -{devices_discount_total / 100}₽)"
)
logger.info(message)
- logger.info(f" ИТОГО: {final_price/100}₽")
-
+ logger.info(f" ИТОГО: {final_price / 100}₽")
+
if db_user.balance_kopeks < final_price:
missing_kopeks = final_price - db_user.balance_kopeks
message_text = texts.t(
@@ -3395,7 +3400,7 @@ async def confirm_purchase(
)
await callback.answer()
return
-
+
purchase_completed = False
try:
@@ -3403,7 +3408,7 @@ async def confirm_purchase(
db, db_user, final_price,
f"Покупка подписки на {data['period_days']} дней"
)
-
+
if not success:
missing_kopeks = final_price - db_user.balance_kopeks
message_text = texts.t(
@@ -3432,7 +3437,7 @@ async def confirm_purchase(
)
await callback.answer()
return
-
+
existing_subscription = db_user.subscription
was_trial_conversion = False
current_time = datetime.utcnow()
@@ -3468,10 +3473,11 @@ async def confirm_purchase(
first_payment_amount_kopeks=final_price,
first_paid_period_days=data['period_days']
)
- logger.info(f"Записана конверсия: {trial_duration} дн. триал → {data['period_days']} дн. платная за {final_price/100}₽")
+ logger.info(
+ f"Записана конверсия: {trial_duration} дн. триал → {data['period_days']} дн. платная за {final_price / 100}₽")
except Exception as conversion_error:
logger.error(f"Ошибка записи конверсии: {conversion_error}")
-
+
existing_subscription.is_trial = False
existing_subscription.status = SubscriptionStatus.ACTIVE.value
existing_subscription.traffic_limit_gb = final_traffic_gb
@@ -3487,7 +3493,7 @@ async def confirm_purchase(
await db.commit()
await db.refresh(existing_subscription)
subscription = existing_subscription
-
+
else:
logger.info(f"Создаем новую подписку для пользователя {db_user.telegram_id}")
subscription = await create_paid_subscription_with_traffic_mode(
@@ -3498,25 +3504,25 @@ async def confirm_purchase(
connected_squads=data['countries'],
traffic_gb=final_traffic_gb
)
-
+
from app.utils.user_utils import mark_user_as_had_paid_subscription
await mark_user_as_had_paid_subscription(db, db_user)
-
+
from app.database.crud.server_squad import get_server_ids_by_uuids, add_user_to_servers
from app.database.crud.subscription import add_subscription_servers
-
+
server_ids = await get_server_ids_by_uuids(db, data['countries'])
-
+
if server_ids:
await add_subscription_servers(db, subscription, server_ids, server_prices)
await add_user_to_servers(db, server_ids)
-
+
logger.info(f"Сохранены цены серверов за весь период: {server_prices}")
-
+
await db.refresh(db_user)
-
+
subscription_service = SubscriptionService()
-
+
if db_user.remnawave_uuid:
remnawave_user = await subscription_service.update_remnawave_user(
db,
@@ -3531,7 +3537,7 @@ async def confirm_purchase(
reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
reset_reason="покупка подписки",
)
-
+
if not remnawave_user:
logger.error(f"Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
remnawave_user = await subscription_service.create_remnawave_user(
@@ -3540,7 +3546,7 @@ async def confirm_purchase(
reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
reset_reason="покупка подписки (повторная попытка)",
)
-
+
transaction = await create_transaction(
db=db,
user_id=db_user.id,
@@ -3548,7 +3554,7 @@ async def confirm_purchase(
amount_kopeks=final_price,
description=f"Подписка на {data['period_days']} дней ({months_in_period} мес)"
)
-
+
try:
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_subscription_purchase_notification(
@@ -3556,39 +3562,39 @@ async def confirm_purchase(
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о покупке: {e}")
-
+
await db.refresh(db_user)
await db.refresh(subscription)
-
+
subscription_link = get_display_subscription_link(subscription)
hide_subscription_link = settings.should_hide_subscription_link()
if remnawave_user and subscription_link:
if settings.is_happ_cryptolink_mode():
success_text = (
- f"{texts.SUBSCRIPTION_PURCHASED}\n\n"
- + texts.t(
- "SUBSCRIPTION_HAPP_LINK_PROMPT",
- "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.",
- )
- + "\n\n"
- + texts.t(
- "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
- "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
- )
+ f"{texts.SUBSCRIPTION_PURCHASED}\n\n"
+ + texts.t(
+ "SUBSCRIPTION_HAPP_LINK_PROMPT",
+ "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.",
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
+ "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
+ )
)
elif hide_subscription_link:
success_text = (
- f"{texts.SUBSCRIPTION_PURCHASED}\n\n"
- + texts.t(
- "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
- "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
- )
- + "\n\n"
- + texts.t(
- "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
- "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
- )
+ f"{texts.SUBSCRIPTION_PURCHASED}\n\n"
+ + texts.t(
+ "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
+ "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
+ "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
+ )
)
else:
import_link_section = texts.t(
@@ -3612,7 +3618,8 @@ async def confirm_purchase(
web_app=types.WebAppInfo(url=subscription_link),
)
],
- [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")],
+ [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")],
])
elif connect_mode == "miniapp_custom":
if not settings.MINIAPP_CUSTOM_URL:
@@ -3632,7 +3639,8 @@ async def confirm_purchase(
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
)
],
- [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")],
+ [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")],
])
elif connect_mode == "link":
rows = [
@@ -3641,7 +3649,8 @@ async def confirm_purchase(
happ_row = get_happ_download_button_row(texts)
if happ_row:
rows.append(happ_row)
- rows.append([InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")])
+ rows.append([InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")])
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
elif connect_mode == "happ_cryptolink":
rows = [
@@ -3655,14 +3664,17 @@ async def confirm_purchase(
happ_row = get_happ_download_button_row(texts)
if happ_row:
rows.append(happ_row)
- rows.append([InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")])
+ rows.append([InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")])
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
else:
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
- [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), callback_data="subscription_connect")],
- [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")],
+ [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
+ callback_data="subscription_connect")],
+ [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")],
])
-
+
await callback.message.edit_text(
success_text,
reply_markup=connect_keyboard,
@@ -3676,17 +3688,18 @@ async def confirm_purchase(
).format(purchase_text=texts.SUBSCRIPTION_PURCHASED),
reply_markup=get_back_keyboard(db_user.language)
)
-
+
purchase_completed = True
- logger.info(f"Пользователь {db_user.telegram_id} купил подписку на {data['period_days']} дней за {final_price/100}₽")
-
+ logger.info(
+ f"Пользователь {db_user.telegram_id} купил подписку на {data['period_days']} дней за {final_price / 100}₽")
+
except Exception as e:
logger.error(f"Ошибка покупки подписки: {e}")
await callback.message.edit_text(
texts.ERROR,
reply_markup=get_back_keyboard(db_user.language)
)
-
+
if purchase_completed:
await clear_subscription_checkout_draft(db_user.id)
@@ -3694,11 +3707,10 @@ async def confirm_purchase(
await callback.answer()
-
async def resume_subscription_checkout(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User,
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User,
):
texts = get_texts(db_user.language)
@@ -3729,19 +3741,21 @@ async def resume_subscription_checkout(
)
await callback.answer()
+
+
async def add_traffic(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ 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
-
+
base_price = settings.get_traffic_price(traffic_gb)
if base_price == 0 and traffic_gb != 0:
@@ -3797,7 +3811,7 @@ async def add_traffic(
)
await callback.answer()
return
-
+
try:
success = await subtract_user_balance(
db,
@@ -3805,19 +3819,19 @@ async def add_traffic(
price,
f"Добавление {traffic_gb} ГБ трафика",
)
-
+
if not success:
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
-
- if traffic_gb == 0:
+
+ 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,
@@ -3825,11 +3839,10 @@ async def add_traffic(
amount_kopeks=price,
description=f"Добавление {traffic_gb} ГБ трафика",
)
-
-
+
await db.refresh(db_user)
await db.refresh(subscription)
-
+
success_text = f"✅ Трафик успешно добавлен!\n\n"
if traffic_gb == 0:
success_text += "🎉 Теперь у вас безлимитный трафик!"
@@ -3849,36 +3862,37 @@ async def add_traffic(
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,
- duration_days: int,
- device_limit: int,
- connected_squads: List[str],
- traffic_gb: Optional[int] = None
+ db: AsyncSession,
+ user_id: int,
+ duration_days: int,
+ device_limit: int,
+ connected_squads: List[str],
+ traffic_gb: Optional[int] = None
):
from app.config import settings
-
+
if traffic_gb is None:
if settings.is_traffic_fixed():
traffic_limit_gb = settings.get_fixed_traffic_limit()
else:
- traffic_limit_gb = 0
+ traffic_limit_gb = 0
else:
traffic_limit_gb = traffic_gb
-
+
subscription = await create_paid_subscription(
db=db,
user_id=user_id,
@@ -3887,35 +3901,36 @@ async def create_paid_subscription_with_traffic_mode(
device_limit=device_limit,
connected_squads=connected_squads
)
-
+
logger.info(f"📋 Создана подписка с трафиком: {traffic_limit_gb} ГБ (режим: {settings.TRAFFIC_SELECTION_MODE})")
-
+
return subscription
+
def validate_traffic_price(gb: int) -> bool:
from app.config import settings
-
+
price = settings.get_traffic_price(gb)
- if gb == 0:
+ if gb == 0:
return True
-
+
return price > 0
async def handle_subscription_settings(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
if not subscription or subscription.is_trial:
await callback.answer("⚠️ Настройки доступны только для платных подписок", show_alert=True)
return
-
+
devices_used = await get_current_devices_count(db_user)
-
+
settings_text = f"""
⚙️ Настройки подписки
@@ -3926,9 +3941,9 @@ async def handle_subscription_settings(
Выберите что хотите изменить:
"""
-
+
show_countries = await _should_show_countries_management(db_user)
-
+
await callback.message.edit_text(
settings_text,
reply_markup=get_updated_subscription_settings_keyboard(db_user.language, show_countries),
@@ -3938,24 +3953,23 @@ async def handle_subscription_settings(
async def handle_autopay_menu(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
subscription = db_user.subscription
if not subscription:
await callback.answer("⚠️ У вас нет активной подписки!", show_alert=True)
return
-
+
status = "включен" if subscription.autopay_enabled else "выключен"
days = subscription.autopay_days_before
-
+
text = f"💳 Автоплатеж\n\n"
text += f"📊 Статус: {status}\n"
text += f"⏰ Списание за: {days} дн. до окончания\n\n"
text += "Выберите действие:"
-
+
await callback.message.edit_text(
text,
reply_markup=get_autopay_keyboard(db_user.language)
@@ -3964,27 +3978,25 @@ async def handle_autopay_menu(
async def toggle_autopay(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
subscription = db_user.subscription
enable = callback.data == "autopay_enable"
-
+
await update_subscription_autopay(db, subscription, enable)
-
+
status = "включен" if enable else "выключен"
await callback.answer(f"✅ Автоплатеж {status}!")
-
+
await handle_autopay_menu(callback, db_user, db)
async def show_autopay_days(
- callback: types.CallbackQuery,
- db_user: User
+ callback: types.CallbackQuery,
+ db_user: User
):
-
await callback.message.edit_text(
"⏰ Выберите за сколько дней до окончания списывать средства:",
reply_markup=get_autopay_days_keyboard(db_user.language)
@@ -3993,31 +4005,31 @@ async def show_autopay_days(
async def set_autopay_days(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
days = int(callback.data.split('_')[2])
subscription = db_user.subscription
-
+
await update_subscription_autopay(
db, subscription, subscription.autopay_enabled, days
)
-
+
await callback.answer(f"✅ Установлено {days} дней!")
-
+
await handle_autopay_menu(callback, db_user, db)
+
async def handle_subscription_config_back(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User,
+ db: AsyncSession
):
current_state = await state.get_state()
texts = get_texts(db_user.language)
-
+
if current_state == SubscriptionStates.selecting_traffic.state:
await callback.message.edit_text(
_build_subscription_period_prompt(db_user, texts),
@@ -4062,21 +4074,21 @@ async def handle_subscription_config_back(
reply_markup=get_subscription_period_keyboard(db_user.language)
)
await state.set_state(SubscriptionStates.selecting_period)
-
+
else:
from app.handlers.menu import show_main_menu
await show_main_menu(callback, db_user, db)
await state.clear()
-
+
await callback.answer()
-async def handle_subscription_cancel(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User,
- db: AsyncSession
-):
+async def handle_subscription_cancel(
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User,
+ db: AsyncSession
+):
texts = get_texts(db_user.language)
await state.clear()
@@ -4087,6 +4099,7 @@ async def handle_subscription_cancel(
await callback.answer("❌ Покупка отменена")
+
async def _get_available_countries(promo_group_id: Optional[int] = None):
from app.utils.cache import cache, cache_key
from app.database.database import AsyncSessionLocal
@@ -4115,23 +4128,24 @@ async def _get_available_countries(promo_group_id: Optional[int] = None):
for server in available_servers:
countries.append({
"uuid": server.squad_uuid,
- "name": server.display_name,
+ "name": server.display_name,
"price_kopeks": server.price_kopeks,
"country_code": server.country_code,
"is_available": server.is_available and not server.is_full
})
-
+
if not countries:
logger.info("🔄 Серверов в БД нет, получаем из RemnaWave...")
from app.services.remnawave_service import RemnaWaveService
-
+
service = RemnaWaveService()
squads = await service.get_all_squads()
-
+
for squad in squads:
squad_name = squad["name"]
-
- if not any(flag in squad_name for flag in ["🇳🇱", "🇩🇪", "🇺🇸", "🇫🇷", "🇬🇧", "🇮🇹", "🇪🇸", "🇨🇦", "🇯🇵", "🇸🇬", "🇦🇺"]):
+
+ if not any(flag in squad_name for flag in
+ ["🇳🇱", "🇩🇪", "🇺🇸", "🇫🇷", "🇬🇧", "🇮🇹", "🇪🇸", "🇨🇦", "🇯🇵", "🇸🇬", "🇦🇺"]):
name_lower = squad_name.lower()
if "netherlands" in name_lower or "нидерланды" in name_lower or "nl" in name_lower:
squad_name = f"🇳🇱 {squad_name}"
@@ -4141,14 +4155,14 @@ async def _get_available_countries(promo_group_id: Optional[int] = None):
squad_name = f"🇺🇸 {squad_name}"
else:
squad_name = f"🌐 {squad_name}"
-
+
countries.append({
"uuid": squad["uuid"],
"name": squad_name,
- "price_kopeks": 0,
+ "price_kopeks": 0,
"is_available": True
})
-
+
await cache.set(cache_key_value, countries, 300)
return countries
@@ -4161,35 +4175,36 @@ async def _get_available_countries(promo_group_id: Optional[int] = None):
await cache.set(cache_key_value, fallback_countries, 60)
return fallback_countries
+
async def _get_countries_info(squad_uuids):
countries = await _get_available_countries()
return [c for c in countries if c['uuid'] in squad_uuids]
+
async def handle_reset_devices(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
await handle_device_management(callback, db_user, db)
+
async def handle_add_country_to_subscription(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext
):
-
logger.info(f"🔍 handle_add_country_to_subscription вызван для {db_user.telegram_id}")
logger.info(f"🔍 Callback data: {callback.data}")
-
+
current_state = await state.get_state()
logger.info(f"🔍 Текущее состояние: {current_state}")
-
+
country_uuid = callback.data.split('_')[1]
data = await state.get_data()
logger.info(f"🔍 Данные состояния: {data}")
-
+
selected_countries = data.get('countries', [])
countries = await _get_available_countries(db_user.promo_group_id)
allowed_country_ids = {country['uuid'] for country in countries}
@@ -4197,14 +4212,14 @@ async def handle_add_country_to_subscription(
if country_uuid not in allowed_country_ids and country_uuid not in selected_countries:
await callback.answer("❌ Сервер недоступен для вашей промогруппы", show_alert=True)
return
-
+
if country_uuid in selected_countries:
selected_countries.remove(country_uuid)
logger.info(f"🔍 Удалена страна: {country_uuid}")
else:
selected_countries.append(country_uuid)
logger.info(f"🔍 Добавлена страна: {country_uuid}")
-
+
total_price = 0
subscription = db_user.subscription
period_hint_days = _get_period_hint_from_subscription(subscription)
@@ -4219,8 +4234,8 @@ async def handle_add_country_to_subscription(
continue
if (
- country['uuid'] in selected_countries
- and country['uuid'] not in subscription.connected_squads
+ country['uuid'] in selected_countries
+ and country['uuid'] not in subscription.connected_squads
):
server_price = country['price_kopeks']
if servers_discount_percent > 0 and server_price > 0:
@@ -4238,7 +4253,7 @@ async def handle_add_country_to_subscription(
logger.info(f"🔍 Новые выбранные страны: {selected_countries}")
logger.info(f"🔍 Общая стоимость: {total_price}")
-
+
try:
from app.keyboards.inline import get_manage_countries_keyboard
await callback.message.edit_reply_markup(
@@ -4254,9 +4269,10 @@ async def handle_add_country_to_subscription(
logger.info(f"✅ Клавиатура обновлена")
except Exception as e:
logger.error(f"❌ Ошибка обновления клавиатуры: {e}")
-
+
await callback.answer()
+
async def _should_show_countries_management(user: Optional[User] = None) -> bool:
try:
promo_group_id = user.promo_group_id if user else None
@@ -4294,16 +4310,15 @@ async def _should_show_countries_management(user: Optional[User] = None) -> bool
async def confirm_add_countries_to_subscription(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext
):
-
data = await state.get_data()
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
selected_countries = data.get('countries', [])
current_countries = subscription.connected_squads
@@ -4318,11 +4333,11 @@ async def confirm_add_countries_to_subscription(
new_countries = [c for c in selected_countries if c not in current_countries]
removed_countries = [c for c in current_countries if c not in selected_countries]
-
+
if not new_countries and not removed_countries:
await callback.answer("⚠️ Изменения не обнаружены", show_alert=True)
return
-
+
total_price = 0
new_countries_names = []
removed_countries_names = []
@@ -4360,7 +4375,7 @@ async def confirm_add_countries_to_subscription(
new_countries_names.append(country['name'])
if country['uuid'] in removed_countries:
removed_countries_names.append(country['name'])
-
+
if new_countries and db_user.balance_kopeks < total_price:
missing_kopeks = total_price - db_user.balance_kopeks
message_text = texts.t(
@@ -4389,18 +4404,18 @@ async def confirm_add_countries_to_subscription(
await state.clear()
await callback.answer()
return
-
+
try:
if new_countries and total_price > 0:
success = await subtract_user_balance(
db, db_user, total_price,
f"Добавление стран к подписке: {', '.join(new_countries_names)}"
)
-
+
if not success:
await callback.answer("❌ Ошибка списания средств", show_alert=True)
return
-
+
await create_transaction(
db=db,
user_id=db_user.id,
@@ -4408,19 +4423,19 @@ async def confirm_add_countries_to_subscription(
amount_kopeks=total_price,
description=f"Добавление стран к подписке: {', '.join(new_countries_names)}"
)
-
+
subscription.connected_squads = selected_countries
subscription.updated_at = datetime.utcnow()
await db.commit()
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
-
+
await db.refresh(db_user)
await db.refresh(subscription)
-
+
success_text = "✅ Страны успешно обновлены!\n\n"
-
+
if new_countries_names:
success_text += f"➕ Добавлены страны:\n{chr(10).join(f'• {name}' for name in new_countries_names)}\n"
if total_price > 0:
@@ -4431,42 +4446,44 @@ async def confirm_add_countries_to_subscription(
f" -{texts.format_price(total_discount_value)})"
)
success_text += "\n"
-
+
if removed_countries_names:
success_text += f"\n➖ Отключены страны:\n{chr(10).join(f'• {name}' for name in removed_countries_names)}\n"
success_text += "ℹ️ Повторное подключение будет платным\n"
-
+
success_text += f"\n🌍 Активных стран: {len(selected_countries)}"
-
+
await callback.message.edit_text(
success_text,
reply_markup=get_back_keyboard(db_user.language)
)
-
- logger.info(f"✅ Пользователь {db_user.telegram_id} обновил страны подписки. Добавлено: {len(new_countries)}, убрано: {len(removed_countries)}")
-
+
+ logger.info(
+ f"✅ Пользователь {db_user.telegram_id} обновил страны подписки. Добавлено: {len(new_countries)}, убрано: {len(removed_countries)}")
+
except Exception as e:
logger.error(f"Ошибка обновления стран подписки: {e}")
await callback.message.edit_text(
texts.ERROR,
reply_markup=get_back_keyboard(db_user.language)
)
-
+
await state.clear()
await callback.answer()
-async def confirm_reset_devices(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
-):
+async def confirm_reset_devices(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
+):
await handle_device_management(callback, db_user, db)
+
async def handle_happ_download_request(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
texts = get_texts(db_user.language)
prompt_text = texts.t(
@@ -4481,9 +4498,9 @@ async def handle_happ_download_request(
async def handle_happ_download_platform_choice(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
platform = callback.data.split('_')[-1]
if platform == "pc":
@@ -4517,9 +4534,9 @@ async def handle_happ_download_platform_choice(
async def handle_happ_download_close(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
try:
await callback.message.delete()
@@ -4530,9 +4547,9 @@ async def handle_happ_download_close(
async def handle_happ_download_back(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
texts = get_texts(db_user.language)
prompt_text = texts.t(
@@ -4545,10 +4562,11 @@ async def handle_happ_download_back(
await callback.message.edit_text(prompt_text, reply_markup=keyboard, parse_mode="HTML")
await callback.answer()
+
async def handle_connect_subscription(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
texts = get_texts(db_user.language)
subscription = db_user.subscription
@@ -4707,14 +4725,14 @@ async def handle_connect_subscription(
reply_markup=get_device_selection_keyboard(db_user.language),
parse_mode="HTML"
)
-
+
await callback.answer()
async def claim_discount_offer(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
):
texts = get_texts(db_user.language)
@@ -4782,11 +4800,11 @@ async def claim_discount_offer(
async def handle_device_guide(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
- device_type = callback.data.split('_')[2]
+ device_type = callback.data.split('_')[2]
texts = get_texts(db_user.language)
subscription = db_user.subscription
subscription_link = get_display_subscription_link(subscription)
@@ -4812,62 +4830,62 @@ async def handle_device_guide(
if hide_subscription_link:
link_section = (
- texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
- + "\n"
- + texts.t(
- "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
- "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
- )
- + "\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
+ + "\n"
+ + texts.t(
+ "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
+ "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
+ )
+ + "\n\n"
)
else:
link_section = (
- texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
- + f"\n{subscription_link}\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
+ + f"\n{subscription_link}\n\n"
)
guide_text = (
- texts.t(
- "SUBSCRIPTION_DEVICE_GUIDE_TITLE",
- "📱 Настройка для {device_name}",
- ).format(device_name=get_device_name(device_type, db_user.language))
- + "\n\n"
- + link_section
- + texts.t(
- "SUBSCRIPTION_DEVICE_FEATURED_APP",
- "📋 Рекомендуемое приложение: {app_name}",
- ).format(app_name=featured_app['name'])
- + "\n\n"
- + texts.t("SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE", "Шаг 1 - Установка:")
- + f"\n{featured_app['installationStep']['description'][db_user.language]}\n\n"
- + texts.t("SUBSCRIPTION_DEVICE_STEP_ADD_TITLE", "Шаг 2 - Добавление подписки:")
- + f"\n{featured_app['addSubscriptionStep']['description'][db_user.language]}\n\n"
- + texts.t("SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE", "Шаг 3 - Подключение:")
- + f"\n{featured_app['connectAndUseStep']['description'][db_user.language]}\n\n"
- + texts.t("SUBSCRIPTION_DEVICE_HOW_TO_TITLE", "💡 Как подключить:")
- + "\n"
- + "\n".join(
- [
- texts.t(
- "SUBSCRIPTION_DEVICE_HOW_TO_STEP1",
- "1. Установите приложение по ссылке выше",
- ),
- texts.t(
- "SUBSCRIPTION_DEVICE_HOW_TO_STEP2",
- "2. Скопируйте ссылку подписки (нажмите на неё)",
- ),
- texts.t(
- "SUBSCRIPTION_DEVICE_HOW_TO_STEP3",
- "3. Откройте приложение и вставьте ссылку",
- ),
- texts.t(
- "SUBSCRIPTION_DEVICE_HOW_TO_STEP4",
- "4. Подключитесь к серверу",
- ),
- ]
- )
+ texts.t(
+ "SUBSCRIPTION_DEVICE_GUIDE_TITLE",
+ "📱 Настройка для {device_name}",
+ ).format(device_name=get_device_name(device_type, db_user.language))
+ + "\n\n"
+ + link_section
+ + texts.t(
+ "SUBSCRIPTION_DEVICE_FEATURED_APP",
+ "📋 Рекомендуемое приложение: {app_name}",
+ ).format(app_name=featured_app['name'])
+ + "\n\n"
+ + texts.t("SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE", "Шаг 1 - Установка:")
+ + f"\n{featured_app['installationStep']['description'][db_user.language]}\n\n"
+ + texts.t("SUBSCRIPTION_DEVICE_STEP_ADD_TITLE", "Шаг 2 - Добавление подписки:")
+ + f"\n{featured_app['addSubscriptionStep']['description'][db_user.language]}\n\n"
+ + texts.t("SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE", "Шаг 3 - Подключение:")
+ + f"\n{featured_app['connectAndUseStep']['description'][db_user.language]}\n\n"
+ + texts.t("SUBSCRIPTION_DEVICE_HOW_TO_TITLE", "💡 Как подключить:")
+ + "\n"
+ + "\n".join(
+ [
+ texts.t(
+ "SUBSCRIPTION_DEVICE_HOW_TO_STEP1",
+ "1. Установите приложение по ссылке выше",
+ ),
+ texts.t(
+ "SUBSCRIPTION_DEVICE_HOW_TO_STEP2",
+ "2. Скопируйте ссылку подписки (нажмите на неё)",
+ ),
+ texts.t(
+ "SUBSCRIPTION_DEVICE_HOW_TO_STEP3",
+ "3. Откройте приложение и вставьте ссылку",
+ ),
+ texts.t(
+ "SUBSCRIPTION_DEVICE_HOW_TO_STEP4",
+ "4. Подключитесь к серверу",
+ ),
+ ]
)
-
+ )
+
await callback.message.edit_text(
guide_text,
reply_markup=get_connection_guide_keyboard(
@@ -4881,16 +4899,16 @@ async def handle_device_guide(
async def handle_app_selection(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
- device_type = callback.data.split('_')[2]
+ device_type = callback.data.split('_')[2]
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
apps = get_apps_for_device(device_type, db_user.language)
-
+
if not apps:
await callback.answer(
texts.t("SUBSCRIPTION_DEVICE_APPS_NOT_FOUND", "❌ Приложения для этого устройства не найдены"),
@@ -4899,14 +4917,14 @@ async def handle_app_selection(
return
app_text = (
- texts.t(
- "SUBSCRIPTION_APPS_TITLE",
- "📱 Приложения для {device_name}",
- ).format(device_name=get_device_name(device_type, db_user.language))
- + "\n\n"
- + texts.t("SUBSCRIPTION_APPS_PROMPT", "Выберите приложение для подключения:")
+ texts.t(
+ "SUBSCRIPTION_APPS_TITLE",
+ "📱 Приложения для {device_name}",
+ ).format(device_name=get_device_name(device_type, db_user.language))
+ + "\n\n"
+ + texts.t("SUBSCRIPTION_APPS_PROMPT", "Выберите приложение для подключения:")
)
-
+
await callback.message.edit_text(
app_text,
reply_markup=get_app_selection_keyboard(device_type, apps, db_user.language),
@@ -4916,14 +4934,14 @@ async def handle_app_selection(
async def handle_specific_app_guide(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
- _, device_type, app_id = callback.data.split('_')
+ _, device_type, app_id = callback.data.split('_')
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
subscription_link = get_display_subscription_link(subscription)
if not subscription_link:
@@ -4935,7 +4953,7 @@ async def handle_specific_app_guide(
apps = get_apps_for_device(device_type, db_user.language)
app = next((a for a in apps if a['id'] == app_id), None)
-
+
if not app:
await callback.answer(
texts.t("SUBSCRIPTION_APP_NOT_FOUND", "❌ Приложение не найдено"),
@@ -4947,46 +4965,46 @@ async def handle_specific_app_guide(
if hide_subscription_link:
link_section = (
- texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
- + "\n"
- + texts.t(
- "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
- "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
- )
- + "\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
+ + "\n"
+ + texts.t(
+ "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
+ "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
+ )
+ + "\n\n"
)
else:
link_section = (
- texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
- + f"\n{subscription_link}\n\n"
+ texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
+ + f"\n{subscription_link}\n\n"
)
guide_text = (
- texts.t(
- "SUBSCRIPTION_SPECIFIC_APP_TITLE",
- "📱 {app_name} - {device_name}",
- ).format(app_name=app['name'], device_name=get_device_name(device_type, db_user.language))
- + "\n\n"
- + link_section
- + texts.t("SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE", "Шаг 1 - Установка:")
- + f"\n{app['installationStep']['description'][db_user.language]}\n\n"
- + texts.t("SUBSCRIPTION_DEVICE_STEP_ADD_TITLE", "Шаг 2 - Добавление подписки:")
- + f"\n{app['addSubscriptionStep']['description'][db_user.language]}\n\n"
- + texts.t("SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE", "Шаг 3 - Подключение:")
- + f"\n{app['connectAndUseStep']['description'][db_user.language]}"
+ texts.t(
+ "SUBSCRIPTION_SPECIFIC_APP_TITLE",
+ "📱 {app_name} - {device_name}",
+ ).format(app_name=app['name'], device_name=get_device_name(device_type, db_user.language))
+ + "\n\n"
+ + link_section
+ + texts.t("SUBSCRIPTION_DEVICE_STEP_INSTALL_TITLE", "Шаг 1 - Установка:")
+ + f"\n{app['installationStep']['description'][db_user.language]}\n\n"
+ + texts.t("SUBSCRIPTION_DEVICE_STEP_ADD_TITLE", "Шаг 2 - Добавление подписки:")
+ + f"\n{app['addSubscriptionStep']['description'][db_user.language]}\n\n"
+ + texts.t("SUBSCRIPTION_DEVICE_STEP_CONNECT_TITLE", "Шаг 3 - Подключение:")
+ + f"\n{app['connectAndUseStep']['description'][db_user.language]}"
)
if 'additionalAfterAddSubscriptionStep' in app:
additional = app['additionalAfterAddSubscriptionStep']
guide_text += (
- "\n\n"
- + texts.t(
- "SUBSCRIPTION_ADDITIONAL_STEP_TITLE",
- "{title}:",
- ).format(title=additional['title'][db_user.language])
- + f"\n{additional['description'][db_user.language]}"
+ "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_ADDITIONAL_STEP_TITLE",
+ "{title}:",
+ ).format(title=additional['title'][db_user.language])
+ + f"\n{additional['description'][db_user.language]}"
)
-
+
await callback.message.edit_text(
guide_text,
reply_markup=get_specific_app_keyboard(
@@ -4999,21 +5017,22 @@ async def handle_specific_app_guide(
)
await callback.answer()
+
async def handle_no_traffic_packages(
- callback: types.CallbackQuery,
- db_user: User
+ callback: types.CallbackQuery,
+ db_user: User
):
await callback.answer(
"⚠️ В данный момент нет доступных пакетов трафика. "
- "Обратитесь в техподдержку для получения информации.",
+ "Обратитесь в техподдержку для получения информации.",
show_alert=True
)
async def handle_open_subscription_link(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
texts = get_texts(db_user.language)
subscription = db_user.subscription
@@ -5030,20 +5049,20 @@ async def handle_open_subscription_link(
redirect_link = get_happ_cryptolink_redirect_link(subscription_link)
happ_scheme_link = convert_subscription_link_to_happ_scheme(subscription_link)
happ_message = (
- texts.t(
- "SUBSCRIPTION_HAPP_OPEN_TITLE",
- "🔗 Подключение через Happ",
- )
- + "\n\n"
- + texts.t(
- "SUBSCRIPTION_HAPP_OPEN_LINK",
- "🔓 Открыть ссылку в Happ",
- ).format(subscription_link=happ_scheme_link)
- + "\n\n"
- + texts.t(
- "SUBSCRIPTION_HAPP_OPEN_HINT",
- "💡 Если ссылка не открывается автоматически, скопируйте её вручную:",
- )
+ texts.t(
+ "SUBSCRIPTION_HAPP_OPEN_TITLE",
+ "🔗 Подключение через Happ",
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_HAPP_OPEN_LINK",
+ "🔓 Открыть ссылку в Happ",
+ ).format(subscription_link=happ_scheme_link)
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_HAPP_OPEN_HINT",
+ "💡 Если ссылка не открывается автоматически, скопируйте её вручную:",
+ )
)
if redirect_link:
@@ -5073,43 +5092,44 @@ async def handle_open_subscription_link(
return
link_text = (
- texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
- + "\n\n"
- + f"{subscription_link}\n\n"
- + texts.t("SUBSCRIPTION_LINK_USAGE_TITLE", "📱 Как использовать:")
- + "\n"
- + "\n".join(
- [
- texts.t(
- "SUBSCRIPTION_LINK_STEP1",
- "1. Нажмите на ссылку выше чтобы её скопировать",
- ),
- texts.t(
- "SUBSCRIPTION_LINK_STEP2",
- "2. Откройте ваше VPN приложение",
- ),
- texts.t(
- "SUBSCRIPTION_LINK_STEP3",
- "3. Найдите функцию \"Добавить подписку\" или \"Import\"",
- ),
- texts.t(
- "SUBSCRIPTION_LINK_STEP4",
- "4. Вставьте скопированную ссылку",
- ),
- ]
- )
- + "\n\n"
- + texts.t(
- "SUBSCRIPTION_LINK_HINT",
- "💡 Если ссылка не скопировалась, выделите её вручную и скопируйте.",
- )
+ texts.t("SUBSCRIPTION_DEVICE_LINK_TITLE", "🔗 Ссылка подписки:")
+ + "\n\n"
+ + f"{subscription_link}\n\n"
+ + texts.t("SUBSCRIPTION_LINK_USAGE_TITLE", "📱 Как использовать:")
+ + "\n"
+ + "\n".join(
+ [
+ texts.t(
+ "SUBSCRIPTION_LINK_STEP1",
+ "1. Нажмите на ссылку выше чтобы её скопировать",
+ ),
+ texts.t(
+ "SUBSCRIPTION_LINK_STEP2",
+ "2. Откройте ваше VPN приложение",
+ ),
+ texts.t(
+ "SUBSCRIPTION_LINK_STEP3",
+ "3. Найдите функцию \"Добавить подписку\" или \"Import\"",
+ ),
+ texts.t(
+ "SUBSCRIPTION_LINK_STEP4",
+ "4. Вставьте скопированную ссылку",
+ ),
+ ]
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_LINK_HINT",
+ "💡 Если ссылка не скопировалась, выделите её вручную и скопируйте.",
+ )
)
await callback.message.edit_text(
link_text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[
- InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), callback_data="subscription_connect")
+ InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
+ callback_data="subscription_connect")
],
[
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
@@ -5124,7 +5144,7 @@ def load_app_config() -> Dict[str, Any]:
try:
from app.config import settings
config_path = settings.get_app_config_path()
-
+
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
@@ -5133,16 +5153,16 @@ def load_app_config() -> Dict[str, Any]:
def get_apps_for_device(device_type: str, language: str = "ru") -> List[Dict[str, Any]]:
- config = load_app_config()
-
+ config = load_app_config()['platforms']
+
device_mapping = {
'ios': 'ios',
- 'android': 'android',
- 'windows': 'pc',
- 'mac': 'pc',
- 'tv': 'tv'
+ 'android': 'android',
+ 'windows': 'windows',
+ 'mac': 'macos',
+ 'tv': 'androidTV'
}
-
+
config_key = device_mapping.get(device_type, device_type)
return config.get(config_key, [])
@@ -5164,13 +5184,11 @@ def get_device_name(device_type: str, language: str = "ru") -> str:
'mac': 'macOS',
'tv': 'Android TV'
}
-
+
return names.get(device_type, device_type)
def create_deep_link(app: Dict[str, Any], subscription_url: str) -> str:
- from app.config import settings
-
return subscription_url
@@ -5179,7 +5197,7 @@ def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMa
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
- text="✅ Да, сбросить все устройства",
+ text="✅ Да, сбросить все устройства",
callback_data="confirm_reset_devices"
)
],
@@ -5188,19 +5206,21 @@ def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMa
]
])
-async def send_trial_notification(callback: types.CallbackQuery, db: AsyncSession, db_user: User, subscription: Subscription):
+
+async def send_trial_notification(callback: types.CallbackQuery, db: AsyncSession, db_user: User,
+ subscription: Subscription):
try:
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_trial_activation_notification(db, db_user, subscription)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о триале: {e}")
+
async def show_device_connection_help(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
subscription = db_user.subscription
subscription_link = get_display_subscription_link(subscription)
@@ -5230,7 +5250,7 @@ async def show_device_connection_help(
💡 Совет: Сохраните эту ссылку - она понадобится для подключения новых устройств
"""
-
+
await callback.message.edit_text(
help_text,
reply_markup=get_device_management_help_keyboard(db_user.language),
@@ -5238,18 +5258,19 @@ async def show_device_connection_help(
)
await callback.answer()
+
async def send_purchase_notification(
- callback: types.CallbackQuery,
- db: AsyncSession,
- db_user: User,
- subscription: Subscription,
- transaction_id: int,
- period_days: int,
- was_trial_conversion: bool = False
+ callback: types.CallbackQuery,
+ db: AsyncSession,
+ db_user: User,
+ subscription: Subscription,
+ transaction_id: int,
+ period_days: int,
+ was_trial_conversion: bool = False
):
try:
from app.database.crud.transaction import get_transaction_by_id
-
+
transaction = await get_transaction_by_id(db, transaction_id)
if transaction:
notification_service = AdminNotificationService(callback.bot)
@@ -5259,18 +5280,19 @@ async def send_purchase_notification(
except Exception as e:
logger.error(f"Ошибка отправки уведомления о покупке: {e}")
+
async def send_extension_notification(
- callback: types.CallbackQuery,
- db: AsyncSession,
- db_user: User,
- subscription: Subscription,
- transaction_id: int,
- extended_days: int,
- old_end_date: datetime
+ callback: types.CallbackQuery,
+ db: AsyncSession,
+ db_user: User,
+ subscription: Subscription,
+ transaction_id: int,
+ extended_days: int,
+ old_end_date: datetime
):
try:
from app.database.crud.transaction import get_transaction_by_id
-
+
transaction = await get_transaction_by_id(db, transaction_id)
if transaction:
notification_service = AdminNotificationService(callback.bot)
@@ -5280,24 +5302,25 @@ async def send_extension_notification(
except Exception as e:
logger.error(f"Ошибка отправки уведомления о продлении: {e}")
+
async def handle_switch_traffic(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
from app.config import settings
-
+
if settings.is_traffic_fixed():
await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True)
return
-
+
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
if not subscription or subscription.is_trial:
await callback.answer("⚠️ Эта функция доступна только для платных подписок", show_alert=True)
return
-
+
current_traffic = subscription.traffic_limit_gb
period_hint_days = _get_period_hint_from_subscription(subscription)
traffic_discount_percent = _get_addon_discount_percent_for_user(
@@ -5321,26 +5344,25 @@ async def handle_switch_traffic(
),
parse_mode="HTML"
)
-
+
await callback.answer()
async def confirm_switch_traffic(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
new_traffic_gb = int(callback.data.split('_')[2])
texts = get_texts(db_user.language)
subscription = db_user.subscription
-
+
current_traffic = subscription.traffic_limit_gb
-
+
if new_traffic_gb == current_traffic:
await callback.answer("ℹ️ Лимит трафика не изменился", show_alert=True)
return
-
+
old_price_per_month = settings.get_traffic_price(current_traffic)
new_price_per_month = settings.get_traffic_price(new_traffic_gb)
@@ -5362,9 +5384,9 @@ async def confirm_switch_traffic(
)
price_difference_per_month = discounted_new_per_month - discounted_old_per_month
discount_savings_per_month = (
- (new_price_per_month - old_price_per_month) - price_difference_per_month
+ (new_price_per_month - old_price_per_month) - price_difference_per_month
)
-
+
if price_difference_per_month > 0:
total_price_difference = price_difference_per_month * months_remaining
@@ -5395,7 +5417,7 @@ async def confirm_switch_traffic(
)
await callback.answer()
return
-
+
action_text = f"увеличить до {texts.format_traffic(new_traffic_gb)}"
cost_text = f"Доплата: {texts.format_price(total_price_difference)} (за {months_remaining} мес)"
if discount_savings_per_month > 0:
@@ -5408,61 +5430,61 @@ async def confirm_switch_traffic(
total_price_difference = 0
action_text = f"уменьшить до {texts.format_traffic(new_traffic_gb)}"
cost_text = "Возврат средств не производится"
-
+
confirm_text = f"🔄 Подтверждение переключения трафика\n\n"
confirm_text += f"Текущий лимит: {texts.format_traffic(current_traffic)}\n"
confirm_text += f"Новый лимит: {texts.format_traffic(new_traffic_gb)}\n\n"
confirm_text += f"Действие: {action_text}\n"
confirm_text += f"💰 {cost_text}\n\n"
confirm_text += "Подтвердить переключение?"
-
+
await callback.message.edit_text(
confirm_text,
reply_markup=get_confirm_switch_traffic_keyboard(new_traffic_gb, total_price_difference, db_user.language),
parse_mode="HTML"
)
-
+
await callback.answer()
+
async def clear_saved_cart(
- callback: types.CallbackQuery,
- state: FSMContext,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ state: FSMContext,
+ db_user: User,
+ db: AsyncSession
):
await state.clear()
-
+
from app.handlers.menu import show_main_menu
await show_main_menu(callback, db_user, db)
-
+
await callback.answer("🗑️ Корзина очищена")
async def execute_switch_traffic(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
):
-
callback_parts = callback.data.split('_')
new_traffic_gb = int(callback_parts[3])
price_difference = int(callback_parts[4])
-
+
texts = get_texts(db_user.language)
subscription = db_user.subscription
current_traffic = subscription.traffic_limit_gb
-
+
try:
if price_difference > 0:
success = await subtract_user_balance(
db, db_user, price_difference,
f"Переключение трафика с {current_traffic}GB на {new_traffic_gb}GB"
)
-
+
if not success:
await callback.answer("⚠️ Ошибка списания средств", show_alert=True)
return
-
+
months_remaining = get_remaining_months(subscription.end_date)
await create_transaction(
db=db,
@@ -5471,18 +5493,18 @@ async def execute_switch_traffic(
amount_kopeks=price_difference,
description=f"Переключение трафика с {current_traffic}GB на {new_traffic_gb}GB на {months_remaining} мес"
)
-
+
subscription.traffic_limit_gb = new_traffic_gb
subscription.updated_at = datetime.utcnow()
-
+
await db.commit()
-
+
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, subscription)
-
+
await db.refresh(db_user)
await db.refresh(subscription)
-
+
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(callback.bot)
@@ -5491,7 +5513,7 @@ async def execute_switch_traffic(
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления об изменении трафика: {e}")
-
+
if new_traffic_gb > current_traffic:
success_text = f"✅ Лимит трафика увеличен!\n\n"
success_text += f"📊 Было: {texts.format_traffic(current_traffic)} → "
@@ -5503,50 +5525,51 @@ async def execute_switch_traffic(
success_text += f"📊 Было: {texts.format_traffic(current_traffic)} → "
success_text += f"Стало: {texts.format_traffic(new_traffic_gb)}\n"
success_text += f"ℹ️ Возврат средств не производится"
-
+
await callback.message.edit_text(
success_text,
reply_markup=get_back_keyboard(db_user.language)
)
-
- logger.info(f"✅ Пользователь {db_user.telegram_id} переключил трафик с {current_traffic}GB на {new_traffic_gb}GB, доплата: {price_difference/100}₽")
-
+
+ logger.info(
+ f"✅ Пользователь {db_user.telegram_id} переключил трафик с {current_traffic}GB на {new_traffic_gb}GB, доплата: {price_difference / 100}₽")
+
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()
def get_traffic_switch_keyboard(
- current_traffic_gb: int,
- language: str = "ru",
- subscription_end_date: datetime = None,
- discount_percent: int = 0,
+ current_traffic_gb: int,
+ language: str = "ru",
+ subscription_end_date: datetime = None,
+ discount_percent: int = 0,
) -> InlineKeyboardMarkup:
from app.config import settings
-
+
months_multiplier = 1
period_text = ""
if subscription_end_date:
months_multiplier = get_remaining_months(subscription_end_date)
if months_multiplier > 1:
period_text = f" (за {months_multiplier} мес)"
-
+
packages = settings.get_traffic_packages()
enabled_packages = [pkg for pkg in packages if pkg['enabled']]
-
+
current_price_per_month = settings.get_traffic_price(current_traffic_gb)
discounted_current_per_month, _ = apply_percentage_discount(
current_price_per_month,
discount_percent,
)
-
+
buttons = []
-
+
for package in enabled_packages:
gb = package['gb']
price_per_month = package['price']
@@ -5565,14 +5588,14 @@ def get_traffic_switch_keyboard(
elif total_price_diff > 0:
emoji = "⬆️"
action_text = ""
- price_text = f" (+{total_price_diff//100}₽{period_text})"
+ price_text = f" (+{total_price_diff // 100}₽{period_text})"
if discount_percent > 0:
discount_total = (
- (price_per_month - current_price_per_month) * months_multiplier
- - total_price_diff
+ (price_per_month - current_price_per_month) * months_multiplier
+ - total_price_diff
)
if discount_total > 0:
- price_text += f" (скидка {discount_percent}%: -{discount_total//100}₽)"
+ price_text += f" (скидка {discount_percent}%: -{discount_total // 100}₽)"
elif total_price_diff < 0:
emoji = "⬇️"
action_text = ""
@@ -5581,34 +5604,33 @@ def get_traffic_switch_keyboard(
emoji = "🔄"
action_text = ""
price_text = " (бесплатно)"
-
+
if gb == 0:
traffic_text = "Безлимит"
else:
traffic_text = f"{gb} ГБ"
-
+
button_text = f"{emoji} {traffic_text}{action_text}{price_text}"
-
+
buttons.append([
InlineKeyboardButton(text=button_text, callback_data=f"switch_traffic_{gb}")
])
-
+
buttons.append([
InlineKeyboardButton(
text="⬅️ Назад" if language == "ru" else "⬅️ Back",
callback_data="subscription_settings"
)
])
-
+
return InlineKeyboardMarkup(inline_keyboard=buttons)
def get_confirm_switch_traffic_keyboard(
- new_traffic_gb: int,
- price_difference: int,
- language: str = "ru"
+ new_traffic_gb: int,
+ price_difference: int,
+ language: str = "ru"
) -> InlineKeyboardMarkup:
-
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
@@ -5627,32 +5649,32 @@ def get_confirm_switch_traffic_keyboard(
def register_handlers(dp: Dispatcher):
update_traffic_prices()
-
+
dp.callback_query.register(
show_subscription_info,
F.data == "menu_subscription"
)
-
+
dp.callback_query.register(
show_trial_offer,
F.data == "menu_trial"
)
-
+
dp.callback_query.register(
activate_trial,
F.data == "trial_activate"
)
-
+
dp.callback_query.register(
start_subscription_purchase,
F.data.in_(["menu_buy", "subscription_upgrade"])
)
-
+
dp.callback_query.register(
handle_add_countries,
F.data == "subscription_add_countries"
)
-
+
dp.callback_query.register(
handle_switch_traffic,
F.data == "subscription_switch_traffic"
@@ -5662,12 +5684,12 @@ def register_handlers(dp: Dispatcher):
confirm_switch_traffic,
F.data.startswith("switch_traffic_")
)
-
+
dp.callback_query.register(
execute_switch_traffic,
F.data.startswith("confirm_switch_traffic_")
)
-
+
dp.callback_query.register(
handle_change_devices,
F.data == "subscription_change_devices"
@@ -5677,33 +5699,32 @@ def register_handlers(dp: Dispatcher):
confirm_change_devices,
F.data.startswith("change_devices_")
)
-
+
dp.callback_query.register(
execute_change_devices,
F.data.startswith("confirm_change_devices_")
)
-
+
dp.callback_query.register(
handle_extend_subscription,
F.data == "subscription_extend"
)
-
+
dp.callback_query.register(
handle_reset_traffic,
F.data == "subscription_reset_traffic"
)
-
-
+
dp.callback_query.register(
confirm_add_devices,
F.data.startswith("add_devices_")
)
-
+
dp.callback_query.register(
confirm_extend_subscription,
F.data.startswith("extend_period_")
)
-
+
dp.callback_query.register(
confirm_reset_traffic,
F.data == "confirm_reset_traffic"
@@ -5713,36 +5734,36 @@ def register_handlers(dp: Dispatcher):
handle_reset_devices,
F.data == "subscription_reset_devices"
)
-
+
dp.callback_query.register(
confirm_reset_devices,
F.data == "confirm_reset_devices"
)
-
+
dp.callback_query.register(
select_period,
F.data.startswith("period_"),
SubscriptionStates.selecting_period
)
-
+
dp.callback_query.register(
select_traffic,
F.data.startswith("traffic_"),
SubscriptionStates.selecting_traffic
)
-
+
dp.callback_query.register(
select_devices,
F.data.startswith("devices_") & ~F.data.in_(["devices_continue"]),
SubscriptionStates.selecting_devices
)
-
+
dp.callback_query.register(
devices_continue,
F.data == "devices_continue",
SubscriptionStates.selecting_devices
)
-
+
dp.callback_query.register(
confirm_purchase,
F.data == "subscription_confirm",
@@ -5763,17 +5784,17 @@ def register_handlers(dp: Dispatcher):
clear_saved_cart,
F.data == "clear_saved_cart",
)
-
+
dp.callback_query.register(
handle_autopay_menu,
F.data == "subscription_autopay"
)
-
+
dp.callback_query.register(
toggle_autopay,
F.data.in_(["autopay_enable", "autopay_disable"])
)
-
+
dp.callback_query.register(
show_autopay_days,
F.data == "autopay_set_days"
@@ -5783,12 +5804,12 @@ def register_handlers(dp: Dispatcher):
handle_subscription_config_back,
F.data == "subscription_config_back"
)
-
+
dp.callback_query.register(
handle_subscription_cancel,
F.data == "subscription_cancel"
)
-
+
dp.callback_query.register(
set_autopay_days,
F.data.startswith("autopay_days_")
@@ -5799,7 +5820,7 @@ def register_handlers(dp: Dispatcher):
F.data.startswith("country_"),
SubscriptionStates.selecting_countries
)
-
+
dp.callback_query.register(
countries_continue,
F.data == "countries_continue",
@@ -5810,7 +5831,7 @@ def register_handlers(dp: Dispatcher):
handle_manage_country,
F.data.startswith("country_manage_")
)
-
+
dp.callback_query.register(
apply_countries_changes,
F.data == "countries_apply"
@@ -5851,22 +5872,22 @@ def register_handlers(dp: Dispatcher):
handle_connect_subscription,
F.data == "subscription_connect"
)
-
+
dp.callback_query.register(
handle_device_guide,
F.data.startswith("device_guide_")
)
-
+
dp.callback_query.register(
handle_app_selection,
F.data.startswith("app_list_")
)
-
+
dp.callback_query.register(
handle_specific_app_guide,
F.data.startswith("app_")
)
-
+
dp.callback_query.register(
handle_open_subscription_link,
F.data == "open_subscription_link"
@@ -5886,17 +5907,17 @@ def register_handlers(dp: Dispatcher):
handle_device_management,
F.data == "subscription_manage_devices"
)
-
+
dp.callback_query.register(
handle_devices_page,
F.data.startswith("devices_page_")
)
-
+
dp.callback_query.register(
handle_single_device_reset,
- F.data.regexp(r"^reset_device_\d+_\d+$")
+ F.data.regexp(r"^reset_device_\d+_\d+$")
)
-
+
dp.callback_query.register(
handle_all_devices_reset_from_management,
F.data == "reset_all_devices"
@@ -5905,4 +5926,4 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(
show_device_connection_help,
F.data == "device_connection_help"
- )
+ )
\ No newline at end of file