""" Background job to process subscription downgrades at period end Runs periodically to check for subscriptions with: - status = 'pending_cancellation' - cancellation_effective_date <= now() Converts them to 'inactive' status """ import structlog import asyncio from datetime import datetime, timezone from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_async_session_factory from app.models.tenants import Subscription logger = structlog.get_logger() async def process_pending_cancellations(): """ Process all subscriptions that have reached their cancellation_effective_date """ async_session_factory = get_async_session_factory() async with async_session_factory() as session: try: query = select(Subscription).where( Subscription.status == 'pending_cancellation', Subscription.cancellation_effective_date <= datetime.now(timezone.utc) ) result = await session.execute(query) subscriptions_to_downgrade = result.scalars().all() downgraded_count = 0 for subscription in subscriptions_to_downgrade: subscription.status = 'inactive' subscription.plan = 'free' subscription.monthly_price = 0.0 logger.info( "subscription_downgraded_to_inactive", tenant_id=str(subscription.tenant_id), previous_plan=subscription.plan, cancellation_effective_date=subscription.cancellation_effective_date.isoformat() ) downgraded_count += 1 if downgraded_count > 0: await session.commit() logger.info( "subscriptions_downgraded", count=downgraded_count ) else: logger.debug("no_subscriptions_to_downgrade") return downgraded_count except Exception as e: logger.error( "subscription_downgrade_job_failed", error=str(e) ) await session.rollback() raise async def run_subscription_downgrade_job(): """ Main entry point for the subscription downgrade job Runs in a loop with configurable interval """ interval_seconds = 3600 # Run every hour logger.info("subscription_downgrade_job_started", interval_seconds=interval_seconds) while True: try: downgraded_count = await process_pending_cancellations() logger.info( "subscription_downgrade_job_completed", downgraded_count=downgraded_count ) except Exception as e: logger.error( "subscription_downgrade_job_error", error=str(e) ) await asyncio.sleep(interval_seconds) if __name__ == "__main__": asyncio.run(run_subscription_downgrade_job())