diff --git a/bot/handlers/user/payment.py b/bot/handlers/user/payment.py index 14f4dd1..2249a3e 100644 --- a/bot/handlers/user/payment.py +++ b/bot/handlers/user/payment.py @@ -20,6 +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 payment_processing_lock = asyncio.Lock() @@ -175,6 +176,15 @@ async def process_successful_payment(session: AsyncSession, bot: Bot, f"Failed to send final payment success message to user {user_id}: {e_notify}" ) + await notify_admin_new_payment( + bot, + settings, + i18n, + user_id, + subscription_months, + payment_value, + ) + except Exception as e_process: logging.error( f"Error during process_successful_payment main try block for user {user_id}: {e_process}", diff --git a/bot/handlers/user/trial_handler.py b/bot/handlers/user/trial_handler.py index 530c2c0..ad38106 100644 --- a/bot/handlers/user/trial_handler.py +++ b/bot/handlers/user/trial_handler.py @@ -7,6 +7,7 @@ from datetime import datetime from config.settings import Settings from bot.services.subscription_service import SubscriptionService from bot.services.panel_api_service import PanelApiService +from bot.services.notification_service import notify_admin_new_trial from bot.keyboards.inline.user_keyboards import ( get_trial_confirmation_keyboard, get_main_menu_inline_keyboard, @@ -189,6 +190,15 @@ async def confirm_activate_trial_handler( disable_web_page_preview=True, ) + if activation_result and activation_result.get("activated") and end_date_obj: + await notify_admin_new_trial( + callback.bot, + settings, + i18n, + user_id, + end_date_obj, + ) + @router.callback_query(F.data == "main_action:cancel_trial") async def cancel_trial_activation( diff --git a/bot/main_bot.py b/bot/main_bot.py index 66bdb5a..a2f799b 100644 --- a/bot/main_bot.py +++ b/bot/main_bot.py @@ -32,7 +32,6 @@ from bot.handlers.admin import admin_router_aggregate from bot.filters.admin_filter import AdminFilter from bot.services.notification_service import schedule_subscription_notifications -from bot.services.auto_renew_service import schedule_tribute_autorenew from bot.services.yookassa_service import YooKassaService from bot.services.panel_api_service import PanelApiService from bot.services.subscription_service import SubscriptionService @@ -117,12 +116,6 @@ async def on_startup_configured(dispatcher: Dispatcher): panel_service, async_session_factory, ) - schedule_tribute_autorenew( - settings, - scheduler, - panel_service, - async_session_factory, - ) scheduler.start() dispatcher["scheduler"] = scheduler logging.info("STARTUP: APScheduler started.") diff --git a/bot/services/auto_renew_service.py b/bot/services/auto_renew_service.py deleted file mode 100644 index 4eb6ebe..0000000 --- a/bot/services/auto_renew_service.py +++ /dev/null @@ -1,75 +0,0 @@ -import logging -from datetime import datetime, timezone -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from sqlalchemy.orm import sessionmaker - -from config.settings import Settings -from bot.services.subscription_service import SubscriptionService -from bot.services.panel_api_service import PanelApiService -from db.dal import subscription_dal - - -async def auto_extend_tribute_subscriptions( - settings: Settings, panel_service: PanelApiService, - async_session_factory: sessionmaker) -> None: - logging.info( - f"Scheduler job 'auto_extend_tribute_subscriptions' started at {datetime.now(timezone.utc)} UTC." - ) - async with async_session_factory() as session: - try: - sub_service = SubscriptionService(settings, panel_service) - subs = await subscription_dal.get_active_subscriptions_for_autorenew( - session, 'tribute', require_skip_flag=False) - if not subs: - logging.info("No Tribute subscriptions to auto-extend.") - return - for sub in subs: - if not sub.skip_notifications: - from db.dal import payment_dal - has_pay = await payment_dal.user_has_successful_payment_for_provider( - session, sub.user_id, 'tribute') - if not has_pay: - continue - await subscription_dal.set_skip_notifications_for_provider( - session, sub.user_id, 'tribute', True) - - months = sub.duration_months or 1 - bonus_days = months * 30 - await sub_service.extend_active_subscription_days( - session, sub.user_id, bonus_days, - reason='tribute_autorenew') - await session.commit() - logging.info( - f"Auto-extended {len(subs)} Tribute subscriptions and committed session.") - except Exception as e: - logging.error( - f"Error during auto_extend_tribute_subscriptions: {e}", - exc_info=True) - await session.rollback() - logging.info("Session rolled back due to error in auto_extend_tribute_subscriptions.") - - -def schedule_tribute_autorenew( - settings: Settings, scheduler: AsyncIOScheduler, - panel_service: PanelApiService, - async_session_factory: sessionmaker) -> None: - - async def job_wrapper(): - try: - await auto_extend_tribute_subscriptions( - settings, panel_service, async_session_factory) - except Exception as e: - logging.error( - f"Unhandled error in scheduled job 'auto_extend_tribute_subscriptions': {e}", - exc_info=True) - - scheduler.add_job( - job_wrapper, - 'cron', - hour=0, - minute=5, - name='daily_tribute_autorenew', - misfire_grace_time=60 * 15, - replace_existing=True) - logging.info( - "Tribute subscription auto-renew job scheduled daily at 00:05 UTC.") diff --git a/bot/services/notification_service.py b/bot/services/notification_service.py index 9faf8dc..e0c1d66 100644 --- a/bot/services/notification_service.py +++ b/bot/services/notification_service.py @@ -133,3 +133,46 @@ async def schedule_subscription_notifications( logging.info( f"Subscription expiration warning job scheduled daily at {notification_hour:02d}:{notification_minute:02d} UTC." ) + + +async def notify_admins(bot: Bot, settings: Settings, i18n: JsonI18n, + message_key: str, parse_mode: str | None = None, + **kwargs) -> None: + if not settings.ADMIN_IDS: + return + admin_lang = settings.DEFAULT_LANGUAGE + msg = i18n.gettext(admin_lang, message_key, **kwargs) + for admin_id in settings.ADMIN_IDS: + try: + await bot.send_message(admin_id, msg, parse_mode=parse_mode) + except Exception as e: + logging.error(f"Failed to send admin notification to {admin_id}: {e}") + + +async def notify_admin_new_trial(bot: Bot, settings: Settings, i18n: JsonI18n, + user_id: int, end_date: datetime) -> None: + end_date_str = end_date.strftime('%Y-%m-%d') if isinstance(end_date, datetime) else str(end_date) + await notify_admins( + bot, + settings, + i18n, + "admin_new_trial_notification", + user_id=user_id, + end_date=end_date_str, + ) + + +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, + ) diff --git a/locales/en.json b/locales/en.json index f17bac5..8ed8beb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -214,5 +214,8 @@ "subscription_ending_soon_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription ends on {end_date} (in {days_left} days).\n\nTo avoid interruption, please renew it in the main menu.", "subscription_cancelled_notification": "Your recurring subscription was cancelled. You will keep access until the paid period ends.", + "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}.", + "error_unknown": "An unknown error occurred." } diff --git a/locales/ru.json b/locales/ru.json index e08e427..25fd0bc 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -214,5 +214,8 @@ "subscription_ending_soon_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает {end_date} (через {days_left} дн.).\n\nЧтобы не потерять доступ, пожалуйста, продлите ее заранее в главном меню бота.", "subscription_cancelled_notification": "Ваша подписка отменена. Доступ сохранится до конца оплаченного периода.", + "admin_new_trial_notification": "\ud83c\udf21 Пользователь {user_id} активировал пробный период до {end_date}.", + "admin_new_payment_notification": "\ud83d\udcb3 Получен платеж от пользователя {user_id}: {months} мес. за {amount} {currency}.", + "error_unknown": "Произошла неизвестная ошибка." }