felhasználói felületre pü
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
🤖 Inactivity Monitor Worker (Robot-21)
|
||||
Detects users who have been inactive for 180+ days and flags them so the
|
||||
MLM Commission Engine can bypass them for Gen1/Gen2 reward calculations.
|
||||
Detects users who have been inactive for N+ days (configured via system_parameters)
|
||||
and flags them so the MLM Commission Engine can bypass them for Gen1/Gen2 rewards.
|
||||
|
||||
Process:
|
||||
1. Find users where last_activity_at < NOW() - INTERVAL '180 days'
|
||||
1. Read inactivity_threshold_days from system.system_parameters (fallback: 180)
|
||||
2. Find users where last_activity_at < NOW() - INTERVAL 'N days'
|
||||
AND is_active = True AND is_deleted = False
|
||||
2. Set is_active = False
|
||||
3. Set custom_permissions['inactivity_suspended'] = True
|
||||
3. Set is_active = False
|
||||
4. Set custom_permissions['inactivity_suspended'] = True
|
||||
and custom_permissions['inactivity_suspended_at'] = ISO timestamp
|
||||
4. Record ledger entry (ACCOUNT_SUSPENDED_INACTIVITY)
|
||||
5. Send notification
|
||||
5. Record ledger entry (ACCOUNT_SUSPENDED_INACTIVITY)
|
||||
6. Send notification
|
||||
|
||||
MLM Integration:
|
||||
- The custom_permissions['inactivity_suspended'] flag is checked by the
|
||||
@@ -38,30 +39,61 @@ 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.models.system import SystemParameter
|
||||
from app.services.notification_service import NotificationService
|
||||
|
||||
logger = logging.getLogger("inactivity-monitor-worker")
|
||||
|
||||
# ── Constants ──────────────────────────────────────────────────────────────────
|
||||
PROCESS_NAME = "Inactivity-Monitor"
|
||||
INACTIVITY_DAYS = 180
|
||||
DEFAULT_INACTIVITY_DAYS = 180
|
||||
|
||||
|
||||
async def _get_inactivity_threshold(db: AsyncSession) -> int:
|
||||
"""
|
||||
Read the inactivity_threshold_days from system.system_parameters.
|
||||
Falls back to DEFAULT_INACTIVITY_DAYS (180) if not found.
|
||||
"""
|
||||
try:
|
||||
stmt = select(SystemParameter).where(
|
||||
SystemParameter.key == "inactivity_threshold_days",
|
||||
SystemParameter.scope_level == "global",
|
||||
SystemParameter.is_active == True,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
param = result.scalar_one_or_none()
|
||||
if param is not None:
|
||||
val = param.value
|
||||
if isinstance(val, dict):
|
||||
# Could be stored as {"value": 180} or just 180
|
||||
return int(val.get("value", val.get("days", DEFAULT_INACTIVITY_DAYS)))
|
||||
return int(val)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to read inactivity_threshold_days from DB: {e}. "
|
||||
f"Falling back to {DEFAULT_INACTIVITY_DAYS}."
|
||||
)
|
||||
return DEFAULT_INACTIVITY_DAYS
|
||||
|
||||
|
||||
async def process_inactive_users(db: AsyncSession) -> dict:
|
||||
"""
|
||||
Finds and flags users inactive for 180+ days.
|
||||
Finds and flags users inactive for N+ days (configurable via system_parameters).
|
||||
|
||||
Two-phase detection:
|
||||
A. Users with last_activity_at: last_activity_at < NOW() - 180 days
|
||||
A. Users with last_activity_at: last_activity_at < NOW() - N days
|
||||
B. Users without last_activity_at (never logged in):
|
||||
created_at < NOW() - 180 days AND last_activity_at IS NULL
|
||||
created_at < NOW() - N days AND last_activity_at IS NULL
|
||||
|
||||
Returns:
|
||||
dict: Statistics (processed_count, suspended_users, errors)
|
||||
"""
|
||||
inactivity_days = await _get_inactivity_threshold(db)
|
||||
now = datetime.now(timezone.utc)
|
||||
cutoff = now - timedelta(days=INACTIVITY_DAYS)
|
||||
stats = {"processed": 0, "suspended": [], "errors": []}
|
||||
cutoff = now - timedelta(days=inactivity_days)
|
||||
stats = {"processed": 0, "suspended": [], "errors": [], "threshold_days": inactivity_days}
|
||||
|
||||
logger.info(f"Inactivity threshold: {inactivity_days} days (cutoff={cutoff.isoformat()})")
|
||||
|
||||
# ── Phase A: Users with last_activity_at ──
|
||||
# NOTE: We use a subquery approach to avoid PostgreSQL's
|
||||
@@ -114,7 +146,7 @@ async def process_inactive_users(db: AsyncSession) -> dict:
|
||||
try:
|
||||
# Determine the reference timestamp for this user
|
||||
ref_ts = user.last_activity_at or user.created_at
|
||||
days_since = (now - ref_ts).days if ref_ts else INACTIVITY_DAYS
|
||||
days_since = (now - ref_ts).days if ref_ts else inactivity_days
|
||||
|
||||
# 1. Deactivate the user
|
||||
user.is_active = False
|
||||
@@ -141,7 +173,7 @@ async def process_inactive_users(db: AsyncSession) -> dict:
|
||||
),
|
||||
"inactivity_days": days_since,
|
||||
"last_activity_at": ref_ts.isoformat() if ref_ts else None,
|
||||
"cutoff_days": INACTIVITY_DAYS,
|
||||
"cutoff_days": inactivity_days,
|
||||
},
|
||||
)
|
||||
db.add(ledger_entry)
|
||||
|
||||
@@ -72,7 +72,11 @@ async def process_expired_subscriptions(db: AsyncSession) -> dict:
|
||||
user_id=user.id,
|
||||
amount=0.0,
|
||||
entry_type=LedgerEntryType.DEBIT,
|
||||
wallet_type=WalletType.SYSTEM,
|
||||
# JAVÍTÁS: WalletType.SYSTEM nem létező enum érték.
|
||||
# A WalletType csak EARNED, PURCHASED, SERVICE_COINS, VOUCHER értékeket támogat.
|
||||
# Mivel ez csak egy subscription lejárati naplóbejegyzés (amount=0),
|
||||
# a SERVICE_COINS a legmegfelelőbb alapértelmezett választás.
|
||||
wallet_type=WalletType.SERVICE_COINS,
|
||||
transaction_type="SUBSCRIPTION_EXPIRED",
|
||||
description=f"Előfizetés lejárt: {old_plan} → FREE",
|
||||
reference_type="subscription",
|
||||
|
||||
Reference in New Issue
Block a user