from datetime import datetime, timedelta, timezone from loguru import logger from app.models.payment import PaymentStatus from app.models.tariff import Tariff from app.models.user import User from app.repositories.payment import PaymentRepository from app.repositories.tariff import TariffRepository from app.repositories.user import UserRepository class SubscriptionInfo: def __init__( self, is_active: bool, tariff: Tariff | None = None, start_date: datetime | None = None, end_date: datetime | None = None, remaining_days: int = 0, auto_renew: bool = False, ): self.is_active = is_active self.tariff = tariff self.start_date = start_date self.end_date = end_date self.remaining_days = remaining_days self.auto_renew = auto_renew class BillingService: def __init__( self, payment_repo: PaymentRepository, tariff_repo: TariffRepository, user_repo: UserRepository, ): self.payment_repo = payment_repo self.tariff_repo = tariff_repo self.user_repo = user_repo async def activate_subscription( self, user_id: int, tariff_id: int, payment_id: int ) -> SubscriptionInfo: user = await self.user_repo.get(user_id) if user is None: raise ValueError(f"User not found: {user_id}") tariff = await self.tariff_repo.get(tariff_id) if tariff is None: raise ValueError(f"Tariff not found: {tariff_id}") payment = await self.payment_repo.get(payment_id) if payment is None: raise ValueError(f"Payment not found: {payment_id}") if payment.status != PaymentStatus.CONFIRMED: raise ValueError(f"Payment {payment_id} is not confirmed") now = datetime.now(timezone.utc) end_date = now + timedelta(days=tariff.duration_days) logger.info( "Subscription activated: user={} tariff={} until={}", user_id, tariff.name, end_date.date(), ) return self._build_subscription_info(tariff, now, end_date) async def get_active_subscription(self, user_id: int) -> SubscriptionInfo | None: user = await self.user_repo.get(user_id) if user is None: return None confirmed = await self.payment_repo.get_by_user_and_status( user_id, PaymentStatus.CONFIRMED ) if not confirmed: return SubscriptionInfo(is_active=False) latest = confirmed[0] tariff = await self.tariff_repo.get(latest.tariff_id) if tariff is None: return SubscriptionInfo(is_active=False) now = datetime.now(timezone.utc) start = latest.paid_at or latest.created_at end = start + timedelta(days=tariff.duration_days) if now > end: return SubscriptionInfo( is_active=False, tariff=tariff, start_date=start, end_date=end, remaining_days=0, ) remaining = (end - now).days return SubscriptionInfo( is_active=True, tariff=tariff, start_date=start, end_date=end, remaining_days=remaining, ) async def has_active_subscription(self, user_id: int) -> bool: info = await self.get_active_subscription(user_id) return info is not None and info.is_active async def expire_subscriptions(self) -> list[int]: expired_user_ids: list[int] = [] confirmed = await self.payment_repo.get_by_status(PaymentStatus.CONFIRMED) now = datetime.now(timezone.utc) for payment in confirmed: tariff = await self.tariff_repo.get(payment.tariff_id) if tariff is None: continue start = payment.paid_at or payment.created_at end = start + timedelta(days=tariff.duration_days) if now > end: expired_user_ids.append(payment.user_id) logger.info( "Subscription expired: user={} payment={}", payment.user_id, payment.id, ) return expired_user_ids @staticmethod def _build_subscription_info( tariff: Tariff, start: datetime, end: datetime ) -> SubscriptionInfo: remaining = max(0, (end - datetime.now(timezone.utc)).days) return SubscriptionInfo( is_active=True, tariff=tariff, start_date=start, end_date=end, remaining_days=remaining, )