140 lines
5.2 KiB
Python
140 lines
5.2 KiB
Python
#!/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()) |