# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ View models for the /api/attributes/ endpoint. These extend base DB models with computed / enriched fields that the gateway adds at response time (JOINs, aggregations, synthetics). They are NEVER used for DB operations — only for ``getModelAttributeDefinitions()`` so the frontend can resolve column types via ``resolveColumnTypes`` without hardcoding. Naming convention: ``{BaseModel}View``. ``getModelClasses()`` in ``attributeUtils.py`` auto-discovers every ``datamodel*.py`` under ``modules/datamodels/`` — so placing them here is sufficient for registration. """ from typing import Optional, List from pydantic import Field from modules.datamodels.datamodelBase import MODEL_REGISTRY, PowerOnModel from modules.datamodels.datamodelMembership import UserMandate, FeatureAccess from modules.datamodels.datamodelBilling import BillingTransaction from modules.datamodels.datamodelSubscription import MandateSubscription from modules.datamodels.datamodelUiLanguage import UiLanguageSet from modules.features.neutralization.datamodelFeatureNeutralizer import DataNeutralizerAttributes from modules.shared.i18nRegistry import i18nModel # ============================================================================ # Punkt 1a: UserMandate + enriched user fields # ============================================================================ @i18nModel("Benutzer-Mandant (Ansicht)") class UserMandateView(UserMandate): """UserMandate erweitert um aufgeloeste Benutzerfelder und Rollenlabels.""" username: Optional[str] = Field( default=None, description="Username (resolved from userId)", json_schema_extra={"label": "Benutzername", "frontend_type": "text", "frontend_readonly": True}, ) email: Optional[str] = Field( default=None, description="E-Mail address (resolved from userId)", json_schema_extra={"label": "E-Mail", "frontend_type": "text", "frontend_readonly": True}, ) fullName: Optional[str] = Field( default=None, description="Full name (resolved from userId)", json_schema_extra={"label": "Vollstaendiger Name", "frontend_type": "text", "frontend_readonly": True}, ) roleLabels: Optional[List[str]] = Field( default=None, description="Role labels (resolved from junction table)", json_schema_extra={"label": "Rollen", "frontend_type": "text", "frontend_readonly": True}, ) # ============================================================================ # Punkt 1b: FeatureAccess + enriched user fields # ============================================================================ @i18nModel("Feature-Zugang (Ansicht)") class FeatureAccessView(FeatureAccess): """FeatureAccess erweitert um aufgeloeste Benutzerfelder und Rollenlabels.""" username: Optional[str] = Field( default=None, description="Username (resolved from userId)", json_schema_extra={"label": "Benutzername", "frontend_type": "text", "frontend_readonly": True}, ) email: Optional[str] = Field( default=None, description="E-Mail address (resolved from userId)", json_schema_extra={"label": "E-Mail", "frontend_type": "text", "frontend_readonly": True}, ) fullName: Optional[str] = Field( default=None, description="Full name (resolved from userId)", json_schema_extra={"label": "Vollstaendiger Name", "frontend_type": "text", "frontend_readonly": True}, ) roleLabels: Optional[List[str]] = Field( default=None, description="Role labels (resolved from junction table)", json_schema_extra={"label": "Rollen", "frontend_type": "text", "frontend_readonly": True}, ) # ============================================================================ # Punkt 1d: BillingTransaction + enriched mandate/user names # ============================================================================ @i18nModel("Transaktion (Ansicht)") class BillingTransactionView(BillingTransaction): """BillingTransaction erweitert um aufgeloeste Mandanten-/Benutzernamen.""" mandateName: Optional[str] = Field( default=None, description="Mandate name (resolved from accountId/mandateId)", json_schema_extra={"label": "Mandant", "frontend_type": "text", "frontend_readonly": True}, ) userName: Optional[str] = Field( default=None, description="User name (resolved from createdByUserId)", json_schema_extra={"label": "Benutzer", "frontend_type": "text", "frontend_readonly": True}, ) # ============================================================================ # Punkt 3a: MandateSubscription + aggregated fields # ============================================================================ @i18nModel("Abonnement (Ansicht)") class MandateSubscriptionView(MandateSubscription): """MandateSubscription erweitert um aggregierte Laufzeitwerte.""" mandateName: Optional[str] = Field( default=None, description="Mandate name (resolved from mandateId)", json_schema_extra={"label": "Mandant", "frontend_type": "text", "frontend_readonly": True}, ) planTitle: Optional[str] = Field( default=None, description="Plan title (resolved from planKey)", json_schema_extra={"label": "Plan", "frontend_type": "text", "frontend_readonly": True}, ) activeUsers: Optional[int] = Field( default=None, description="Number of active users in the mandate", json_schema_extra={"label": "Benutzer", "frontend_type": "number", "frontend_readonly": True}, ) activeInstances: Optional[int] = Field( default=None, description="Number of active feature instances in the mandate", json_schema_extra={"label": "Module", "frontend_type": "number", "frontend_readonly": True}, ) monthlyRevenueCHF: Optional[float] = Field( default=None, description="Calculated monthly revenue in CHF", json_schema_extra={"label": "Umsatz pro Monat", "frontend_type": "number", "frontend_readonly": True}, ) # ============================================================================ # Punkt 3b: UiLanguageSet + computed counts # ============================================================================ @i18nModel("Sprachset (Ansicht)") class UiLanguageSetView(UiLanguageSet): """UiLanguageSet erweitert um berechnete Uebersetzungszaehler.""" uiCount: Optional[int] = Field( default=None, description="Number of UI translation entries", json_schema_extra={"label": "UI", "frontend_type": "number", "frontend_readonly": True}, ) gatewayCount: Optional[int] = Field( default=None, description="Number of gateway/API translation entries", json_schema_extra={"label": "API", "frontend_type": "number", "frontend_readonly": True}, ) entriesCount: Optional[int] = Field( default=None, description="Total number of translation entries", json_schema_extra={"label": "Gesamt", "frontend_type": "number", "frontend_readonly": True}, ) # ============================================================================ # Punkt 1c: DataNeutralizerAttributes + enriched fields # # DataNeutralizerAttributes extends BaseModel (not PowerOnModel), so its # subclass does NOT auto-register in MODEL_REGISTRY. We register manually. # ============================================================================ @i18nModel("Neutralisierungs-Zuordnung (Ansicht)") class DataNeutralizerAttributesView(DataNeutralizerAttributes): """DataNeutralizerAttributes erweitert um synthetische/aufgeloeste Felder.""" placeholder: Optional[str] = Field( default=None, description="Synthetic placeholder string [patternType.id]", json_schema_extra={"label": "Platzhalter", "frontend_type": "text", "frontend_readonly": True}, ) username: Optional[str] = Field( default=None, description="Username (resolved from userId)", json_schema_extra={"label": "Benutzer", "frontend_type": "text", "frontend_readonly": True}, ) instanceLabel: Optional[str] = Field( default=None, description="Feature instance label (resolved from featureInstanceId)", json_schema_extra={"label": "Feature-Instanz", "frontend_type": "text", "frontend_readonly": True}, ) # Manual registration for non-PowerOnModel view MODEL_REGISTRY["DataNeutralizerAttributesView"] = DataNeutralizerAttributesView # type: ignore[assignment]