From 1e666f90d3f80fe9d09103b1381922cf2a112e04 Mon Sep 17 00:00:00 2001 From: Machka Pasla <161734431+machka-pasla@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:05:33 +0700 Subject: [PATCH] Improve Tribute auto-renew --- bot/main_bot.py | 7 +++ bot/services/auto_renew_service.py | 75 ++++++++++++++++++++++++++++++ db/dal/payment_dal.py | 13 ++++++ db/dal/subscription_dal.py | 22 +++++++++ 4 files changed, 117 insertions(+) create mode 100644 bot/services/auto_renew_service.py diff --git a/bot/main_bot.py b/bot/main_bot.py index a2f799b..66bdb5a 100644 --- a/bot/main_bot.py +++ b/bot/main_bot.py @@ -32,6 +32,7 @@ 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 @@ -116,6 +117,12 @@ 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 new file mode 100644 index 0000000..4eb6ebe --- /dev/null +++ b/bot/services/auto_renew_service.py @@ -0,0 +1,75 @@ +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/db/dal/payment_dal.py b/db/dal/payment_dal.py index b003117..b530212 100644 --- a/db/dal/payment_dal.py +++ b/db/dal/payment_dal.py @@ -96,6 +96,19 @@ async def update_payment_status_by_db_id( return payment +async def user_has_successful_payment_for_provider( + session: AsyncSession, user_id: int, provider: str) -> bool: + """Check if a user has at least one successful payment for the provider.""" + + stmt = (select(Payment.payment_id) + .where(Payment.user_id == user_id, + Payment.provider == provider, + Payment.status == 'succeeded') + .limit(1)) + result = await session.execute(stmt) + return result.scalar_one_or_none() is not None + + async def update_payment_status_by_yk_id(session: AsyncSession, yookassa_payment_id: str, new_status: str) -> Optional[Payment]: diff --git a/db/dal/subscription_dal.py b/db/dal/subscription_dal.py index e203184..105303c 100644 --- a/db/dal/subscription_dal.py +++ b/db/dal/subscription_dal.py @@ -231,3 +231,25 @@ async def set_skip_notifications_for_provider( Subscription.provider == provider).values(skip_notifications=skip)) result = await session.execute(stmt) return result.rowcount + + +async def get_active_subscriptions_for_autorenew( + session: AsyncSession, provider: str, + days_threshold: int = 1, + require_skip_flag: bool = True) -> List[Subscription]: + """Fetch active subscriptions nearing expiration for auto-renew logic.""" + + now_utc = datetime.now(timezone.utc) + threshold_date = now_utc + timedelta(days=days_threshold) + + conditions = [ + Subscription.provider == provider, + Subscription.is_active == True, + Subscription.end_date <= threshold_date, + ] + if require_skip_flag: + conditions.append(Subscription.skip_notifications == True) + + stmt = select(Subscription).where(*conditions) + result = await session.execute(stmt) + return result.scalars().all()