From 472ef3749073c1da11bebe36c7b2591534495724 Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 12 Jan 2026 15:25:25 +0300 Subject: [PATCH] Add files via upload --- app/database/crud/subscription.py | 164 +++++++++++++++++++++++++++++- app/database/crud/tariff.py | 10 ++ 2 files changed, 172 insertions(+), 2 deletions(-) diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index 37df85f2..8786ffff 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -1756,7 +1756,167 @@ async def activate_pending_subscription( await db.commit() await db.refresh(pending_subscription) - + logger.info(f"Подписка пользователя {user_id} активирована, ID: {pending_subscription.id}") - + return pending_subscription + + +# ==================== СУТОЧНЫЕ ПОДПИСКИ ==================== + + +async def get_daily_subscriptions_for_charge(db: AsyncSession) -> List[Subscription]: + """ + Получает все суточные подписки, которые нужно обработать для списания. + + Критерии: + - Тариф подписки суточный (is_daily=True) + - Подписка активна + - Подписка не приостановлена пользователем + - Прошло более 24 часов с последнего списания (или списания ещё не было) + """ + from app.database.models import Tariff + + now = datetime.utcnow() + one_day_ago = now - timedelta(hours=24) + + query = ( + select(Subscription) + .join(Tariff, Subscription.tariff_id == Tariff.id) + .options( + selectinload(Subscription.user), + selectinload(Subscription.tariff), + ) + .where( + and_( + Tariff.is_daily.is_(True), + Tariff.is_active.is_(True), + Subscription.status == SubscriptionStatus.ACTIVE.value, + Subscription.is_daily_paused.is_(False), + # Списания ещё не было ИЛИ прошло более 24 часов + ( + (Subscription.last_daily_charge_at.is_(None)) | + (Subscription.last_daily_charge_at < one_day_ago) + ), + ) + ) + ) + + result = await db.execute(query) + subscriptions = result.scalars().all() + + logger.info( + f"🔍 Найдено {len(subscriptions)} суточных подписок для списания" + ) + + return list(subscriptions) + + +async def pause_daily_subscription( + db: AsyncSession, + subscription: Subscription, +) -> Subscription: + """Приостанавливает суточную подписку (списание не будет происходить).""" + if not subscription.is_daily_tariff: + logger.warning( + f"Попытка приостановить не-суточную подписку {subscription.id}" + ) + return subscription + + subscription.is_daily_paused = True + await db.commit() + await db.refresh(subscription) + + logger.info( + f"⏸️ Суточная подписка {subscription.id} приостановлена пользователем {subscription.user_id}" + ) + + return subscription + + +async def resume_daily_subscription( + db: AsyncSession, + subscription: Subscription, +) -> Subscription: + """Возобновляет суточную подписку (списание продолжится).""" + if not subscription.is_daily_tariff: + logger.warning( + f"Попытка возобновить не-суточную подписку {subscription.id}" + ) + return subscription + + subscription.is_daily_paused = False + await db.commit() + await db.refresh(subscription) + + logger.info( + f"▶️ Суточная подписка {subscription.id} возобновлена пользователем {subscription.user_id}" + ) + + return subscription + + +async def update_daily_charge_time( + db: AsyncSession, + subscription: Subscription, + charge_time: datetime = None, +) -> Subscription: + """Обновляет время последнего суточного списания.""" + subscription.last_daily_charge_at = charge_time or datetime.utcnow() + await db.commit() + await db.refresh(subscription) + + return subscription + + +async def suspend_daily_subscription_insufficient_balance( + db: AsyncSession, + subscription: Subscription, +) -> Subscription: + """ + Приостанавливает подписку из-за недостатка баланса. + Отличается от pause_daily_subscription тем, что меняет статус на DISABLED. + """ + subscription.status = SubscriptionStatus.DISABLED.value + await db.commit() + await db.refresh(subscription) + + logger.info( + f"⚠️ Суточная подписка {subscription.id} приостановлена: недостаточно средств (user_id={subscription.user_id})" + ) + + return subscription + + +async def get_subscription_with_tariff( + db: AsyncSession, + user_id: int, +) -> Optional[Subscription]: + """Получает подписку пользователя с загруженным тарифом.""" + result = await db.execute( + select(Subscription) + .options( + selectinload(Subscription.user), + selectinload(Subscription.tariff), + ) + .where(Subscription.user_id == user_id) + .order_by(Subscription.created_at.desc()) + .limit(1) + ) + subscription = result.scalar_one_or_none() + + if subscription: + subscription = await check_and_update_subscription_status(db, subscription) + + return subscription + + +async def toggle_daily_subscription_pause( + db: AsyncSession, + subscription: Subscription, +) -> Subscription: + """Переключает состояние паузы суточной подписки.""" + if subscription.is_daily_paused: + return await resume_daily_subscription(db, subscription) + else: + return await pause_daily_subscription(db, subscription) diff --git a/app/database/crud/tariff.py b/app/database/crud/tariff.py index 04c4f193..b607608c 100644 --- a/app/database/crud/tariff.py +++ b/app/database/crud/tariff.py @@ -170,6 +170,8 @@ async def create_tariff( traffic_topup_enabled: bool = False, traffic_topup_packages: Optional[Dict[str, int]] = None, max_topup_traffic_gb: int = 0, + is_daily: bool = False, + daily_price_kopeks: int = 0, ) -> Tariff: """Создает новый тариф.""" normalized_prices = _normalize_period_prices(period_prices) @@ -188,6 +190,8 @@ async def create_tariff( traffic_topup_enabled=traffic_topup_enabled, traffic_topup_packages=traffic_topup_packages or {}, max_topup_traffic_gb=max(0, max_topup_traffic_gb), + is_daily=is_daily, + daily_price_kopeks=max(0, daily_price_kopeks), ) db.add(tariff) @@ -236,6 +240,8 @@ async def update_tariff( traffic_topup_enabled: Optional[bool] = None, traffic_topup_packages: Optional[Dict[str, int]] = None, max_topup_traffic_gb: Optional[int] = None, + is_daily: Optional[bool] = None, + daily_price_kopeks: Optional[int] = None, ) -> Tariff: """Обновляет существующий тариф.""" if name is not None: @@ -267,6 +273,10 @@ async def update_tariff( tariff.traffic_topup_packages = traffic_topup_packages if max_topup_traffic_gb is not None: tariff.max_topup_traffic_gb = max(0, max_topup_traffic_gb) + if is_daily is not None: + tariff.is_daily = is_daily + if daily_price_kopeks is not None: + tariff.daily_price_kopeks = max(0, daily_price_kopeks) # Обновляем промогруппы если указаны if promo_group_ids is not None: