# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Trustee Feature Container - Main Module. Handles feature initialization and RBAC catalog registration. """ import logging from typing import Dict, List, Any logger = logging.getLogger(__name__) # Feature metadata FEATURE_CODE = "trustee" FEATURE_LABEL = "Treuhand" FEATURE_ICON = "mdi-briefcase" # UI Objects for RBAC catalog # Note: organisations and contracts removed - feature instance = organisation UI_OBJECTS = [ { "objectKey": "ui.feature.trustee.dashboard", "label": "Dashboard", "meta": {"area": "dashboard"} }, { "objectKey": "ui.feature.trustee.positions", "label": "Positionen", "meta": {"area": "positions"} }, { "objectKey": "ui.feature.trustee.documents", "label": "Dokumente", "meta": {"area": "documents"} }, { "objectKey": "ui.feature.trustee.expense-import", "label": "Spesen Import", "meta": {"area": "expense-import"} }, { "objectKey": "ui.feature.trustee.scan-upload", "label": "Scannen / Hochladen", "meta": {"area": "scan-upload"} }, { "objectKey": "ui.feature.trustee.analyse", "label": "Analyse & Reporting", "meta": {"area": "analyse"} }, { "objectKey": "ui.feature.trustee.abschluss", "label": "Abschluss & Prüfung", "meta": {"area": "abschluss"} }, { "objectKey": "ui.feature.trustee.settings", "label": "Buchhaltungs-Einstellungen", "meta": {"area": "settings", "admin_only": True} }, { "objectKey": "ui.feature.trustee.instance-roles", "label": "Instanz-Rollen & Berechtigungen", "meta": {"area": "admin", "admin_only": True} }, ] # DATA Objects for RBAC catalog (tables/entities) # Used for AccessRules on data-level permissions DATA_OBJECTS = [ { "objectKey": "data.feature.trustee.TrusteeOrganisation", "label": "Organisation", "meta": { "table": "TrusteeOrganisation", "fields": ["id", "label", "enabled"], "isParent": True, "displayFields": ["label"], } }, { "objectKey": "data.feature.trustee.TrusteePosition", "label": "Position", "meta": { "table": "TrusteePosition", "fields": ["id", "label", "description", "organisationId"], "parentTable": "TrusteeOrganisation", "parentKey": "organisationId", } }, { "objectKey": "data.feature.trustee.TrusteeDocument", "label": "Dokument", "meta": {"table": "TrusteeDocument", "fields": ["id", "filename", "mimeType", "fileSize", "uploadDate"]} }, { "objectKey": "data.feature.trustee.TrusteeAccountingConfig", "label": "Buchhaltungs-Konfiguration", "meta": { "table": "TrusteeAccountingConfig", "fields": ["id", "connectorType", "displayLabel", "encryptedConfig", "isActive"], "parentTable": "TrusteeOrganisation", "parentKey": "organisationId", } }, { "objectKey": "data.feature.trustee.TrusteeAccountingSync", "label": "Buchhaltungs-Synchronisation", "meta": {"table": "TrusteeAccountingSync", "fields": ["id", "positionId", "syncStatus", "externalId"]} }, { "objectKey": "data.feature.trustee.TrusteeDataAccount", "label": "Kontenplan (Sync)", "meta": {"table": "TrusteeDataAccount", "fields": ["id", "accountNumber", "label", "accountType", "accountGroup", "currency", "isActive"]} }, { "objectKey": "data.feature.trustee.TrusteeDataJournalEntry", "label": "Buchungen (Sync)", "meta": {"table": "TrusteeDataJournalEntry", "fields": ["id", "externalId", "bookingDate", "reference", "description", "currency", "totalAmount"]} }, { "objectKey": "data.feature.trustee.TrusteeDataJournalLine", "label": "Buchungszeilen (Sync)", "meta": {"table": "TrusteeDataJournalLine", "fields": ["id", "journalEntryId", "accountNumber", "debitAmount", "creditAmount", "currency", "taxCode", "costCenter", "description"]} }, { "objectKey": "data.feature.trustee.TrusteeDataContact", "label": "Kontakte (Sync)", "meta": {"table": "TrusteeDataContact", "fields": ["id", "externalId", "contactType", "contactNumber", "name", "address", "zip", "city", "country", "email", "phone", "vatNumber"]} }, { "objectKey": "data.feature.trustee.TrusteeDataAccountBalance", "label": "Kontosalden (Sync)", "meta": {"table": "TrusteeDataAccountBalance", "fields": ["id", "accountNumber", "periodYear", "periodMonth", "openingBalance", "debitTotal", "creditTotal", "closingBalance", "currency"]} }, { "objectKey": "data.feature.trustee.*", "label": "Alle Treuhand-Daten", "meta": {"wildcard": True, "description": "Wildcard for all trustee data tables"} }, ] # Resource Objects for RBAC catalog # Note: organisations and contracts removed - feature instance = organisation RESOURCE_OBJECTS = [ { "objectKey": "resource.feature.trustee.documents.create", "label": "Dokument hochladen", "meta": {"endpoint": "/api/trustee/{instanceId}/documents", "method": "POST"} }, { "objectKey": "resource.feature.trustee.documents.update", "label": "Dokument aktualisieren", "meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "PUT"} }, { "objectKey": "resource.feature.trustee.documents.delete", "label": "Dokument löschen", "meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "DELETE"} }, { "objectKey": "resource.feature.trustee.positions.create", "label": "Position erstellen", "meta": {"endpoint": "/api/trustee/{instanceId}/positions", "method": "POST"} }, { "objectKey": "resource.feature.trustee.positions.update", "label": "Position aktualisieren", "meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "PUT"} }, { "objectKey": "resource.feature.trustee.positions.delete", "label": "Position löschen", "meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "DELETE"} }, { "objectKey": "resource.feature.trustee.instance-roles.manage", "label": "Instanz-Rollen verwalten", "meta": {"endpoint": "/api/trustee/{instanceId}/instance-roles", "method": "ALL", "admin_only": True} }, { "objectKey": "resource.feature.trustee.accounting.manage", "label": "Buchhaltungs-Integration verwalten", "meta": {"endpoint": "/api/trustee/{instanceId}/accounting/config", "method": "ALL", "admin_only": True} }, { "objectKey": "resource.feature.trustee.accounting.sync", "label": "Buchhaltung synchronisieren", "meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync", "method": "POST"} }, { "objectKey": "resource.feature.trustee.accounting.view", "label": "Sync-Status einsehen", "meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync-status", "method": "GET"} }, { "objectKey": "resource.feature.trustee.workflows.view", "label": "Workflows einsehen", "meta": {"endpoint": "/api/workflows/{instanceId}/workflows", "method": "GET"} }, { "objectKey": "resource.feature.trustee.workflows.execute", "label": "Workflows ausführen", "meta": {"endpoint": "/api/workflows/{instanceId}/execute", "method": "POST"} }, { "objectKey": "resource.feature.trustee.workflows.manage", "label": "Workflows verwalten", "meta": {"endpoint": "/api/workflows/{instanceId}/workflows", "method": "ALL", "admin_only": True} }, ] # Template roles for this feature with AccessRules # Each role defines default UI and DATA permissions # Note: UI item=None means ALL views, specific items restrict to named views # IMPORTANT: item uses vollqualifizierte ObjectKeys (gemäss Navigation-API-Konzept) QUICK_ACTION_CATEGORIES = [ {"id": "import", "label": "Import & Verarbeitung", "sortOrder": 1}, {"id": "analyse", "label": "Analyse & Reporting", "sortOrder": 2}, {"id": "abschluss", "label": "Abschluss & Prüfung", "sortOrder": 3}, ] QUICK_ACTIONS = [ { "id": "trustee-process-receipts", "label": "Belege verarbeiten", "description": "Belege aus SharePoint importieren, klassifizieren und verbuchen", "icon": "mdi-file-document-check-outline", "color": "#4CAF50", "category": "import", "actionType": "link", "config": {"targetView": "expense-import"}, "requiredRoles": ["trustee-user", "trustee-accountant", "trustee-admin"], "sortOrder": 1, }, { "id": "trustee-sync-accounting", "label": "Daten synchronisieren", "description": "Buchhaltungsdaten aus dem externen System aktualisieren", "icon": "mdi-sync", "color": "#FF9800", "category": "import", "actionType": "link", "config": {"targetView": "settings"}, "requiredRoles": ["trustee-accountant", "trustee-admin"], "sortOrder": 2, }, { "id": "trustee-upload-receipt", "label": "Beleg hochladen", "description": "Beleg scannen oder als Datei hochladen", "icon": "mdi-camera-document-outline", "color": "#607D8B", "category": "import", "actionType": "link", "config": {"targetView": "scan-upload"}, "requiredRoles": ["trustee-user", "trustee-client", "trustee-accountant", "trustee-admin"], "sortOrder": 3, }, { "id": "trustee-budget-comparison", "label": "Budget-Vergleich", "description": "Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel", "icon": "mdi-chart-bar", "color": "#2196F3", "category": "analyse", "actionType": "link", "config": {"targetView": "analyse", "tab": "budget"}, "requiredRoles": ["trustee-accountant", "trustee-admin"], "sortOrder": 4, }, { "id": "trustee-kpi-dashboard", "label": "KPI-Dashboard", "description": "Kennzahlen berechnen und visualisieren", "icon": "mdi-view-dashboard-outline", "color": "#9C27B0", "category": "analyse", "actionType": "link", "config": {"targetView": "analyse", "tab": "kpi"}, "requiredRoles": ["trustee-accountant", "trustee-admin"], "sortOrder": 5, }, { "id": "trustee-cashflow", "label": "Cashflow-Rechnung", "description": "Cashflow berechnen und analysieren", "icon": "mdi-cash-multiple", "color": "#009688", "category": "analyse", "actionType": "link", "config": {"targetView": "analyse", "tab": "cashflow"}, "requiredRoles": ["trustee-accountant", "trustee-admin"], "sortOrder": 6, }, { "id": "trustee-forecast", "label": "Prognose erstellen", "description": "Trend-Analyse und Prognose der nächsten Monate", "icon": "mdi-chart-timeline-variant", "color": "#E91E63", "category": "analyse", "actionType": "link", "config": {"targetView": "analyse", "tab": "forecast"}, "requiredRoles": ["trustee-accountant", "trustee-admin"], "sortOrder": 7, }, { "id": "trustee-year-end-check", "label": "Jahresabschluss prüfen", "description": "Automatische Prüfungen für den Jahresabschluss", "icon": "mdi-clipboard-check-outline", "color": "#795548", "category": "abschluss", "actionType": "link", "config": {"targetView": "abschluss", "tab": "year-end"}, "requiredRoles": ["trustee-accountant", "trustee-admin"], "sortOrder": 8, }, ] # --------------------------------------------------------------------------- # Template Workflows — bootstrapped into each new feature instance. # Graphs use existing nodes: trigger.manual, trustee.refreshAccountingData, ai.prompt. # The placeholder {{featureInstanceId}} is replaced by _copyTemplateWorkflows. # --------------------------------------------------------------------------- def _buildAnalysisWorkflowGraph(prompt: str) -> Dict[str, Any]: """Build a standard analysis graph: trigger -> refreshAccountingData -> ai.prompt.""" return { "nodes": [ {"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}}, {"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten laden", "_method": "trustee", "_action": "refreshAccountingData", "parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": False}, "position": {"x": 250, "y": 0}}, {"id": "analyse", "type": "ai.prompt", "label": "Analyse", "_method": "ai", "_action": "process", "parameters": { "aiPrompt": prompt, "context": {"type": "ref", "nodeId": "refresh", "path": ["data", "accountingData"]}, "simpleMode": False, }, "position": {"x": 500, "y": 0}}, ], "connections": [ {"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0}, {"source": "refresh", "sourcePort": 0, "target": "analyse", "targetPort": 0}, ], } TEMPLATE_WORKFLOWS = [ { "id": "trustee-receipt-import", "label": "Beleg-Import Pipeline", "description": "Belege extrahieren, verarbeiten und in Buchhaltung synchronisieren", "tags": ["feature:trustee", "template:trustee-receipt-import"], "graph": { "nodes": [ {"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}}, {"id": "extract", "type": "trustee.extractFromFiles", "label": "Dokumente extrahieren", "_method": "trustee", "_action": "extractFromFiles", "parameters": {"featureInstanceId": "{{featureInstanceId}}", "prompt": ""}, "position": {"x": 250, "y": 0}}, {"id": "process", "type": "trustee.processDocuments", "label": "Verarbeiten", "_method": "trustee", "_action": "processDocuments", "parameters": {"documentList": [], "featureInstanceId": "{{featureInstanceId}}"}, "position": {"x": 500, "y": 0}}, {"id": "sync", "type": "trustee.syncToAccounting", "label": "Synchronisieren", "_method": "trustee", "_action": "syncToAccounting", "parameters": {"documentList": [], "featureInstanceId": "{{featureInstanceId}}"}, "position": {"x": 750, "y": 0}}, ], "connections": [ {"source": "trigger", "sourcePort": 0, "target": "extract", "targetPort": 0}, {"source": "extract", "sourcePort": 0, "target": "process", "targetPort": 0}, {"source": "process", "sourcePort": 0, "target": "sync", "targetPort": 0}, ], }, }, { "id": "trustee-sync-accounting", "label": "Buchhaltung synchronisieren", "description": "Buchhaltungsdaten aus dem externen System aktualisieren", "tags": ["feature:trustee", "template:trustee-sync-accounting"], "graph": { "nodes": [ {"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}}, {"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten aktualisieren", "_method": "trustee", "_action": "refreshAccountingData", "parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": True}, "position": {"x": 250, "y": 0}}, ], "connections": [ {"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0}, ], }, }, { "id": "trustee-budget-comparison", "label": "Budget-Vergleich", "description": "Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel", "tags": ["feature:trustee", "template:trustee-budget-comparison"], "graph": { "nodes": [ {"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}}, {"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten laden", "_method": "trustee", "_action": "refreshAccountingData", "parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": False}, "position": {"x": 250, "y": 0}}, {"id": "analyse", "type": "ai.prompt", "label": "Budget-Analyse", "_method": "ai", "_action": "process", "parameters": { "aiPrompt": ( "Fuehre einen Budget-Soll/Ist-Vergleich durch.\n" "Die Budget-Datei (Excel) wurde als Dokument uebergeben. " "Die aktuellen Buchhaltungsdaten sind im Kontext verfuegbar.\n" "1. Lies die Soll-Werte aus dem uebergebenen Budget-Dokument\n" "2. Vergleiche sie mit den Ist-Werten aus der Buchhaltung pro Konto\n" "3. Berechne die Abweichung (absolut und prozentual)\n" "4. Erstelle ein Abweichungs-Chart (Balkendiagramm: Soll vs. Ist pro Konto)\n" "5. Markiere kritische Abweichungen (>10%) und gib eine kurze Einschaetzung" ), "documentList": {"type": "ref", "nodeId": "trigger", "path": ["payload", "documentList"]}, "context": {"type": "ref", "nodeId": "refresh", "path": ["data", "accountingData"]}, "simpleMode": False, }, "position": {"x": 500, "y": 0}}, ], "connections": [ {"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0}, {"source": "refresh", "sourcePort": 0, "target": "analyse", "targetPort": 0}, ], }, }, { "id": "trustee-kpi-dashboard", "label": "KPI-Dashboard", "description": "Kennzahlen berechnen und visualisieren", "tags": ["feature:trustee", "template:trustee-kpi-dashboard"], "graph": _buildAnalysisWorkflowGraph( "Erstelle ein KPI-Dashboard basierend auf den aktuellen Buchhaltungsdaten. Berechne und visualisiere:\n" "1. Bruttogewinn und Bruttogewinnmarge\n" "2. EBIT (Betriebsergebnis)\n" "3. Gewinnmarge (Reingewinn / Umsatz)\n" "4. Eigenkapitalquote und Check auf hälftigen Kapitalverlust (OR Art. 725)\n" "5. Liquiditätsgrad 1-3 (Cash Ratio, Quick Ratio, Current Ratio)\n" "6. Überschuldungs-Check\n\n" "Erstelle für jede Kennzahl einen kurzen Kommentar (gut/kritisch/Handlungsbedarf). " "Erstelle mindestens 2 Charts: ein Übersichts-Chart der Margen und ein Liquiditäts-Chart." ), }, { "id": "trustee-cashflow", "label": "Cashflow-Rechnung", "description": "Cashflow berechnen und analysieren", "tags": ["feature:trustee", "template:trustee-cashflow"], "graph": _buildAnalysisWorkflowGraph( "Erstelle eine Cashflow-Rechnung basierend auf den aktuellen Buchhaltungsdaten:\n" "1. Operativer Cashflow: Starte vom Reingewinn, bereinige um nicht-cash-wirksame Positionen\n" "2. Investitions-Cashflow: Investitionen in Sachanlagen, Finanzanlagen\n" "3. Finanzierungs-Cashflow: Darlehensaufnahmen/-rückzahlungen, Dividenden, Kapitalerhöhungen\n" "4. Netto-Cashflow und Veränderung der liquiden Mittel\n\n" "Warne bei kritischen Werten. Erstelle ein Wasserfall-Chart oder gestapeltes Balkendiagramm." ), }, { "id": "trustee-forecast", "label": "Prognose erstellen", "description": "Trend-Analyse und Prognose der nächsten Monate", "tags": ["feature:trustee", "template:trustee-forecast"], "graph": _buildAnalysisWorkflowGraph( "Erstelle eine Finanzprognose basierend auf den historischen Buchhaltungsdaten:\n" "1. Analysiere die Umsatz- und Aufwandsentwicklung der letzten 6 Monate\n" "2. Identifiziere Trends und Saisonalitäten\n" "3. Prognostiziere Umsatz, Aufwand und Gewinn für die nächsten 3 Monate\n" "4. Erstelle ein Chart mit Ist-Werten und Prognose-Korridor\n" "5. Markiere Risiken\n\n" "Nutze eine einfache lineare Extrapolation mit Saisonalitätskorrektur wo sinnvoll." ), }, { "id": "trustee-year-end-check", "label": "Jahresabschluss prüfen", "description": "Automatische Prüfungen für den Jahresabschluss", "tags": ["feature:trustee", "template:trustee-year-end-check"], "graph": _buildAnalysisWorkflowGraph( "Führe eine automatische Jahresabschluss-Prüfung durch:\n" "1. Saldovalidierung: Prüfe alle Bilanzkonten auf Plausibilität\n" "2. Vorjahresvergleich: Vergleiche Bilanz- und ER-Positionen mit dem Vorjahr, markiere Abweichungen >20%\n" "3. Abgrenzungen: Identifiziere potenzielle transitorische Aktiven/Passiven\n" "4. Gesetzliche Prüfungen: Hälftiger Kapitalverlust (OR 725), Überschuldung, Mindestkapital\n" "5. MWST-Plausibilisierung: Vorsteuer vs. geschätzter Aufwand, Umsatzsteuer vs. Umsatz\n\n" "Erstelle eine Checkliste mit Status (OK / Warnung / Kritisch) pro Prüfpunkt." ), }, ] TEMPLATE_ROLES = [ { "roleLabel": "trustee-viewer", "description": "Treuhand-Betrachter - Treuhand-Daten einsehen (nur lesen)", "accessRules": [ {"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True}, {"context": "UI", "item": "ui.feature.trustee.positions", "view": True}, {"context": "UI", "item": "ui.feature.trustee.documents", "view": True}, {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True}, {"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"}, ], }, { "roleLabel": "trustee-user", "description": "Treuhand-Benutzer - Eigene Treuhand-Daten erstellen und verwalten", "accessRules": [ {"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True}, {"context": "UI", "item": "ui.feature.trustee.positions", "view": True}, {"context": "UI", "item": "ui.feature.trustee.documents", "view": True}, {"context": "UI", "item": "ui.feature.trustee.expense-import", "view": True}, {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True}, {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True}, {"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"}, ], }, { "roleLabel": "trustee-admin", "description": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen", "accessRules": [ {"context": "UI", "item": None, "view": True}, {"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"}, {"context": "RESOURCE", "item": "resource.feature.trustee.instance-roles.manage", "view": True}, {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True}, {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True}, {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.manage", "view": True}, ], }, { "roleLabel": "trustee-accountant", "description": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten", "accessRules": [ {"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True}, {"context": "UI", "item": "ui.feature.trustee.positions", "view": True}, {"context": "UI", "item": "ui.feature.trustee.documents", "view": True}, {"context": "UI", "item": "ui.feature.trustee.analyse", "view": True}, {"context": "UI", "item": "ui.feature.trustee.abschluss", "view": True}, {"context": "UI", "item": "ui.feature.trustee.settings", "view": True}, {"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"}, {"context": "RESOURCE", "item": "resource.feature.trustee.accounting.sync", "view": True}, {"context": "RESOURCE", "item": "resource.feature.trustee.accounting.view", "view": True}, {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True}, {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True}, ], }, { "roleLabel": "trustee-client", "description": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen", "accessRules": [ {"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True}, {"context": "UI", "item": "ui.feature.trustee.positions", "view": True}, {"context": "UI", "item": "ui.feature.trustee.documents", "view": True}, {"context": "UI", "item": "ui.feature.trustee.expense-import", "view": True}, {"context": "UI", "item": "ui.feature.trustee.scan-upload", "view": True}, {"context": "DATA", "item": "data.feature.trustee.TrusteePosition", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"}, {"context": "DATA", "item": "data.feature.trustee.TrusteeDocument", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"}, ], }, ] def getFeatureDefinition() -> Dict[str, Any]: """Return the feature definition for registration.""" return { "code": FEATURE_CODE, "label": FEATURE_LABEL, "icon": FEATURE_ICON } def getUiObjects() -> List[Dict[str, Any]]: """Return UI objects for RBAC catalog registration.""" return UI_OBJECTS def getResourceObjects() -> List[Dict[str, Any]]: """Return resource objects for RBAC catalog registration.""" return RESOURCE_OBJECTS def getTemplateRoles() -> List[Dict[str, Any]]: """Return template roles for this feature.""" return TEMPLATE_ROLES def getTemplateWorkflows() -> List[Dict[str, Any]]: """Return template workflow definitions for bootstrap on instance creation.""" return TEMPLATE_WORKFLOWS def getQuickActions() -> List[Dict[str, Any]]: """Return quick action definitions for the Trustee dashboard.""" return QUICK_ACTIONS def getQuickActionCategories() -> List[Dict[str, Any]]: """Return quick action category definitions.""" return QUICK_ACTION_CATEGORIES def getDataObjects() -> List[Dict[str, Any]]: """Return DATA objects for RBAC catalog registration.""" return DATA_OBJECTS def registerFeature(catalogService) -> bool: """ Register this feature's RBAC objects in the catalog. Args: catalogService: The RBAC catalog service instance Returns: True if registration was successful """ try: # Register UI objects for uiObj in UI_OBJECTS: catalogService.registerUiObject( featureCode=FEATURE_CODE, objectKey=uiObj["objectKey"], label=uiObj["label"], meta=uiObj.get("meta") ) # Register Resource objects for resObj in RESOURCE_OBJECTS: catalogService.registerResourceObject( featureCode=FEATURE_CODE, objectKey=resObj["objectKey"], label=resObj["label"], meta=resObj.get("meta") ) # Register DATA objects (tables/entities) for dataObj in DATA_OBJECTS: catalogService.registerDataObject( featureCode=FEATURE_CODE, objectKey=dataObj["objectKey"], label=dataObj["label"], meta=dataObj.get("meta") ) # Sync template roles to database (with AccessRules) _syncTemplateRolesToDb() logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI, {len(RESOURCE_OBJECTS)} resource, {len(DATA_OBJECTS)} data objects") return True except Exception as e: logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}") return False def _syncTemplateRolesToDb() -> int: """ Sync template roles and their AccessRules to the database. Creates global template roles (mandateId=None) if they don't exist. Returns: Number of roles created/updated """ try: from modules.interfaces.interfaceDbApp import getRootInterface from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext from modules.datamodels.datamodelUtils import coerce_text_multilingual rootInterface = getRootInterface() # Get existing template roles for this feature (Pydantic models) existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE) templateRoles = [r for r in existingRoles if r.mandateId is None] existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles} createdCount = 0 for roleTemplate in TEMPLATE_ROLES: roleLabel = roleTemplate["roleLabel"] if roleLabel in existingRoleLabels: roleId = existingRoleLabels[roleLabel] # Ensure AccessRules exist for this role _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", [])) else: # Create new template role newRole = Role( roleLabel=roleLabel, description=coerce_text_multilingual(roleTemplate.get("description", {})), featureCode=FEATURE_CODE, mandateId=None, # Global template featureInstanceId=None, isSystemRole=False ) createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump()) roleId = createdRole.get("id") # Create AccessRules for this role _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", [])) logger.info(f"Created template role '{roleLabel}' with ID {roleId}") createdCount += 1 if createdCount > 0: logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles") # Repair instance-specific roles that are missing AccessRules _repairInstanceRolesAccessRules(rootInterface, existingRoleLabels) return createdCount except Exception as e: logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}") return 0 def _repairInstanceRolesAccessRules(rootInterface, templateRoleLabels: Dict[str, str]) -> int: """ Repair instance-specific roles by copying AccessRules from their template roles. This ensures instance roles created before AccessRules were defined get updated. Args: rootInterface: Root interface instance templateRoleLabels: Dict mapping roleLabel to template role ID Returns: Number of instance roles repaired """ from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext repairedCount = 0 # Get all instance-specific roles for this feature (Pydantic models) allRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE) instanceRoles = [r for r in allRoles if r.mandateId is not None] for instanceRole in instanceRoles: roleLabel = instanceRole.roleLabel instanceRoleId = str(instanceRole.id) # Find matching template role templateRoleId = templateRoleLabels.get(roleLabel) if not templateRoleId: continue # Check if instance role has AccessRules (Pydantic models) existingRules = rootInterface.getAccessRulesByRole(instanceRoleId) if existingRules: continue # Already has rules, skip # Copy AccessRules from template role (Pydantic models) templateRules = rootInterface.getAccessRulesByRole(templateRoleId) if not templateRules: continue # Template has no rules for rule in templateRules: newRule = AccessRule( roleId=instanceRoleId, context=rule.context, item=rule.item, view=rule.view if rule.view else False, read=rule.read, create=rule.create, update=rule.update, delete=rule.delete, ) rootInterface.db.recordCreate(AccessRule, newRule.model_dump()) logger.info(f"Repaired instance role '{roleLabel}' (ID: {instanceRoleId}): copied {len(templateRules)} AccessRules from template") repairedCount += 1 if repairedCount > 0: logger.info(f"Feature '{FEATURE_CODE}': Repaired {repairedCount} instance roles with missing AccessRules") return repairedCount def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int: """ Ensure AccessRules exist for a role based on templates. Args: rootInterface: Root interface instance roleId: Role ID ruleTemplates: List of rule templates Returns: Number of rules created """ from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext # Get existing rules for this role (Pydantic models) existingRules = rootInterface.getAccessRulesByRole(roleId) # Create a set of existing rule signatures to avoid duplicates # IMPORTANT: Use .value for enum comparison, not str() which gives "AccessRuleContext.DATA" in Python 3.11+ existingSignatures = set() for rule in existingRules: sig = (rule.context.value if rule.context else None, rule.item) existingSignatures.add(sig) createdCount = 0 for template in ruleTemplates: context = template.get("context", "UI") item = template.get("item") sig = (context, item) if sig in existingSignatures: continue # Map context string to enum if context == "UI": contextEnum = AccessRuleContext.UI elif context == "DATA": contextEnum = AccessRuleContext.DATA elif context == "RESOURCE": contextEnum = AccessRuleContext.RESOURCE else: contextEnum = context newRule = AccessRule( roleId=roleId, context=contextEnum, item=item, view=template.get("view", False), read=template.get("read"), create=template.get("create"), update=template.get("update"), delete=template.get("delete"), ) rootInterface.db.recordCreate(AccessRule, newRule.model_dump()) createdCount += 1 if createdCount > 0: logger.debug(f"Created {createdCount} AccessRules for role {roleId}") return createdCount