Some checks failed
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 2s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 42s
Test Suite / playwright-tests (push) Successful in 1m19s
- Added support for club feature quota bypass based on portal roles and profile grants in the capabilities check. - Introduced new functions to handle quota bypass logic in club feature access and consumption. - Updated the FeatureUsageBadge component to reflect platform exemptions for features. - Incremented application version to 0.8.195 and database schema version to 20260606083 to reflect these changes. - Enhanced backend routers to include new logic for consuming club features during AI-related actions.
114 lines
3.7 KiB
Python
114 lines
3.7 KiB
Python
"""
|
|
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,
|
|
}
|