From aa976181656fd0506505c8cc3b015d410c4606a1 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Sun, 3 Aug 2025 18:58:50 +0300 Subject: [PATCH] Refactor notification service and enhance user management features - Replaced legacy payment notification functions with a new NotificationService for improved code organization and maintainability. - Updated user management handlers to utilize the new notification system for payment confirmations. - Enhanced user card formatting with localized labels for better clarity and user experience. - Introduced context management for the PanelApiService to ensure proper session handling. - Updated localization files to include new labels and messages for user management and notifications. --- bot/handlers/admin/promo_codes.py | 3 +- bot/handlers/admin/statistics.py | 95 +++++++++++++-------------- bot/handlers/admin/user_management.py | 69 +++++++++---------- bot/handlers/user/payment.py | 12 +--- bot/handlers/user/start.py | 1 - bot/services/crypto_pay_service.py | 25 ++++--- bot/services/notification_service.py | 15 +---- bot/services/panel_api_service.py | 10 ++- bot/services/stars_service.py | 25 ++++--- bot/services/tribute_service.py | 25 ++++--- locales/en.json | 40 ++++++++++- locales/ru.json | 40 ++++++++++- 12 files changed, 215 insertions(+), 145 deletions(-) diff --git a/bot/handlers/admin/promo_codes.py b/bot/handlers/admin/promo_codes.py index b230ec8..a22cc7d 100644 --- a/bot/handlers/admin/promo_codes.py +++ b/bot/handlers/admin/promo_codes.py @@ -266,7 +266,8 @@ async def process_promo_set_validity(callback: types.CallbackQuery, async def process_promo_validity_days_handler(message: types.Message, state: FSMContext, i18n_data: dict, - settings: Settings): + settings: Settings, + session: AsyncSession): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") if not i18n: diff --git a/bot/handlers/admin/statistics.py b/bot/handlers/admin/statistics.py index d7c1af4..cfe3279 100644 --- a/bot/handlers/admin/statistics.py +++ b/bot/handlers/admin/statistics.py @@ -142,56 +142,53 @@ async def show_statistics_handler(callback: types.CallbackQuery, stats_text_parts.append(f"\n🖥 {_('admin_panel_stats_header', default='Статистика панели')}") try: - panel_service = PanelApiService(settings) - - # Get panel system statistics - logging.info("Fetching panel statistics...") - panel_stats = await panel_service.get_panel_statistics() - nodes_stats = await panel_service.get_nodes_statistics() - online_count = await panel_service.get_online_users_count() - users_activity = await panel_service.get_users_activity_stats() - - logging.info(f"Panel stats response: panel_stats={panel_stats}, nodes_stats={nodes_stats}, online_count={online_count}, users_activity={users_activity}") - - # Online users - if online_count is not None: - stats_text_parts.append(f"🟢 Онлайн сейчас: {online_count}") - else: - stats_text_parts.append(f"🟢 Онлайн сейчас: N/A") - - # Users activity - if users_activity: - today_connected = users_activity.get('today_connected', 'N/A') - week_connected = users_activity.get('week_connected', 'N/A') - never_connected = users_activity.get('never_connected', 'N/A') - stats_text_parts.append(f"📅 Подключались сегодня: {today_connected}") - stats_text_parts.append(f"📅 Подключались за неделю: {week_connected}") - stats_text_parts.append(f"❌ Никогда не подключались: {never_connected}") - else: - stats_text_parts.append(f"📅 Активность пользователей: N/A") - - # Nodes statistics - if nodes_stats: - active_nodes = len([node for node in nodes_stats if node.get('status') == 'active']) - total_nodes = len(nodes_stats) - stats_text_parts.append(f"🔗 Активных нод: {active_nodes}/{total_nodes}") - else: - stats_text_parts.append(f"🔗 Ноды: N/A") - - # System statistics - if panel_stats: - system_info = panel_stats.get('system', {}) - if system_info: - memory_usage = system_info.get('memory_usage_percent', 'N/A') - cpu_usage = system_info.get('cpu_usage_percent', 'N/A') - stats_text_parts.append(f"💾 Использование RAM: {memory_usage}%") - stats_text_parts.append(f"🔄 Загрузка CPU: {cpu_usage}%") + async with PanelApiService(settings) as panel_service: + # Get panel system statistics + logging.info("Fetching panel statistics...") + panel_stats = await panel_service.get_panel_statistics() + nodes_stats = await panel_service.get_nodes_statistics() + online_count = await panel_service.get_online_users_count() + users_activity = await panel_service.get_users_activity_stats() + + logging.info(f"Panel stats response: panel_stats={panel_stats}, nodes_stats={nodes_stats}, online_count={online_count}, users_activity={users_activity}") + + # Online users + if online_count is not None: + stats_text_parts.append(f"🟢 Онлайн сейчас: {online_count}") else: - stats_text_parts.append(f"💾 Системная информация: N/A") - else: - stats_text_parts.append(f"💾 Системная статистика: N/A") - - await panel_service.close() + stats_text_parts.append(f"🟢 Онлайн сейчас: N/A") + + # Users activity + if users_activity: + today_connected = users_activity.get('today_connected', 'N/A') + week_connected = users_activity.get('week_connected', 'N/A') + never_connected = users_activity.get('never_connected', 'N/A') + stats_text_parts.append(f"📅 Подключались сегодня: {today_connected}") + stats_text_parts.append(f"📅 Подключались за неделю: {week_connected}") + stats_text_parts.append(f"❌ Никогда не подключались: {never_connected}") + else: + stats_text_parts.append(f"📅 Активность пользователей: N/A") + + # Nodes statistics + if nodes_stats: + active_nodes = len([node for node in nodes_stats if node.get('status') == 'active']) + total_nodes = len(nodes_stats) + stats_text_parts.append(f"🔗 Активных нод: {active_nodes}/{total_nodes}") + else: + stats_text_parts.append(f"🔗 Ноды: N/A") + + # System statistics + if panel_stats: + system_info = panel_stats.get('system', {}) + if system_info: + memory_usage = system_info.get('memory_usage_percent', 'N/A') + cpu_usage = system_info.get('cpu_usage_percent', 'N/A') + stats_text_parts.append(f"💾 Использование RAM: {memory_usage}%") + stats_text_parts.append(f"🔄 Загрузка CPU: {cpu_usage}%") + else: + stats_text_parts.append(f"💾 Системная информация: N/A") + else: + stats_text_parts.append(f"💾 Системная статистика: N/A") except Exception as e: logging.error(f"Failed to fetch panel statistics: {e}", exc_info=True) diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py index 65daa9d..747c8e7 100644 --- a/bot/handlers/admin/user_management.py +++ b/bot/handlers/admin/user_management.py @@ -113,27 +113,28 @@ async def format_user_card(user: User, session: AsyncSession, card_parts.append(f"👤 {_('admin_user_card_title', default='Карточка пользователя')}\n") # User details - user_name = user.first_name or "N/A" - username_display = f"@{user.username}" if user.username else "N/A" - registration_date = user.registration_date.strftime('%Y-%m-%d %H:%M') if user.registration_date else "N/A" + na_value = _("admin_user_na_value", default="N/A") + user_name = user.first_name or na_value + username_display = f"@{user.username}" if user.username else na_value + registration_date = user.registration_date.strftime('%Y-%m-%d %H:%M') if user.registration_date else na_value - card_parts.append(f"🆔 ID: {hcode(str(user.user_id))}") - card_parts.append(f"👤 Имя: {hcode(user_name)}") - card_parts.append(f"📱 Username: {hcode(username_display)}") - card_parts.append(f"🌍 Язык: {hcode(user.language_code or 'N/A')}") - card_parts.append(f"📅 Регистрация: {hcode(registration_date)}") + card_parts.append(f"{_('admin_user_id_label', default='🆔 ID:')} {hcode(str(user.user_id))}") + card_parts.append(f"{_('admin_user_name_label', default='👤 Имя:')} {hcode(user_name)}") + card_parts.append(f"{_('admin_user_username_label', default='📱 Username:')} {hcode(username_display)}") + card_parts.append(f"{_('admin_user_language_label', default='🌍 Язык:')} {hcode(user.language_code or na_value)}") + card_parts.append(f"{_('admin_user_registration_label', default='📅 Регистрация:')} {hcode(registration_date)}") # Ban status - ban_status = "🚫 Заблокирован" if user.is_banned else "✅ Активен" - card_parts.append(f"🛡 Статус: {ban_status}") + ban_status = _("admin_user_status_banned", default="🚫 Заблокирован") if user.is_banned else _("admin_user_status_active", default="✅ Активен") + card_parts.append(f"{_('admin_user_status_label', default='🛡 Статус:')} {ban_status}") # Referral info if user.referred_by_id: - card_parts.append(f"🎁 Привлечен по реферальной программе от: {hcode(str(user.referred_by_id))}") + card_parts.append(f"{_('admin_user_referral_label', default='🎁 Привлечен по реферальной программе от:')} {hcode(str(user.referred_by_id))}") # Panel info if user.panel_user_uuid: - card_parts.append(f"🔗 Panel UUID: {hcode(user.panel_user_uuid[:8] + '...' if len(user.panel_user_uuid) > 8 else user.panel_user_uuid)}") + card_parts.append(f"{_('admin_user_panel_uuid_label', default='🔗 Panel UUID:')} {hcode(user.panel_user_uuid[:8] + '...' if len(user.panel_user_uuid) > 8 else user.panel_user_uuid)}") card_parts.append("") # Empty line @@ -146,33 +147,33 @@ async def format_user_card(user: User, session: AsyncSession, end_date = subscription_details.get('end_date') if end_date: end_date_str = end_date.strftime('%Y-%m-%d %H:%M') if isinstance(end_date, datetime) else str(end_date) - card_parts.append(f"⏰ Действует до: {hcode(end_date_str)}") + card_parts.append(f"{_('admin_user_subscription_active_until', default='⏰ Действует до:')} {hcode(end_date_str)}") status = subscription_details.get('status_from_panel', 'UNKNOWN') - card_parts.append(f"📊 Статус на панели: {hcode(status)}") + card_parts.append(f"{_('admin_user_panel_status_label', default='📊 Статус на панели:')} {hcode(status)}") traffic_limit = subscription_details.get('traffic_limit_bytes') traffic_used = subscription_details.get('traffic_used_bytes') if traffic_limit and traffic_used is not None: traffic_limit_gb = traffic_limit / (1024**3) traffic_used_gb = traffic_used / (1024**3) - card_parts.append(f"📊 Трафик: {hcode(f'{traffic_used_gb:.2f}GB / {traffic_limit_gb:.2f}GB')}") + card_parts.append(f"{_('admin_user_traffic_label', default='📊 Трафик:')} {hcode(f'{traffic_used_gb:.2f}GB / {traffic_limit_gb:.2f}GB')}") else: - card_parts.append(f"💳 Подписка: {hcode('Отсутствует')}") + card_parts.append(f"{_('admin_user_subscription_label', default='💼 Подписка:')} {hcode(_('admin_user_subscription_none', default='Нет активной подписки'))}") except Exception as e: logging.error(f"Error getting subscription details for user {user.user_id}: {e}") - card_parts.append(f"💳 Подписка: {hcode('Ошибка загрузки')}") + card_parts.append(f"{_('admin_user_subscription_label', default='💼 Подписка:')} {hcode(_('admin_user_subscription_error', default='Ошибка загрузки'))}") # Statistics try: # Count user logs logs_count = await message_log_dal.count_user_message_logs(session, user.user_id) - card_parts.append(f"📜 Всего действий: {hcode(str(logs_count))}") + card_parts.append(f"{_('admin_user_actions_count_label', default='📜 Всего действий:')} {hcode(str(logs_count))}") # Check if user had any subscriptions had_subscriptions = await subscription_service.has_had_any_subscription(session, user.user_id) - trial_status = "Использовал" if had_subscriptions else "Не использовал" - card_parts.append(f"🆓 Триал: {hcode(trial_status)}") + trial_status = _("admin_user_trial_used", default="Использовал") if had_subscriptions else _("admin_user_trial_not_used", default="Не использовал") + card_parts.append(f"{_('admin_user_trial_label', default='🏡 Триал:')} {hcode(trial_status)}") except Exception as e: logging.error(f"Error getting user statistics for {user.user_id}: {e}") @@ -353,7 +354,7 @@ async def handle_toggle_ban(callback: types.CallbackQuery, user: User, await session.commit() - status_text = "заблокирован" if new_ban_status else "разблокирован" + status_text = _("admin_user_ban_action_banned", default="заблокирован") if new_ban_status else _("admin_user_ban_action_unbanned", default="разблокирован") await callback.answer(_( "admin_user_ban_toggle_success", default="✅ Пользователь {status}", @@ -365,9 +366,9 @@ async def handle_toggle_ban(callback: types.CallbackQuery, user: User, from config.settings import Settings from bot.services.panel_api_service import PanelApiService settings = Settings() - panel_service = PanelApiService(settings) - subscription_service = SubscriptionService(settings, panel_service) - await handle_refresh_user_card(callback, user, subscription_service, session, i18n_instance, lang) + async with PanelApiService(settings) as panel_service: + subscription_service = SubscriptionService(settings, panel_service) + await handle_refresh_user_card(callback, user, subscription_service, session, i18n_instance, lang) except Exception as e: logging.error(f"Error toggling ban for user {user.user_id}: {e}") @@ -631,16 +632,16 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte # Show user card again from bot.services.panel_api_service import PanelApiService - panel_service = PanelApiService(settings) - subscription_service = SubscriptionService(settings, panel_service) - user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang) - keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang) - - await message.answer( - user_card_text, - reply_markup=keyboard.as_markup(), - parse_mode="HTML" - ) + async with PanelApiService(settings) as panel_service: + subscription_service = SubscriptionService(settings, panel_service) + user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang) + keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang) + + await message.answer( + user_card_text, + reply_markup=keyboard.as_markup(), + parse_mode="HTML" + ) except Exception as e: logging.error(f"Error sending direct message to user {target_user_id}: {e}") diff --git a/bot/handlers/user/payment.py b/bot/handlers/user/payment.py index bc54a9c..a8c2463 100644 --- a/bot/handlers/user/payment.py +++ b/bot/handlers/user/payment.py @@ -20,7 +20,7 @@ from bot.services.panel_api_service import PanelApiService from bot.services.yookassa_service import YooKassaService from bot.middlewares.i18n import JsonI18n from config.settings import Settings -from bot.services.notification_service import notify_admin_new_payment, NotificationService +from bot.services.notification_service import NotificationService from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard payment_processing_lock = asyncio.Lock() @@ -209,16 +209,6 @@ async def process_successful_payment(session: AsyncSession, bot: Bot, ) except Exception as e: logging.error(f"Failed to send payment notification: {e}") - - # Legacy notification for backwards compatibility - await notify_admin_new_payment( - bot, - settings, - i18n, - user_id, - subscription_months, - payment_value, - ) except Exception as e_process: logging.error( diff --git a/bot/handlers/user/start.py b/bot/handlers/user/start.py index 996e7de..ab9c19e 100644 --- a/bot/handlers/user/start.py +++ b/bot/handlers/user/start.py @@ -237,7 +237,6 @@ async def start_command_handler(message: types.Message, config_link = active.get("config_link") if active else None config_link = config_link or _("config_link_not_available") - from datetime import datetime new_end_date = result if isinstance(result, datetime) else None promo_success_text = _( diff --git a/bot/services/crypto_pay_service.py b/bot/services/crypto_pay_service.py index b2a40a9..8c67195 100644 --- a/bot/services/crypto_pay_service.py +++ b/bot/services/crypto_pay_service.py @@ -14,7 +14,7 @@ from bot.middlewares.i18n import JsonI18n from bot.services.subscription_service import SubscriptionService from bot.services.referral_service import ReferralService from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard -from bot.services.notification_service import notify_admin_new_payment +from bot.services.notification_service import NotificationService from db.dal import payment_dal, user_dal @@ -194,15 +194,20 @@ class CryptoPayService: except Exception as e: logging.error(f"Failed to send CryptoPay success message: {e}") - await notify_admin_new_payment( - bot, - settings, - i18n, - user_id, - months, - float(invoice.amount), - currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL, - ) + # Send notification about payment + try: + notification_service = NotificationService(bot, settings, i18n) + user = await user_dal.get_user_by_id(session, user_id) + await notification_service.notify_payment_received( + user_id=user_id, + amount=float(invoice.amount), + currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL, + months=months, + payment_provider="crypto_pay", + username=user.username if user else None + ) + except Exception as e: + logging.error(f"Failed to send crypto_pay payment notification: {e}") async def webhook_route(self, request: web.Request) -> web.Response: if not self.configured or not self.client: diff --git a/bot/services/notification_service.py b/bot/services/notification_service.py index a8166dc..ee9e963 100644 --- a/bot/services/notification_service.py +++ b/bot/services/notification_service.py @@ -220,20 +220,7 @@ async def notify_admin_new_trial(bot: Bot, settings: Settings, i18n: JsonI18n, await notification_service.notify_trial_activation(user_id, end_date) -async def notify_admin_new_payment(bot: Bot, settings: Settings, i18n: JsonI18n, - user_id: int, months: int, amount: float, - currency: str | None = None) -> None: - currency_symbol = currency or settings.DEFAULT_CURRENCY_SYMBOL - await notify_admins( - bot, - settings, - i18n, - "admin_new_payment_notification", - user_id=user_id, - months=months, - amount=f"{amount:.2f}", - currency=currency_symbol, - ) + async def notify_admin_promo_activation(bot: Bot, settings: Settings, diff --git a/bot/services/panel_api_service.py b/bot/services/panel_api_service.py index a09f499..582ad55 100644 --- a/bot/services/panel_api_service.py +++ b/bot/services/panel_api_service.py @@ -21,6 +21,14 @@ class PanelApiService: self.api_key = settings.PANEL_API_KEY self._session: Optional[aiohttp.ClientSession] = None self.default_client_ip = "127.0.0.1" + + async def __aenter__(self): + """Context manager entry""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - automatically close session""" + await self.close_session() async def _get_session(self) -> aiohttp.ClientSession: if self._session is None or self._session.closed: @@ -32,7 +40,7 @@ class PanelApiService: if self._session and not self._session.closed: await self._session.close() self._session = None - logging.info("Panel API service HTTP session closed.") + logging.debug("Panel API service HTTP session closed.") async def close(self): """Alias for close_session for API consistency.""" diff --git a/bot/services/stars_service.py b/bot/services/stars_service.py index 524547b..548babe 100644 --- a/bot/services/stars_service.py +++ b/bot/services/stars_service.py @@ -10,7 +10,7 @@ from db.dal import payment_dal, user_dal from .subscription_service import SubscriptionService from .referral_service import ReferralService from bot.middlewares.i18n import JsonI18n -from .notification_service import notify_admin_new_payment +from .notification_service import NotificationService from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard @@ -153,13 +153,18 @@ class StarsService: logging.error( f"Failed to send stars payment success message: {e_send}") - await notify_admin_new_payment( - self.bot, - self.settings, - self.i18n, - message.from_user.id, - months, - float(stars_amount), - currency="XTR", - ) + # Send notification about payment + try: + notification_service = NotificationService(self.bot, self.settings, self.i18n) + user = await user_dal.get_user_by_id(session, message.from_user.id) + await notification_service.notify_payment_received( + user_id=message.from_user.id, + amount=float(stars_amount), + currency="XTR", + months=months, + payment_provider="stars", + username=user.username if user else None + ) + except Exception as e: + logging.error(f"Failed to send stars payment notification: {e}") diff --git a/bot/services/tribute_service.py b/bot/services/tribute_service.py index c9d6882..f310f2d 100644 --- a/bot/services/tribute_service.py +++ b/bot/services/tribute_service.py @@ -13,7 +13,7 @@ from bot.middlewares.i18n import JsonI18n from bot.services.subscription_service import SubscriptionService from bot.services.panel_api_service import PanelApiService from bot.services.referral_service import ReferralService -from .notification_service import notify_admin_new_payment +from .notification_service import NotificationService from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard from db.dal import payment_dal, user_dal, subscription_dal @@ -187,15 +187,20 @@ class TributeService: logging.error( f"Failed to send Tribute payment success message to user {user_id}: {e}") - await notify_admin_new_payment( - bot, - settings, - i18n, - user_id, - months, - float(price_rub), - currency="RUB", - ) + # Send notification about payment + try: + notification_service = NotificationService(bot, settings, i18n) + user = await user_dal.get_user_by_id(session, user_id) + await notification_service.notify_payment_received( + user_id=user_id, + amount=float(price_rub), + currency="RUB", + months=months, + payment_provider="tribute", + username=user.username if user else None + ) + except Exception as e: + logging.error(f"Failed to send tribute payment notification: {e}") else: await session.commit() return web.Response(status=200, text="ok") diff --git a/locales/en.json b/locales/en.json index 0705c2d..612a39c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -234,7 +234,7 @@ "subscription_expired_yesterday_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expired yesterday ({end_date}).\n\nPlease renew it using the button below.", "admin_new_trial_notification": "\ud83c\udf21 User {user_id} activated a free trial until {end_date}.", - "admin_new_payment_notification": "\ud83d\udcb3 Payment received from user {user_id}: {months} mo. for {amount} {currency}.", + "admin_promo_activation_notification": "\ud83c\udf81 Promo code {code} activated by user {user_id} (+{bonus_days}d).", "error_unknown": "An unknown error occurred.", @@ -362,5 +362,41 @@ "admin_promo_creation_failed": "❌ Error creating promo code: {error}", "admin_panel_back_button": "⬅️ Back", - "admin_bulk_promo_creation_failed": "❌ Error creating promo codes: {error}" + "admin_bulk_promo_creation_failed": "❌ Error creating promo codes: {error}", + + "admin_user_id_label": "🆔 ID:", + "admin_user_name_label": "👤 Name:", + "admin_user_username_label": "📱 Username:", + "admin_user_language_label": "🌍 Language:", + "admin_user_registration_label": "📅 Registration:", + "admin_user_status_label": "🛡 Status:", + "admin_user_referral_label": "🎁 Referred by:", + "admin_user_panel_uuid_label": "🔗 Panel UUID:", + "admin_user_panel_status_label": "📊 Panel Status:", + "admin_user_traffic_label": "📊 Traffic:", + "admin_user_subscription_label": "💼 Subscription:", + "admin_user_trial_label": "🏡 Trial:", + + "admin_user_status_banned": "🚫 Banned", + "admin_user_status_active": "✅ Active", + "admin_user_trial_used": "Used", + "admin_user_trial_not_used": "Not used", + "admin_user_ban_action_banned": "banned", + "admin_user_ban_action_unbanned": "unbanned", + "admin_user_na_value": "N/A", + + "admin_user_subscription_active": "Active until {end_date}", + "admin_user_subscription_expired": "Expired {end_date}", + "admin_user_subscription_none": "No active subscription", + + "admin_user_logs_title": "📋 User logs {user_id}", + "admin_user_logs_empty": "No logs available", + "admin_user_logs_count": "Total entries: {count}", + "admin_user_logs_entry": "📅 {timestamp}\n📝 {event_type}: {content}", + + "admin_user_actions_count_label": "📜 Total actions:", + "admin_user_subscription_active_until": "⏰ Active until:", + "admin_user_subscription_error": "Loading error", + + "admin_bulk_promo_unique_generation_failed": "Failed to create unique promo code" } diff --git a/locales/ru.json b/locales/ru.json index 15211e8..70e4bc8 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -234,7 +234,7 @@ "subscription_expired_yesterday_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекла сутки назад ({end_date}).\n\nПродлите её по кнопке ниже.", "admin_new_trial_notification": "\ud83c\udf21 Пользователь {user_id} активировал пробный период до {end_date}.", - "admin_new_payment_notification": "\ud83d\udcb3 Получен платеж от пользователя {user_id}: {months} мес. за {amount} {currency}.", + "admin_promo_activation_notification": "\ud83c\udf81 Пользователь {user_id} активировал промокод {code} (+{bonus_days} дн.)", "error_unknown": "Произошла неизвестная ошибка.", @@ -362,5 +362,41 @@ "admin_promo_creation_failed": "❌ Ошибка создания промокода: {error}", "admin_panel_back_button": "⬅️ Назад", - "admin_bulk_promo_creation_failed": "❌ Ошибка создания промокодов: {error}" + "admin_bulk_promo_creation_failed": "❌ Ошибка создания промокодов: {error}", + + "admin_user_id_label": "🆔 ID:", + "admin_user_name_label": "👤 Имя:", + "admin_user_username_label": "📱 Username:", + "admin_user_language_label": "🌍 Язык:", + "admin_user_registration_label": "📅 Регистрация:", + "admin_user_status_label": "🛡 Статус:", + "admin_user_referral_label": "🎁 Привлечен по реферальной программе от:", + "admin_user_panel_uuid_label": "🔗 Panel UUID:", + "admin_user_panel_status_label": "📊 Статус на панели:", + "admin_user_traffic_label": "📊 Трафик:", + "admin_user_subscription_label": "💼 Подписка:", + "admin_user_trial_label": "🏡 Триал:", + + "admin_user_status_banned": "🚫 Заблокирован", + "admin_user_status_active": "✅ Активен", + "admin_user_trial_used": "Использовал", + "admin_user_trial_not_used": "Не использовал", + "admin_user_ban_action_banned": "заблокирован", + "admin_user_ban_action_unbanned": "разблокирован", + "admin_user_na_value": "N/A", + + "admin_user_subscription_active": "Активна до {end_date}", + "admin_user_subscription_expired": "Истекла {end_date}", + "admin_user_subscription_none": "Нет активной подписки", + + "admin_user_logs_title": "📋 Логи пользователя {user_id}", + "admin_user_logs_empty": "Логи отсутствуют", + "admin_user_logs_count": "Всего записей: {count}", + "admin_user_logs_entry": "📅 {timestamp}\n📝 {event_type}: {content}", + + "admin_user_actions_count_label": "📜 Всего действий:", + "admin_user_subscription_active_until": "⏰ Действует до:", + "admin_user_subscription_error": "Ошибка загрузки", + + "admin_bulk_promo_unique_generation_failed": "Не удалось создать уникальный промокод" }