felhasználói felületre pü

This commit is contained in:
Roo
2026-07-27 08:39:18 +00:00
parent c9118bf52f
commit 6f28d3e70d
72 changed files with 6311 additions and 749 deletions

View File

@@ -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)

View File

@@ -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",