diff --git a/app/services/admin_notification_service.py b/app/services/admin_notification_service.py new file mode 100644 index 00000000..43a8fa66 --- /dev/null +++ b/app/services/admin_notification_service.py @@ -0,0 +1,273 @@ +import logging +from typing import Optional, Dict, Any +from datetime import datetime +from aiogram import Bot +from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import settings +from app.database.models import User, Subscription, Transaction + +logger = logging.getLogger(__name__) + + +class AdminNotificationService: + + def __init__(self, bot: Bot): + self.bot = bot + self.chat_id = getattr(settings, 'ADMIN_NOTIFICATIONS_CHAT_ID', None) + self.topic_id = getattr(settings, 'ADMIN_NOTIFICATIONS_TOPIC_ID', None) + self.enabled = getattr(settings, 'ADMIN_NOTIFICATIONS_ENABLED', False) + + async def send_trial_activation_notification( + self, + user: User, + subscription: Subscription + ) -> bool: + if not self._is_enabled(): + return False + + try: + user_status = "🆕 Новый" if not user.has_had_paid_subscription else "🔄 Существующий" + + message = f"""🎯 АКТИВАЦИЯ ТРИАЛА + +👤 Пользователь: {user.full_name} +🆔 Telegram ID: {user.telegram_id} +📱 Username: @{user.username or 'отсутствует'} +👥 Статус: {user_status} + +⏰ Параметры триала: +📅 Период: {settings.TRIAL_DURATION_DAYS} дней +📊 Трафик: {settings.TRIAL_TRAFFIC_LIMIT_GB} ГБ +📱 Устройства: {settings.TRIAL_DEVICE_LIMIT} +🌐 Сервер: {subscription.connected_squads[0] if subscription.connected_squads else 'По умолчанию'} + +📆 Действует до: {subscription.end_date.strftime('%d.%m.%Y %H:%M')} +🔗 Реферер: {f'ID {user.referred_by_id}' if user.referred_by_id else 'Нет'} + +⏰ {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}""" + + return await self._send_message(message) + + except Exception as e: + logger.error(f"Ошибка отправки уведомления о триале: {e}") + return False + + async def send_subscription_purchase_notification( + self, + user: User, + subscription: Subscription, + transaction: Transaction, + period_days: int, + was_trial_conversion: bool = False + ) -> bool: + if not self._is_enabled(): + return False + + try: + event_type = "🔄 КОНВЕРСИЯ ИЗ ТРИАЛА" if was_trial_conversion else "💎 ПОКУПКА ПОДПИСКИ" + + if was_trial_conversion: + user_status = "🎯 Конверсия из триала" + elif user.has_had_paid_subscription: + user_status = "🔄 Продление/Обновление" + else: + user_status = "🆕 Первая покупка" + + servers_info = await self._get_servers_info(subscription.connected_squads) + + payment_method = self._get_payment_method_display(transaction.payment_method) + + message = f"""💎 {event_type} + +👤 Пользователь: {user.full_name} +🆔 Telegram ID: {user.telegram_id} +📱 Username: @{user.username or 'отсутствует'} +👥 Статус: {user_status} + +💰 Платеж: +💵 Сумма: {settings.format_price(transaction.amount_kopeks)} +💳 Способ: {payment_method} +🆔 ID транзакции: {transaction.id} + +📱 Параметры подписки: +📅 Период: {period_days} дней +📊 Трафик: {self._format_traffic(subscription.traffic_limit_gb)} +📱 Устройства: {subscription.device_limit} +🌐 Серверы: {servers_info} + +📆 Действует до: {subscription.end_date.strftime('%d.%m.%Y %H:%M')} +💰 Баланс после покупки: {settings.format_price(user.balance_kopeks)} +🔗 Реферер: {f'ID {user.referred_by_id}' if user.referred_by_id else 'Нет'} + +⏰ {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}""" + + return await self._send_message(message) + + except Exception as e: + logger.error(f"Ошибка отправки уведомления о покупке: {e}") + return False + + async def send_balance_topup_notification( + self, + user: User, + transaction: Transaction, + old_balance: int + ) -> bool: + if not self._is_enabled(): + return False + + try: + topup_status = "🆕 Первое пополнение" if not user.has_made_first_topup else "🔄 Пополнение" + + payment_method = self._get_payment_method_display(transaction.payment_method) + + balance_change = user.balance_kopeks - old_balance + + message = f"""💰 ПОПОЛНЕНИЕ БАЛАНСА + +👤 Пользователь: {user.full_name} +🆔 Telegram ID: {user.telegram_id} +📱 Username: @{user.username or 'отсутствует'} +💳 Статус: {topup_status} + +💰 Детали пополнения: +💵 Сумма: {settings.format_price(transaction.amount_kopeks)} +💳 Способ: {payment_method} +🆔 ID транзакции: {transaction.id} + +💰 Баланс: +📉 Было: {settings.format_price(old_balance)} +📈 Стало: {settings.format_price(user.balance_kopeks)} +➕ Изменение: +{settings.format_price(balance_change)} + +🔗 Реферер: {f'ID {user.referred_by_id}' if user.referred_by_id else 'Нет'} +📱 Подписка: {self._get_subscription_status(user)} + +⏰ {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}""" + + return await self._send_message(message) + + except Exception as e: + logger.error(f"Ошибка отправки уведомления о пополнении: {e}") + return False + + async def send_subscription_extension_notification( + self, + user: User, + subscription: Subscription, + transaction: Transaction, + extended_days: int, + old_end_date: datetime + ) -> bool: + if not self._is_enabled(): + return False + + try: + payment_method = self._get_payment_method_display(transaction.payment_method) + + servers_info = await self._get_servers_info(subscription.connected_squads) + + message = f"""⏰ ПРОДЛЕНИЕ ПОДПИСКИ + +👤 Пользователь: {user.full_name} +🆔 Telegram ID: {user.telegram_id} +📱 Username: @{user.username or 'отсутствует'} + +💰 Платеж: +💵 Сумма: {settings.format_price(transaction.amount_kopeks)} +💳 Способ: {payment_method} +🆔 ID транзакции: {transaction.id} + +📅 Продление: +➕ Добавлено дней: {extended_days} +📆 Было до: {old_end_date.strftime('%d.%m.%Y %H:%M')} +📆 Стало до: {subscription.end_date.strftime('%d.%m.%Y %H:%M')} + +📱 Текущие параметры: +📊 Трафик: {self._format_traffic(subscription.traffic_limit_gb)} +📱 Устройства: {subscription.device_limit} +🌐 Серверы: {servers_info} + +💰 Баланс после операции: {settings.format_price(user.balance_kopeks)} + +⏰ {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}""" + + return await self._send_message(message) + + except Exception as e: + logger.error(f"Ошибка отправки уведомления о продлении: {e}") + return False + + async def _send_message(self, text: str) -> bool: + if not self.chat_id: + logger.warning("ADMIN_NOTIFICATIONS_CHAT_ID не настроен") + return False + + try: + message_kwargs = { + 'chat_id': self.chat_id, + 'text': text, + 'parse_mode': 'HTML', + 'disable_web_page_preview': True + } + + if self.topic_id: + message_kwargs['message_thread_id'] = self.topic_id + + await self.bot.send_message(**message_kwargs) + logger.info(f"Уведомление отправлено в чат {self.chat_id}") + return True + + except TelegramForbiddenError: + logger.error(f"Бот не имеет прав для отправки в чат {self.chat_id}") + return False + except TelegramBadRequest as e: + logger.error(f"Ошибка отправки уведомления: {e}") + return False + except Exception as e: + logger.error(f"Неожиданная ошибка при отправке уведомления: {e}") + return False + + def _is_enabled(self) -> bool: + return self.enabled and bool(self.chat_id) + + def _get_payment_method_display(self, payment_method: Optional[str]) -> str: + method_names = { + 'telegram_stars': '⭐ Telegram Stars', + 'yookassa': '💳 YooKassa (карта)', + 'tribute': '💎 Tribute (карта)', + 'manual': '🛠️ Вручную (админ)', + 'balance': '💰 С баланса' + } + return method_names.get(payment_method, f'❓ {payment_method or "Неизвестно"}') + + def _format_traffic(self, traffic_gb: int) -> str: + if traffic_gb == 0: + return "∞ Безлимит" + return f"{traffic_gb} ГБ" + + def _get_subscription_status(self, user: User) -> str: + if not user.subscription: + return "❌ Нет подписки" + + sub = user.subscription + if sub.is_trial: + return f"🎯 Триал (до {sub.end_date.strftime('%d.%m')})" + elif sub.is_active: + return f"✅ Активна (до {sub.end_date.strftime('%d.%m')})" + else: + return "❌ Неактивна" + + async def _get_servers_info(self, squad_uuids: list) -> str: + if not squad_uuids: + return "❌ Нет серверов" + + try: + from app.handlers.subscription import get_servers_display_names + servers_names = await get_servers_display_names(squad_uuids) + return f"{len(squad_uuids)} шт. ({servers_names})" + except Exception as e: + logger.warning(f"Не удалось получить названия серверов: {e}") + return f"{len(squad_uuids)} шт."