""" Zusammenstellung effektiver Rechte für GET /api/me/entitlements (M4). Spez: CAPABILITY_CATALOG.v1.md §7.1, CLUB_MEMBERSHIP_AND_FEATURES.v1.md §8.1 """ from __future__ import annotations from datetime import datetime from typing import Any, Dict, Optional, TYPE_CHECKING from fastapi import HTTPException from capabilities import club_roles_in_club, resolve_capabilities_map from club_quota_bypass import is_club_feature_quota_bypassed, quota_bypass_access from club_features import club_features_map from club_tenancy import is_platform_admin from tenant_context import _club_exists if TYPE_CHECKING: from tenant_context import TenantContext def _serialize_reset_at(value: Any) -> Optional[str]: if value is None: return None if isinstance(value, datetime): if value.tzinfo is None: return value.replace(tzinfo=None).isoformat() + "Z" return value.isoformat() return str(value) def _resolve_target_club_id( cur, tenant: "TenantContext", club_id: Optional[int], ) -> Optional[int]: """Effektiver Verein für Entitlements (Query > Tenant).""" target = int(club_id) if club_id is not None else tenant.effective_club_id if target is None: return None if is_platform_admin(tenant.global_role): if not _club_exists(cur, target): raise HTTPException(status_code=400, detail="Verein nicht gefunden") return target if target not in tenant.club_ids: raise HTTPException(status_code=403, detail="Keine Mitgliedschaft in diesem Verein") return target def build_me_entitlements( cur, tenant: "TenantContext", *, club_id: Optional[int] = None, ) -> Dict[str, Any]: """ Kombiniert Account-Status, Capabilities und Feature-Kontingente. """ target_club = _resolve_target_club_id(cur, tenant, club_id) club_roles = club_roles_in_club(tenant, target_club) if target_club is not None else [] capabilities = resolve_capabilities_map(cur, tenant, club_id=target_club) features: Dict[str, Any] = {} plan_id = None if target_club is not None: raw = club_features_map(cur, target_club) plan_id = raw.get("plan_id") for fid, row in (raw.get("features") or {}).items(): if is_club_feature_quota_bypassed( cur, profile_id=tenant.profile_id, portal_role=tenant.global_role, feature_id=fid, tenant=tenant, ): ex = quota_bypass_access( feature_id=fid, club_id=target_club, plan_id=plan_id, ) features[fid] = { "allowed": True, "used": row.get("used"), "limit": None, "remaining": None, "reset_at": _serialize_reset_at(row.get("reset_at")), "reason": ex.get("reason"), "platform_exempt": True, } else: features[fid] = { "allowed": row.get("allowed"), "used": row.get("used"), "limit": row.get("limit"), "remaining": row.get("remaining"), "reset_at": _serialize_reset_at(row.get("reset_at")), "reason": row.get("reason"), "platform_exempt": False, } return { "account_state": tenant.account_state, "portal_role": tenant.global_role, "club_id": target_club, "plan_id": plan_id, "club_roles": club_roles, "capabilities": capabilities, "features": features, }