#!/usr/bin/env python3 """ 🤖 Subscription Lifecycle Worker (Robot-20) A 20-as Gitea kártya implementációja: Lejárt előfizetések automatikus kezelése. Folyamat: 1. Lekérdezés: Azokat a User-eket, ahol subscription_expires_at < NOW() ÉS subscription_plan != 'FREE' 2. Downgrade: subscription_plan = "FREE", is_vip = False 3. Naplózás: Főkönyvi bejegyzés (SUBSCRIPTION_EXPIRED) 4. Értesítés: NotificationService küldése a downgrade-ről Futtatás: - Napi egyszer (cron) vagy manuálisan: docker exec sf_api python -m app.workers.system.subscription_worker """ import asyncio import logging from datetime import datetime, timezone from typing import List from sqlalchemy import select, update, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.database import AsyncSessionLocal from app.models.identity import User from app.models.audit import FinancialLedger, LedgerEntryType, WalletType from app.services.billing_engine import record_ledger_entry from app.services.notification_service import NotificationService logger = logging.getLogger("subscription-worker") async def process_expired_subscriptions(db: AsyncSession) -> dict: """ Atomikus tranzakcióban feldolgozza a lejárt előfizetéseket. Returns: dict: Statisztikák (processed_count, downgraded_users, errors) """ now = datetime.now(timezone.utc) # 1. Lekérdezés: Lejárt előfizetések, amelyek még nem FREE-ek stmt = select(User).where( and_( User.subscription_expires_at < now, User.subscription_plan != 'FREE', User.is_deleted == False, User.is_active == True ) ).with_for_update(skip_locked=True) # Atomikus zárolás result = await db.execute(stmt) expired_users: List[User] = result.scalars().all() stats = { "processed_count": len(expired_users), "downgraded_users": [], "errors": [] } for user in expired_users: try: # 2. Downgrade old_plan = user.subscription_plan user.subscription_plan = "FREE" user.is_vip = False # subscription_expires_at marad null vagy régi érték (lejárt) # 3. Főkönyvi bejegyzés (SUBSCRIPTION_EXPIRED) # Megjegyzés: amount = 0, mert nem történik pénzmozgás, csak naplózás ledger_entry = await record_ledger_entry( db=db, user_id=user.id, amount=0.0, entry_type=LedgerEntryType.DEBIT, wallet_type=WalletType.SYSTEM, transaction_type="SUBSCRIPTION_EXPIRED", description=f"Előfizetés lejárt: {old_plan} → FREE", reference_type="subscription", reference_id=user.id ) # 4. Értesítés küldése await NotificationService.send_notification( db=db, user_id=user.id, title="Előfizetésed lejárt", message=f"A(z) {old_plan} előfizetésed lejárt, ezért átállítottuk a FREE csomagra. További előnyökért frissíts előfizetést!", category="billing", priority="medium", data={ "old_plan": old_plan, "new_plan": "FREE", "user_id": user.id, "expired_at": user.subscription_expires_at.isoformat() if user.subscription_expires_at else None }, send_email=True, email_template="subscription_expired", email_vars={ "recipient": user.email, "first_name": user.person.first_name if user.person else "Partnerünk", "old_plan": old_plan, "new_plan": "FREE", "lang": user.preferred_language } ) stats["downgraded_users"].append({ "user_id": user.id, "email": user.email, "old_plan": old_plan, "new_plan": "FREE" }) logger.info(f"User {user.id} ({user.email}) subscription downgraded from {old_plan} to FREE") except Exception as e: logger.error(f"Error processing user {user.id}: {e}", exc_info=True) stats["errors"].append({"user_id": user.id, "error": str(e)}) # Commit a tranzakció await db.commit() return stats async def main(): """Fő futtató függvény, amelyet a cron hív.""" logger.info("Starting subscription worker...") async with AsyncSessionLocal() as db: try: stats = await process_expired_subscriptions(db) logger.info(f"Subscription worker completed. Stats: {stats}") print(f"✅ Subscription worker completed. Processed: {stats['processed_count']}, Downgraded: {len(stats['downgraded_users'])}") except Exception as e: logger.error(f"Subscription worker failed: {e}", exc_info=True) await db.rollback() raise if __name__ == "__main__": asyncio.run(main())