diff --git a/app/handlers/admin/monitoring.py b/app/handlers/admin/monitoring.py index ab2c5b0a..e707b2ac 100644 --- a/app/handlers/admin/monitoring.py +++ b/app/handlers/admin/monitoring.py @@ -54,21 +54,249 @@ def _build_notification_settings_view(language: str): keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=f"{trial_1h_status} • 1 час после триала", callback_data="admin_mon_notify_toggle_trial_1h")], + [InlineKeyboardButton(text="🧪 Тест: 1 час после триала", callback_data="admin_mon_notify_preview_trial_1h")], [InlineKeyboardButton(text=f"{trial_24h_status} • 24 часа после триала", callback_data="admin_mon_notify_toggle_trial_24h")], + [InlineKeyboardButton(text="🧪 Тест: 24 часа после триала", callback_data="admin_mon_notify_preview_trial_24h")], [InlineKeyboardButton(text=f"{expired_1d_status} • 1 день после истечения", callback_data="admin_mon_notify_toggle_expired_1d")], + [InlineKeyboardButton(text="🧪 Тест: 1 день после истечения", callback_data="admin_mon_notify_preview_expired_1d")], [InlineKeyboardButton(text=f"{second_wave_status} • 2-3 дня со скидкой", callback_data="admin_mon_notify_toggle_expired_2d")], + [InlineKeyboardButton(text="🧪 Тест: скидка 2-3 день", callback_data="admin_mon_notify_preview_expired_2d")], [InlineKeyboardButton(text=f"✏️ Скидка 2-3 дня: {second_percent}%", callback_data="admin_mon_notify_edit_2d_percent")], [InlineKeyboardButton(text=f"⏱️ Срок скидки 2-3 дня: {second_hours} ч", callback_data="admin_mon_notify_edit_2d_hours")], [InlineKeyboardButton(text=f"{third_wave_status} • {third_days} дней со скидкой", callback_data="admin_mon_notify_toggle_expired_nd")], + [InlineKeyboardButton(text="🧪 Тест: скидка спустя дни", callback_data="admin_mon_notify_preview_expired_nd")], [InlineKeyboardButton(text=f"✏️ Скидка {third_days} дней: {third_percent}%", callback_data="admin_mon_notify_edit_nd_percent")], [InlineKeyboardButton(text=f"⏱️ Срок скидки {third_days} дней: {third_hours} ч", callback_data="admin_mon_notify_edit_nd_hours")], [InlineKeyboardButton(text=f"📆 Порог уведомления: {third_days} дн.", callback_data="admin_mon_notify_edit_nd_threshold")], + [InlineKeyboardButton(text="🧪 Отправить все тесты", callback_data="admin_mon_notify_preview_all")], [InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_mon_settings")], ]) return summary_text, keyboard +def _build_notification_preview_message(language: str, notification_type: str): + texts = get_texts(language) + now = datetime.now() + price_30_days = settings.format_price(settings.PRICE_30_DAYS) + + from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton + + header = "🧪 Тестовое уведомление мониторинга\n\n" + + if notification_type == "trial_inactive_1h": + template = texts.get( + "TRIAL_INACTIVE_1H", + ( + "⏳ Прошёл час, а подключения нет\n\n" + "Если возникли сложности с запуском — воспользуйтесь инструкциями." + ), + ) + message = template.format( + price=price_30_days, + end_date=(now + timedelta(days=settings.TRIAL_DURATION_DAYS)).strftime("%d.%m.%Y %H:%M"), + ) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + callback_data="subscription_connect", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"), + callback_data="menu_subscription", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), + callback_data="menu_support", + ) + ], + ] + ) + elif notification_type == "trial_inactive_24h": + template = texts.get( + "TRIAL_INACTIVE_24H", + ( + "⏳ Вы ещё не подключились к VPN\n\n" + "Прошли сутки с активации тестового периода, но трафик не зафиксирован." + "\n\nНажмите кнопку ниже, чтобы подключиться." + ), + ) + message = template.format( + price=price_30_days, + end_date=(now + timedelta(days=1)).strftime("%d.%m.%Y %H:%M"), + ) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + callback_data="subscription_connect", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"), + callback_data="menu_subscription", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), + callback_data="menu_support", + ) + ], + ] + ) + elif notification_type == "expired_1d": + template = texts.get( + "SUBSCRIPTION_EXPIRED_1D", + ( + "⛔ Подписка закончилась\n\n" + "Доступ был отключён {end_date}. Продлите подписку, чтобы вернуться в сервис." + ), + ) + message = template.format( + end_date=(now - timedelta(days=1)).strftime("%d.%m.%Y %H:%M"), + price=price_30_days, + ) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"), + callback_data="subscription_extend", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("BALANCE_TOPUP", "💳 Пополнить баланс"), + callback_data="balance_topup", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), + callback_data="menu_support", + ) + ], + ] + ) + elif notification_type == "expired_2d": + percent = NotificationSettingsService.get_second_wave_discount_percent() + valid_hours = NotificationSettingsService.get_second_wave_valid_hours() + bonus_amount = settings.PRICE_30_DAYS * percent // 100 + template = texts.get( + "SUBSCRIPTION_EXPIRED_SECOND_WAVE", + ( + "🔥 Скидка {percent}% на продление\n\n" + "Нажмите «Получить скидку», и мы начислим {bonus} на баланс. " + "Предложение действует до {expires_at}." + ), + ) + message = template.format( + percent=percent, + bonus=settings.format_price(bonus_amount), + expires_at=(now + timedelta(hours=valid_hours)).strftime("%d.%m.%Y %H:%M"), + trigger_days=3, + ) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="🎁 Получить скидку", + callback_data="claim_discount_preview", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"), + callback_data="subscription_extend", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("BALANCE_TOPUP", "💳 Пополнить баланс"), + callback_data="balance_topup", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), + callback_data="menu_support", + ) + ], + ] + ) + elif notification_type == "expired_nd": + percent = NotificationSettingsService.get_third_wave_discount_percent() + valid_hours = NotificationSettingsService.get_third_wave_valid_hours() + trigger_days = NotificationSettingsService.get_third_wave_trigger_days() + bonus_amount = settings.PRICE_30_DAYS * percent // 100 + template = texts.get( + "SUBSCRIPTION_EXPIRED_THIRD_WAVE", + ( + "🎁 Индивидуальная скидка {percent}%\n\n" + "Прошло {trigger_days} дней без подписки — возвращайтесь, и мы добавим {bonus} на баланс. " + "Скидка действует до {expires_at}." + ), + ) + message = template.format( + percent=percent, + bonus=settings.format_price(bonus_amount), + trigger_days=trigger_days, + expires_at=(now + timedelta(hours=valid_hours)).strftime("%d.%m.%Y %H:%M"), + ) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="🎁 Получить скидку", + callback_data="claim_discount_preview", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"), + callback_data="subscription_extend", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("BALANCE_TOPUP", "💳 Пополнить баланс"), + callback_data="balance_topup", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), + callback_data="menu_support", + ) + ], + ] + ) + else: + raise ValueError(f"Unsupported notification type: {notification_type}") + + footer = "\n\nСообщение отправлено только вам для проверки оформления." + return header + message + footer, keyboard + + +async def _send_notification_preview(bot, chat_id: int, language: str, notification_type: str) -> None: + message, keyboard = _build_notification_preview_message(language, notification_type) + await bot.send_message( + chat_id, + message, + parse_mode="HTML", + reply_markup=keyboard, + ) + + async def _render_notification_settings(callback: CallbackQuery) -> None: language = (callback.from_user.language_code or settings.DEFAULT_LANGUAGE) text, keyboard = _build_notification_settings_view(language) @@ -200,6 +428,18 @@ async def toggle_trial_1h_notification(callback: CallbackQuery): await _render_notification_settings(callback) +@router.callback_query(F.data == "admin_mon_notify_preview_trial_1h") +@admin_required +async def preview_trial_1h_notification(callback: CallbackQuery): + try: + language = callback.from_user.language_code or settings.DEFAULT_LANGUAGE + await _send_notification_preview(callback.bot, callback.from_user.id, language, "trial_inactive_1h") + await callback.answer("✅ Пример отправлен") + except Exception as exc: + logger.error("Failed to send trial 1h preview: %s", exc) + await callback.answer("❌ Не удалось отправить тест", show_alert=True) + + @router.callback_query(F.data == "admin_mon_notify_toggle_trial_24h") @admin_required async def toggle_trial_24h_notification(callback: CallbackQuery): @@ -209,6 +449,18 @@ async def toggle_trial_24h_notification(callback: CallbackQuery): await _render_notification_settings(callback) +@router.callback_query(F.data == "admin_mon_notify_preview_trial_24h") +@admin_required +async def preview_trial_24h_notification(callback: CallbackQuery): + try: + language = callback.from_user.language_code or settings.DEFAULT_LANGUAGE + await _send_notification_preview(callback.bot, callback.from_user.id, language, "trial_inactive_24h") + await callback.answer("✅ Пример отправлен") + except Exception as exc: + logger.error("Failed to send trial 24h preview: %s", exc) + await callback.answer("❌ Не удалось отправить тест", show_alert=True) + + @router.callback_query(F.data == "admin_mon_notify_toggle_expired_1d") @admin_required async def toggle_expired_1d_notification(callback: CallbackQuery): @@ -218,6 +470,18 @@ async def toggle_expired_1d_notification(callback: CallbackQuery): await _render_notification_settings(callback) +@router.callback_query(F.data == "admin_mon_notify_preview_expired_1d") +@admin_required +async def preview_expired_1d_notification(callback: CallbackQuery): + try: + language = callback.from_user.language_code or settings.DEFAULT_LANGUAGE + await _send_notification_preview(callback.bot, callback.from_user.id, language, "expired_1d") + await callback.answer("✅ Пример отправлен") + except Exception as exc: + logger.error("Failed to send expired 1d preview: %s", exc) + await callback.answer("❌ Не удалось отправить тест", show_alert=True) + + @router.callback_query(F.data == "admin_mon_notify_toggle_expired_2d") @admin_required async def toggle_second_wave_notification(callback: CallbackQuery): @@ -227,6 +491,18 @@ async def toggle_second_wave_notification(callback: CallbackQuery): await _render_notification_settings(callback) +@router.callback_query(F.data == "admin_mon_notify_preview_expired_2d") +@admin_required +async def preview_second_wave_notification(callback: CallbackQuery): + try: + language = callback.from_user.language_code or settings.DEFAULT_LANGUAGE + await _send_notification_preview(callback.bot, callback.from_user.id, language, "expired_2d") + await callback.answer("✅ Пример отправлен") + except Exception as exc: + logger.error("Failed to send second wave preview: %s", exc) + await callback.answer("❌ Не удалось отправить тест", show_alert=True) + + @router.callback_query(F.data == "admin_mon_notify_toggle_expired_nd") @admin_required async def toggle_third_wave_notification(callback: CallbackQuery): @@ -236,6 +512,38 @@ async def toggle_third_wave_notification(callback: CallbackQuery): await _render_notification_settings(callback) +@router.callback_query(F.data == "admin_mon_notify_preview_expired_nd") +@admin_required +async def preview_third_wave_notification(callback: CallbackQuery): + try: + language = callback.from_user.language_code or settings.DEFAULT_LANGUAGE + await _send_notification_preview(callback.bot, callback.from_user.id, language, "expired_nd") + await callback.answer("✅ Пример отправлен") + except Exception as exc: + logger.error("Failed to send third wave preview: %s", exc) + await callback.answer("❌ Не удалось отправить тест", show_alert=True) + + +@router.callback_query(F.data == "admin_mon_notify_preview_all") +@admin_required +async def preview_all_notifications(callback: CallbackQuery): + try: + language = callback.from_user.language_code or settings.DEFAULT_LANGUAGE + chat_id = callback.from_user.id + for notification_type in [ + "trial_inactive_1h", + "trial_inactive_24h", + "expired_1d", + "expired_2d", + "expired_nd", + ]: + await _send_notification_preview(callback.bot, chat_id, language, notification_type) + await callback.answer("✅ Все тестовые уведомления отправлены") + except Exception as exc: + logger.error("Failed to send all notification previews: %s", exc) + await callback.answer("❌ Не удалось отправить тесты", show_alert=True) + + async def _start_notification_value_edit( callback: CallbackQuery, state: FSMContext,