# Trustee Feature - Architektur und Implementierungsplan ## Executive Summary Dieses Dokument beschreibt die Architektur und den Implementierungsplan für das **Trustee** Feature, welches das erste Feature in einem neuen Client-Management-System ist. Das Trustee Feature ermöglicht es Treuhandgesellschaften, die Spesenabrechnung und zugehörige Dokumente für ihre Kunden zentral zu verwalten. ## Inhaltsverzeichnis 1. [Übersicht](#overview) 2. [Architektur-Analyse](#architecture-analysis) 3. [Architektur-Entscheidungen](#architecture-decisions) 4. [System-Architektur](#system-architecture) 5. [Datenmodell](#data-model) 6. [RBAC-Integration](#rbac-integration) 7. [API-Design](#api-design) 8. [Implementierungsplan](#implementation-plan) 9. [Migrationsstrategie](#migration-strategy) 10. [Teststrategie](#testing-strategy) 11. [Implementierungsdetails](#implementation-details) **Zugehörige Dokumente**: - [UI-Spezifikation](./doc_trustee_feature_ui_specification.md) - Frontend-Implementierung (UI-Komponenten, FormGenerator-Pattern, React/TypeScript) --- ## Übersicht ### Zweck Das Trustee Feature bietet ein zentralisiertes Spesenabrechnungssystem, bei dem: - Benutzer ihre Spesen an ihre Treuhandgesellschaft melden können - Treuhandgesellschaften Spesendaten zentral für alle ihre Kunden verwalten können - Der Zugriff über ein feature-basiertes RBAC-System gesteuert wird ### Schlüsselkonzepte 1. **Feature-basiertes System**: Das System definiert Zugriffe für Features. "Trustee" ist das erste Feature. 2. **Datenbank-Interface**: Jedes Feature hat ein Datenbank-Interface (wie "chat") mit automatisierten Systemattributen (mandate, created, updated, etc.) 3. **RBAC-Integration**: Feature-Zugriff wird über RBAC-Ressourcen gesteuert 4. **Organisations-basierter Zugriff**: Der Zugriff wird auf Organisationsebene innerhalb des Trustee Features gesteuert --- ## Architektur-Analyse ### Verständnis des aktuellen Systems Basierend auf der Codebase-Analyse: 1. **Interface Pattern**: Custom interfaces follow the pattern of `interfaceDbChatObjects.py`: - Use `DatabaseConnector` for database operations - Implement CRUD operations - Integrate with RBAC through `interfaceRbac.py` - Support standard attributes (mandate, _createdAt, _modifiedAt, _createdBy, _modifiedBy) - automatisch vom DatabaseConnector gesetzt 2. **RBAC System**: - Uses `AccessRuleContext` enum: DATA, UI, RESOURCE - Access levels: ALL, MY, GROUP, NONE - Resources are defined as cascading strings (e.g., "ai.model.anthropic") - UI elements use cascading strings (e.g., "playground.voice.settings") - **Filterung auf DB-Ebene**: `getRecordsetWithRBAC()` filtert Daten automatisch basierend auf: - `ALL`: Keine Filterung (alle Records) - `MY`: Filter nach `_createdBy = userId` (nur eigene Records) - `GROUP`: Filter nach `mandateId = user.mandateId` (nur Records der eigenen Gruppe) - `NONE`: Keine Records sichtbar (`1 = 0` WHERE-Clause) - **View-Permission**: Wird zuerst geprüft - wenn `view=false`, werden keine Records zurückgegeben 3. **Route Pattern**: Routes follow `routeData*.py` pattern: - Use FastAPI routers - Integrate with authentication via `getCurrentUser` - Support pagination - Use interface methods for data access 4. **Data Model Pattern**: Models in `datamodels/datamodel*.py`: - Use Pydantic BaseModel - Register labels with `registerModelLabels` - Include frontend metadata in `json_schema_extra` 5. **DatabaseConnector Pattern**: - **Automatische Tabellenerstellung**: `_ensureTableExists()` erstellt Tabellen automatisch aus Pydantic-Modellen - **Automatische Spaltenerstellung**: Spalten werden aus Modell-Feldern generiert - **Systemattribute**: Automatisch hinzugefügt: `_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy` - **Index-Erstellung**: Automatische Indizes für Foreign Keys (Felder die auf `Id` enden) - **Migration**: Fehlende Spalten werden automatisch hinzugefügt (additive Migration) - **SQL-Typ-Mapping**: Automatische Konvertierung von Python-Typen zu PostgreSQL-Typen ### Architektur-Entscheidungen 1. **Feature-Registrierung**: Das Trustee Feature wird als neues Interface ähnlich wie Chat registriert 2. **RBAC-Ressourcen**: - `ui.trustee` - UI-Zugriffskontrolle - `resource.trustee` - Feature-Ressourcen-Zugriff 3. **Datenbank-Isolation**: Jedes Feature hat sein eigenes Datenbank-Interface, teilt aber dieselbe Datenbankinstanz 4. **Organisation vs Mandate**: - **Mandate**: System-Level-Organisation (bestehendes Konzept) - **Organisation**: Trustee-Feature-spezifische Firma (neues Konzept innerhalb des Trustee Features) - Dies sind separate Konzepte - Organisation ist auf das Trustee Feature beschränkt - **Beziehung**: - Eine `mandate` kann mehrere `organisationId`s haben - Eine `organisationId` gehört zu genau einer `mandate` - `mandate` wird automatisch aus `currentUser.mandateId` gesetzt - `organisationId` Dropdown zeigt alle gelieferten Organisationen (RBAC filtert automatisch) - `organisationId`s können nicht über `mandate`-Grenzen hinweg geteilt werden --- ## System-Architektur ### Komponenten-Übersicht ``` ┌─────────────────────────────────────────────────────────────┐ │ Frontend (React/TS) │ │ - Trustee UI Components │ │ - Organisation Management │ │ - Contract Management │ │ - Expense Reporting (Position + Document) │ └──────────────────────┬──────────────────────────────────────┘ │ │ HTTP/REST API │ ┌──────────────────────▼──────────────────────────────────────┐ │ API Routes Layer │ │ - routeDataTrusteeOrganisations.py │ │ - routeDataTrusteeRoles.py │ │ - routeDataTrusteeAccess.py │ │ - routeDataTrusteeContracts.py │ │ - routeDataTrusteeDocuments.py │ │ - routeDataTrusteePositions.py │ │ - routeDataTrusteePositionDocuments.py │ └──────────────────────┬──────────────────────────────────────┘ │ │ Interface Methods │ ┌──────────────────────▼──────────────────────────────────────┐ │ Interface Layer (interfaceDbTrusteeObjects) │ │ - CRUD Operations │ │ - RBAC Filtering │ │ - Business Logic │ └──────────────────────┬──────────────────────────────────────┘ │ │ Database Connector │ ┌──────────────────────▼──────────────────────────────────────┐ │ Database Layer (PostgreSQL) │ │ - trustee.organisation │ │ - trustee.role │ │ - trustee.access │ │ - trustee.contract │ │ - trustee.document │ │ - trustee.position │ │ - trustee.xpositiondocument │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ RBAC System │ │ - resource.trustee (feature access) │ │ - ui.trustee (UI access) │ │ - Table-level access (DATA context) │ │ - Feature-level access (trustee.access table) │ └─────────────────────────────────────────────────────────────┘ ``` ### Interface-Struktur Das Trustee-Interface folgt dem Muster von `interfaceDbChatObjects.py`: ```python # gateway/modules/interfaces/interfaceDbTrusteeObjects.py class TrusteeInterface: """ Interface for Trustee feature database operations. Similar to ChatInterface but for trustee-specific data. """ def __init__(self, currentUser: User): self.currentUser = currentUser self.db = DatabaseConnector(...) # Trustee database self.rbac = RbacClass(self.db, dbApp=dbApp) # Organisation CRUD def createOrganisation(...) def getOrganisation(...) def updateOrganisation(...) def deleteOrganisation(...) def getAllOrganisations(...) # Role CRUD def createRole(...) def getAllRoles(...) # Access CRUD def createAccess(...) def getUserAccessForOrganisation(...) def checkUserPermission(...) # Contract CRUD def createContract(...) def getContract(...) # ... etc # Document CRUD def createDocument(...) def getDocument(...) def getDocumentData(...) # Binary data # Position CRUD def createPosition(...) def getPosition(...) # ... etc # Cross-reference operations def linkPositionDocument(...) def unlinkPositionDocument(...) def getDocumentsForPosition(...) def getPositionsForDocument(...) ``` --- ## Datenmodell ### Tabelle: `trustee.organisation` Repräsentiert Treuhandgesellschaften (Organisationen) innerhalb des Trustee Features. ```python class TrusteeOrganisation(BaseModel): id: str = Field( # Unique string label (PK), not UUID description="Unique organisation identifier (label)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, # Änderbar bei Erstellung, danach readonly "frontend_required": True } ) label: str = Field( description="Company name", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": True } ) enabled: bool = Field( default=True, description="Whether the organisation is enabled", json_schema_extra={ "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False } ) mandate: str = Field( # System attribute description="Mandate ID (system-level organisation)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # Systemattribute werden automatisch vom DatabaseConnector gesetzt: # _createdAt, _modifiedAt, _createdBy, _modifiedBy # Diese werden nicht im Modell definiert, aber im Backend automatisch verwaltet # Systemattribute-Verwaltung: # - _createdBy: Automatisch aus currentUser.id gesetzt # - _createdAt: Automatisch beim Erstellen gesetzt (float, UTC timestamp in Sekunden) # - _modifiedAt: Automatisch beim Update gesetzt (float, UTC timestamp in Sekunden) # - _modifiedBy: Automatisch aus currentUser.id beim Update gesetzt # - Frontend: Diese Felder werden als readonly angezeigt # - Formatierung: Timestamps als float (UI rendert gemäß Zeitzoneneinstellungen), User-Namen statt User-ID # - Sichtbarkeit: Kann in FormGeneric definiert werden, welche Felder angezeigt werden, aber sie müssen ans UI geliefert werden über die Routes registerModelLabels( "TrusteeOrganisation", {"en": "Organisation", "fr": "Organisation"}, { "id": {"en": "ID", "fr": "ID"}, "label": {"en": "Label", "fr": "Libellé"}, "enabled": {"en": "Enabled", "fr": "Activé"}, "mandate": {"en": "Mandate", "fr": "Mandat"}, }, ) ``` **RBAC Rules**: - `sysadmin`: Can manage all organisations - `admin`: Can manage organisations for their group (mandate) **Indexes**: - Primary key on `id` - Index on `mandate` for filtering ### Tabelle: `trustee.role` Definiert Rollen innerhalb des Trustee Features. ```python class TrusteeRole(BaseModel): id: str = Field( # Unique string label (PK), not UUID description="Unique role identifier (label)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": True } ) desc: str = Field( description="Role description", json_schema_extra={ "frontend_type": "textarea", "frontend_readonly": False, "frontend_required": True } ) mandate: str = Field( # System attribute description="Mandate ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # Systemattribute werden automatisch vom DatabaseConnector gesetzt registerModelLabels( "TrusteeRole", {"en": "Role", "fr": "Rôle"}, { "id": {"en": "ID", "fr": "ID"}, "desc": {"en": "Description", "fr": "Description"}, "mandate": {"en": "Mandate", "fr": "Mandat"}, }, ) ``` **Initiale Rollen**: - `userreport`: Kann Benutzerdokumente an das System liefern - `admin`: Kann den Zugriff administrieren - `operate`: Kann Daten für Operationen verwenden **RBAC Rules**: - `sysadmin`: Can manage all roles **Indexes**: - Primary key on `id` - Index on `mandate` for filtering ### Tabelle: `trustee.access` Definiert Benutzerzugriff auf Organisationen mit spezifischen Rollen. Der Zugriff kann optional auf einen spezifischen Contract beschränkt werden. ```python class TrusteeAccess(BaseModel): id: str = Field( # UUID PK default_factory=lambda: str(uuid.uuid4()), json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) organisationId: str = Field( description="Reference to trustee.organisation.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.organisation" # String-Referenz für dynamische Options } ) roleId: str = Field( description="Reference to trustee.role.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.role" # String-Referenz für dynamische Options } ) userId: str = Field( description="User ID assigned to this role", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "user" # String-Referenz für User-Liste } ) contractId: Optional[str] = Field( default=None, description="Optional reference to trustee.contract.id. If None, access is for full organisation. If set, access is limited to this specific contract.", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": "trustee.contract", # String-Referenz, dynamisch gefiltert nach organisationId "frontend_depends_on": "organisationId" # Dropdown wird aktualisiert wenn organisationId geändert wird } ) mandate: str = Field( # System attribute description="Mandate ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # Systemattribute werden automatisch vom DatabaseConnector gesetzt registerModelLabels( "TrusteeAccess", {"en": "Access", "fr": "Accès"}, { "id": {"en": "ID", "fr": "ID"}, "organisationId": {"en": "Organisation", "fr": "Organisation"}, "roleId": {"en": "Role", "fr": "Rôle"}, "userId": {"en": "User", "fr": "Utilisateur"}, "contractId": {"en": "Contract (optional)", "fr": "Contrat (optionnel)"}, "mandate": {"en": "Mandate", "fr": "Mandat"}, }, ) ``` **Zugriffslogik**: - **Ohne `contractId` (None)**: Zugriff gilt für die gesamte Organisation - **Mit `contractId`**: Zugriff ist auf diesen spezifischen Contract beschränkt - **RBAC-Filterung**: Bei der Datenfilterung wird geprüft: - Hat User Access für Organisation (ohne Contract) → Zugriff auf alle Contracts dieser Organisation - Hat User Access für Organisation + spezifischen Contract → Zugriff nur auf diesen Contract **RBAC Rules**: - `sysadmin`: Can manage all access records - `admin`: Can manage access for their group (mandate) - Users with `admin` role in trustee.access can manage access for their organisation **Indexes**: - Primary key on `id` - Unique constraint on `(organisationId, roleId, userId, contractId)` to prevent duplicates (erlaubt aber mehrere Rollen pro Benutzer-Organisation durch separate Records) - Index on `organisationId` - Index on `userId` - Index on `roleId` - Index on `contractId` - Index on `mandate` ### Tabelle: `trustee.contract` Definiert Kundenverträge innerhalb von Organisationen. ```python class TrusteeContract(BaseModel): id: str = Field( # UUID PK default_factory=lambda: str(uuid.uuid4()), json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) organisationId: str = Field( description="Reference to trustee.organisation.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, # Änderbar bei Erstellung, danach readonly (immutable) "frontend_required": True, "frontend_options": "trustee.organisation" } ) label: str = Field( description="Label for the customer contract (e.g., 'Muster AG 2026')", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": True } ) enabled: bool = Field( default=True, description="Whether the contract is enabled", json_schema_extra={ "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False } ) mandate: str = Field( # System attribute description="Mandate ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # Systemattribute werden automatisch vom DatabaseConnector gesetzt registerModelLabels( "TrusteeContract", {"en": "Contract", "fr": "Contrat"}, { "id": {"en": "ID", "fr": "ID"}, "organisationId": {"en": "Organisation", "fr": "Organisation"}, "label": {"en": "Label", "fr": "Libellé"}, "enabled": {"en": "Enabled", "fr": "Activé"}, "mandate": {"en": "Mandate", "fr": "Mandat"}, }, ) ``` **RBAC Rules**: - `sysadmin`: Can manage all contracts - `admin`: Can manage contracts for their group (mandate) - Users with `admin` role in trustee.access: Can CRUD contracts for their organisationId - Wenn `contractId` in trustee.access leer: Zugriff auf alle Contracts der Organisation - Wenn `contractId` gesetzt: Zugriff nur auf diesen spezifischen Contract - New records default to their own organisationId - Dropdown to select from granted organisationIds (und optional Contracts) **Wichtig**: Verträge sind unveränderlich bezüglich `organisationId` - kann nach der Erstellung nicht mehr geändert werden. **Implementierung**: - **Backend-Validierung**: In `updateContract()` prüfen: Wenn `organisationId` im Update vorhanden und unterschiedlich zum bestehenden Wert → Fehler - **Frontend-Readonly**: `organisationId` wird auf readonly gesetzt wenn `id` vorhanden (non-blank) ist - **Logik**: ID kann nur gespeichert werden, wenn non-blank. Eine non-blank ID kann nicht mehr geändert werden **Indexes**: - Primary key on `id` - Index on `organisationId` - Index on `mandate` - **Wichtig**: `organisationId` ist immutable nach Erstellung - keine Updates erlaubt **Validierung**: - `id` Format: Alphanumerisch + Bindestrich/Unterstrich - Länge: 3-50 Zeichen - Validierung: Backend + Frontend ### Tabelle: `trustee.document` Enthält Dokumentreferenzen und Belege für Buchungen. ```python class TrusteeDocument(BaseModel): id: str = Field( # UUID PK default_factory=lambda: str(uuid.uuid4()), json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) organisationId: str = Field( description="Reference to trustee.organisation.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.organisation" } ) contractId: str = Field( description="Reference to trustee.contract.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.contract" # Gefiltert nach organisationId } ) documentData: bytes = Field( # Binary data description="The file content", json_schema_extra={ "frontend_type": "file", # Für File Upload "frontend_readonly": False, "frontend_required": True } ) documentName: str = Field( description="File name (e.g., 'Beleg.pdf')", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": True } ) documentMimeType: str = Field( description="MIME type of the document", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": [ {"value": "application/pdf", "label": {"en": "PDF", "fr": "PDF"}}, {"value": "image/jpeg", "label": {"en": "JPEG", "fr": "JPEG"}}, {"value": "image/png", "label": {"en": "PNG", "fr": "PNG"}}, # ... weitere MIME-Types ] } ) mandate: str = Field( # System attribute description="Mandate ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # Systemattribute werden automatisch vom DatabaseConnector gesetzt registerModelLabels( "TrusteeDocument", {"en": "Document", "fr": "Document"}, { "id": {"en": "ID", "fr": "ID"}, "organisationId": {"en": "Organisation", "fr": "Organisation"}, "contractId": {"en": "Contract", "fr": "Contrat"}, "documentData": {"en": "Document Data", "fr": "Données du document"}, "documentName": {"en": "Document Name", "fr": "Nom du document"}, "documentMimeType": {"en": "MIME Type", "fr": "Type MIME"}, "mandate": {"en": "Mandate", "fr": "Mandat"}, }, ) ``` **RBAC Rules**: - `sysadmin`: Can manage all documents - `admin`: Can manage documents for their group (mandate) - Users with `operate` role in trustee.access: Can CRUD documents in their organisationId - Users with `userreport` role in trustee.access: Can CRUD own documents (_createdBy = userId, automatisch über Systemattribute gesetzt) **Speicherung**: - Dokument-Binärdaten werden in PostgreSQL BYTEA-Spalte gespeichert - Einfach, transaktional, einfaches Backup **File Upload/Download**: - **Nicht direkt integriert**: File Upload/Download erfolgt über das Workflow-System mit einer Action - Die Action erstellt automatisch die Datensätze in `trustee.document` - Dies ist nicht Teil der direkten Trustee-Feature-Implementierung **Indexes**: - Primary key on `id` - Index on `organisationId` - Index on `contractId` - Index on `mandate` - Index on `_created_at` (für userreport Filterung) - Index auf `_created_by` (automatisch über Systemattribute, für userreport Filterung) ### Tabelle: `trustee.position` Enthält Buchungspositionen (Speseneinträge). ```python class TrusteePosition(BaseModel): id: str = Field( # UUID PK default_factory=lambda: str(uuid.uuid4()), json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) organisationId: str = Field( description="Reference to trustee.organisation.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.organisation" } ) contractId: str = Field( description="Reference to trustee.contract.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.contract" # Gefiltert nach organisationId } ) valuta: date = Field( description="Value date", json_schema_extra={ "frontend_type": "date", "frontend_readonly": False, "frontend_required": True } ) transactionDateTime: datetime = Field( description="Transaction timestamp", json_schema_extra={ "frontend_type": "timestamp", "frontend_readonly": False, "frontend_required": True } ) company: str = Field( default="", description="Company name", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": False } ) desc: str = Field( default="", description="Description", json_schema_extra={ "frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False } ) tags: str = Field( default="", description="Tags (comma-separated or JSON)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": False } ) bookingCurrency: str = Field( description="Booking currency code", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": [ {"value": "CHF", "label": {"en": "CHF", "fr": "CHF"}}, {"value": "EUR", "label": {"en": "EUR", "fr": "EUR"}}, {"value": "USD", "label": {"en": "USD", "fr": "USD"}}, # ... weitere Währungen ] } ) bookingAmount: float = Field( description="Booking amount", json_schema_extra={ "frontend_type": "number", "frontend_readonly": False, "frontend_required": True } ) originalCurrency: str = Field( description="Original currency code", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": [ {"value": "CHF", "label": {"en": "CHF", "fr": "CHF"}}, {"value": "EUR", "label": {"en": "EUR", "fr": "EUR"}}, {"value": "USD", "label": {"en": "USD", "fr": "USD"}}, ] } ) originalAmount: float = Field( description="Original amount (manuelle Eingabe in Phase 1, keine automatische Währungsumrechnung)", json_schema_extra={ "frontend_type": "number", "frontend_readonly": False, "frontend_required": True } ) vatPercentage: float = Field( default=0.0, description="VAT percentage", json_schema_extra={ "frontend_type": "number", "frontend_readonly": False, "frontend_required": False } ) vatAmount: float = Field( default=0.0, description="VAT amount (wird automatisch berechnet: bookingAmount * vatPercentage / 100, kann manuell überschrieben werden)", json_schema_extra={ "frontend_type": "number", "frontend_readonly": False, # Editierbar für manuelle Überschreibung "frontend_required": False } ) # MwSt-Berechnung: # - Automatisch beim Ändern von bookingAmount oder vatPercentage # - Wenn vatAmount manuell geändert wird, wird automatische Berechnung erneut durchgeführt # - Warnung (Toast) erscheint bereits beim Ändern, nicht erst beim Speichern mandate: str = Field( # System attribute description="Mandate ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # Systemattribute werden automatisch vom DatabaseConnector gesetzt registerModelLabels( "TrusteePosition", {"en": "Position", "fr": "Position"}, { "id": {"en": "ID", "fr": "ID"}, "organisationId": {"en": "Organisation", "fr": "Organisation"}, "contractId": {"en": "Contract", "fr": "Contrat"}, "valuta": {"en": "Value Date", "fr": "Date de valeur"}, "transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction"}, "company": {"en": "Company", "fr": "Entreprise"}, "desc": {"en": "Description", "fr": "Description"}, "tags": {"en": "Tags", "fr": "Tags"}, "bookingCurrency": {"en": "Booking Currency", "fr": "Devise de comptabilisation"}, "bookingAmount": {"en": "Booking Amount", "fr": "Montant de comptabilisation"}, "originalCurrency": {"en": "Original Currency", "fr": "Devise d'origine"}, "originalAmount": {"en": "Original Amount", "fr": "Montant d'origine"}, "vatPercentage": {"en": "VAT Percentage", "fr": "Pourcentage TVA"}, "vatAmount": {"en": "VAT Amount", "fr": "Montant TVA"}, "mandate": {"en": "Mandate", "fr": "Mandat"}, }, ) ``` **RBAC Rules**: - `sysadmin`: Can manage all positions - `admin`: Can manage positions for their group (mandate) - Users with `operate` role in trustee.access: Can CRUD positions in their organisationId - Wenn `contractId` in trustee.access leer: Zugriff auf alle Positions der Organisation - Wenn `contractId` gesetzt: Zugriff nur auf Positions dieses Contracts - Users with `userreport` role in trustee.access: Can CRUD own positions (_createdBy = userId, automatisch über Systemattribute gesetzt) - Contract-Filterung gilt auch für userreport (nur eigene Positions in erlaubten Contracts) **Indexes**: - Primary key on `id` - Index on `organisationId` - Index on `contractId` - Index on `valuta` (for date-based queries) - Index on `transactionDateTime` - Index on `mandate` - Index auf `_createdBy` (automatisch über Systemattribute, für userreport Filterung) ### Tabelle: `trustee.xpositiondocument` Verknüpfungstabelle, die Positionen mit Dokumenten verknüpft (viele-zu-viele). ```python class TrusteePositionDocument(BaseModel): id: str = Field( # UUID PK default_factory=lambda: str(uuid.uuid4()), json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) organisationId: str = Field( description="Reference to trustee.organisation.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.organisation" } ) contractId: str = Field( description="Reference to trustee.contract.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.contract" } ) documentId: str = Field( description="Reference to trustee.document.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.document" # Gefiltert nach organisationId/contractId } ) positionId: str = Field( description="Reference to trustee.position.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "trustee.position" # Gefiltert nach organisationId/contractId } ) mandate: str = Field( # System attribute description="Mandate ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # Systemattribute werden automatisch vom DatabaseConnector gesetzt registerModelLabels( "TrusteePositionDocument", {"en": "Position-Document Link", "fr": "Lien Position-Document"}, { "id": {"en": "ID", "fr": "ID"}, "organisationId": {"en": "Organisation", "fr": "Organisation"}, "contractId": {"en": "Contract", "fr": "Contrat"}, "documentId": {"en": "Document", "fr": "Document"}, "positionId": {"en": "Position", "fr": "Position"}, "mandate": {"en": "Mandate", "fr": "Mandat"}, }, ) ``` <|tool▁calls▁begin|><|tool▁call▁begin|> read_file **RBAC Rules**: - `sysadmin`: Can manage all cross-references - `admin`: Can manage cross-references for their group (mandate) - Users with `operate` role in trustee.access: Can CRUD cross-references in their organisationId - Wenn `contractId` in trustee.access leer: Zugriff auf alle Cross-References der Organisation - Wenn `contractId` gesetzt: Zugriff nur auf Cross-References dieses Contracts - Users with `userreport` role in trustee.access: Can CRUD cross-references for their own positions/documents (createdBy = userId, automatisch über Systemattribute gesetzt) - Contract-Filterung gilt auch für userreport (nur eigene Cross-References in erlaubten Contracts) **Wichtig**: Verknüpfungen sind optional - Positionen können ohne Dokumente existieren, Dokumente können ohne Positionen existieren. **Indexes**: - Primary key on `id` - Unique constraint on `(positionId, documentId)` to prevent duplicates - Index on `organisationId` - Index on `contractId` - Index on `documentId` - Index on `positionId` - Index on `mandate` - Index auf `_created_by` (automatisch über Systemattribute, für userreport Filterung) --- ## RBAC-Integration ### System-Level RBAC Das Trustee Feature integriert sich auf mehreren Ebenen mit dem bestehenden RBAC-System: #### 1. Feature Access (`resource.trustee`) **AccessRule**: ```python AccessRule( roleLabel="admin", # or appropriate system role context=AccessRuleContext.RESOURCE, item="trustee", view=True, # No read/create/update/delete for RESOURCE context ) ``` **Zweck**: Steuert, ob ein Benutzer überhaupt auf das Trustee Feature zugreifen kann. #### 2. UI Access (`ui.trustee`) **AccessRule**: ```python AccessRule( roleLabel="admin", context=AccessRuleContext.UI, item="trustee", view=True, ) ``` **Zweck**: Steuert die UI-Sichtbarkeit für das Trustee Feature. #### 3. Table-Level Access (DATA context) Each trustee table needs DATA context access rules: ```python # Example for trustee.organisation AccessRule( roleLabel="admin", context=AccessRuleContext.DATA, item="trustee.organisation", view=True, read=AccessLevel.GROUP, # Can read group records create=AccessLevel.GROUP, update=AccessLevel.GROUP, delete=AccessLevel.GROUP, ) ``` ### Feature-Level RBAC (trustee.access Tabelle) Die `trustee.access` Tabelle erweitert System-RBAC mit Feature-spezifischen Berechtigungen: 1. **Rollen-basierter Zugriff**: Benutzern werden Rollen (`userreport`, `admin`, `operate`) für spezifische Organisationen zugewiesen 2. **Contract-basierter Zugriff (optional)**: - Wenn `contractId` nicht gesetzt (None): Zugriff gilt für die gesamte Organisation - Wenn `contractId` gesetzt: Zugriff ist auf diesen spezifischen Contract beschränkt 3. **Berechtigungsprüfung**: Bei der Durchführung von Operationen prüfen: - System RBAC (Feature-Zugriff, Tabellen-Zugriff) - Feature RBAC (trustee.access Tabelle für Organisation, Rolle und optional Contract) ### Permission Checking Logic ```python def checkTrusteePermission( user: User, organisationId: str, operation: str, # 'read', 'create', 'update', 'delete' resource: str, # 'contract', 'document', 'position', etc. recordId: Optional[str] = None, contractId: Optional[str] = None # Optional: für Contract-basierte Filterung ) -> bool: """ Check if user has permission for trustee operation. Steps: 1. Check system RBAC: resource.trustee access 2. Check system RBAC: table-level access (trustee.{resource}) 3. Check feature RBAC: trustee.access table - Get user's roles for organisationId (und optional contractId) - Check if role allows operation on resource - Wenn contractId angegeben: Prüfe ob User Access für diesen Contract hat """ # Step 1: Feature access if not checkSystemRbac(user, "resource.trustee"): return False # Step 2: Table access tableName = f"trustee.{resource}" if not checkSystemRbac(user, tableName, operation): return False # Step 3: Feature-level access # Get user's access records for this organisation userAccessRecords = getTrusteeAccessForUser(user.id, organisationId) # Filter by contract if contractId is specified if contractId: # User muss Access für diesen spezifischen Contract haben # Oder Access ohne contractId (full organisation access) userAccessRecords = [ acc for acc in userAccessRecords if acc.contractId is None or acc.contractId == contractId ] else: # Wenn kein contractId angegeben: User muss Access haben (mit oder ohne contractId) pass userRoles = [acc.roleId for acc in userAccessRecords] # Check role permissions if "admin" in userRoles: # Admin can do everything for their organisation (or specific contract) return True if operation in ["read", "create", "update", "delete"]: if "operate" in userRoles: # Operate can CRUD for organisation (or specific contract) if resource in ["contract", "document", "position", "xpositiondocument"]: return True if "userreport" in userRoles: # Userreport can CRUD own records if recordId: record = getRecord(resource, recordId) if record._createdBy == user.id: # _createdBy wird automatisch vom DatabaseConnector gesetzt return True else: # Creating new record - allowed return True return False ``` ### Rollen-Berechtigungs-Matrix | Role | Organisation | Contract | Document | Position | PositionDocument | |------|--------------|----------|-----------|----------|------------------| | **admin** (system) | CRUD (group) | CRUD (group) | CRUD (group) | CRUD (group) | CRUD (group) | | **admin** (trustee.access) | CRUD (org) | CRUD (org) | CRUD (org) | CRUD (org) | CRUD (org) | | **operate** | Read (org) | CRUD (org) | CRUD (org) | CRUD (org) | CRUD (org) | | **userreport** | Read (org) | Read (org) | CRUD (own) | CRUD (own) | CRUD (own) | **Legend**: - CRUD: Create, Read, Update, Delete - (group): Within user's mandate/group - (org): Within assigned organisation - (own): Only records created by the user --- ## API-Design ### RBAC-Berechtigungsabfrage Das UI benötigt Berechtigungsinformationen, um die richtigen Komponenten zu rendern. Hierfür stehen folgende RBAC-Routen zur Verfügung: #### Einzelne Berechtigung abfragen ```python GET /api/rbac/permissions?context=UI&item=trustee GET /api/rbac/permissions?context=RESOURCE&item=trustee ``` Gibt `UserPermissions` für einen spezifischen Kontext und Item zurück. #### Alle Berechtigungen abrufen (für UI-Initialisierung) ```python GET /api/rbac/permissions/all # Alle UI- und RESOURCE-Berechtigungen GET /api/rbac/permissions/all?context=UI # Nur UI-Berechtigungen GET /api/rbac/permissions/all?context=RESOURCE # Nur RESOURCE-Berechtigungen ``` Gibt alle Berechtigungen des aktuellen Users zurück: ```json { "ui": { "trustee": {"view": true, "read": null, "create": null, "update": null, "delete": null}, "trustee.organisation": {"view": true, ...}, "trustee.role": {"view": true, ...}, ... }, "resource": { "trustee": {"view": true, ...}, ... } } ``` **Verwendung**: - **UI-Initialisierung**: Einmaliger Call zu `/api/rbac/permissions/all` beim Laden der Anwendung - **Spezifische Prüfung**: `/api/rbac/permissions?context=UI&item=trustee.organisation` für dynamische Berechtigungsprüfungen ### Route-Struktur Alle Trustee-Routen folgen dem Muster `/api/trustee/{resource}`: #### Organisation Routes (`routeDataTrusteeOrganisations.py`) ```python GET /api/trustee/organisations/ # List organisations (filtered by RBAC) GET /api/trustee/organisations/{id} # Get organisation POST /api/trustee/organisations/ # Create organisation PUT /api/trustee/organisations/{id} # Update organisation DELETE /api/trustee/organisations/{id} # Delete organisation ``` #### Role Routes (`routeDataTrusteeRoles.py`) ```python GET /api/trustee/roles/ # List roles GET /api/trustee/roles/{id} # Get role POST /api/trustee/roles/ # Create role (sysadmin only) PUT /api/trustee/roles/{id} # Update role (sysadmin only) DELETE /api/trustee/roles/{id} # Delete role (sysadmin only) ``` #### Access Routes (`routeDataTrusteeAccess.py`) ```python GET /api/trustee/access/ # List access records (filtered) GET /api/trustee/access/{id} # Get access record POST /api/trustee/access/ # Create access record PUT /api/trustee/access/{id} # Update access record DELETE /api/trustee/access/{id} # Delete access record GET /api/trustee/access/organisation/{orgId} # Get access for organisation GET /api/trustee/access/user/{userId} # Get access for user ``` #### Contract Routes (`routeDataTrusteeContracts.py`) ```python GET /api/trustee/contracts/ # List contracts (filtered) GET /api/trustee/contracts/{id} # Get contract POST /api/trustee/contracts/ # Create contract PUT /api/trustee/contracts/{id} # Update contract DELETE /api/trustee/contracts/{id} # Delete contract GET /api/trustee/contracts/organisation/{orgId} # Get contracts for organisation ``` #### Document Routes (`routeDataTrusteeDocuments.py`) ```python GET /api/trustee/documents/ # List documents (filtered) GET /api/trustee/documents/{id} # Get document metadata GET /api/trustee/documents/{id}/data # Get document binary data POST /api/trustee/documents/ # Create document (with upload) PUT /api/trustee/documents/{id} # Update document metadata DELETE /api/trustee/documents/{id} # Delete document GET /api/trustee/documents/contract/{contractId} # Get documents for contract ``` #### Position Routes (`routeDataTrusteePositions.py`) ```python GET /api/trustee/positions/ # List positions (filtered) GET /api/trustee/positions/{id} # Get position POST /api/trustee/positions/ # Create position PUT /api/trustee/positions/{id} # Update position DELETE /api/trustee/positions/{id} # Delete position GET /api/trustee/positions/contract/{contractId} # Get positions for contract GET /api/trustee/positions/organisation/{orgId} # Get positions for organisation ``` #### Position-Document Cross-Reference Routes (`routeDataTrusteePositionDocuments.py`) ```python GET /api/trustee/position-documents/ # List cross-references GET /api/trustee/position-documents/{id} # Get cross-reference POST /api/trustee/position-documents/ # Link position to document DELETE /api/trustee/position-documents/{id} # Unlink position from document GET /api/trustee/position-documents/position/{positionId} # Get documents for position GET /api/trustee/position-documents/document/{documentId} # Get positions for document ``` ### Request/Response Examples #### Create Organisation ```http POST /api/trustee/organisations/ Content-Type: application/json { "id": "acme-corp", "label": "ACME Corporation", "enabled": true } ``` Response: ```json { "id": "acme-corp", "label": "ACME Corporation", "enabled": true, "mandate": "mandate-123", "created": 1704067200.0, "updated": 1704067200.0 } ``` #### Create Access Record ```http POST /api/trustee/access/ Content-Type: application/json { "organisationId": "acme-corp", "roleId": "operate", "userId": "user-456" } ``` #### Upload Document ```http POST /api/trustee/documents/ Content-Type: multipart/form-data organisationId: acme-corp contractId: contract-789 documentName: receipt.pdf documentMimeType: application/pdf file: ``` --- ## Implementierungsplan ### Phase 1: Grundlagen (Woche 1-2) #### 1.1 Datenmodelle - [ ] Create `datamodelTrustee.py` with all model classes - [ ] Register model labels - [ ] Add frontend metadata - [ ] Define validation rules #### 1.2 Datenbank-Interface - [ ] Create `interfaceDbTrusteeObjects.py` - [ ] Implement database connector initialization - [ ] Implement table creation/initialization - [ ] Implement basic CRUD operations for all tables - [ ] Add RBAC integration helpers #### 1.3 RBAC Setup - [ ] Create initial AccessRules for: - `resource.trustee` - `ui.trustee` - All trustee tables (DATA context) - [ ] Create bootstrap script to initialize roles - [ ] Document RBAC configuration ### Phase 2: Core API (Woche 3-4) #### 2.1 Route Implementation - [ ] `routeDataTrusteeOrganisations.py` - [ ] `routeDataTrusteeRoles.py` - [ ] `routeDataTrusteeAccess.py` - [ ] `routeDataTrusteeContracts.py` - [ ] `routeDataTrusteeDocuments.py` - [ ] `routeDataTrusteePositions.py` - [ ] `routeDataTrusteePositionDocuments.py` #### 2.2 Berechtigungsprüfung - [ ] Implement `checkTrusteePermission()` helper - [ ] Integrate permission checks in all routes - [ ] Add permission checks in interface methods - [ ] Test permission scenarios #### 2.3 Route Registration - [ ] Register routes in main app - [ ] Add route documentation - [ ] Test API endpoints ### Phase 3: Advanced Features (Week 5-6) #### 3.1 Dokumentenspeicherung - [ ] Decide on storage approach (DB vs file system) - [ ] Implement document upload/download - [ ] Add file size limits - [ ] Add MIME type validation #### 3.2 Query Optimization - [ ] Add database indexes - [ ] Optimize RBAC filtering queries - [ ] Add pagination support - [ ] Add filtering and sorting #### 3.3 Validierung & Fehlerbehandlung - [ ] Add input validation - [ ] Add foreign key validation - [ ] Improve error messages - [ ] Add audit logging ### Phase 4: Frontend-Integration (Woche 7-8) #### 4.1 UI Components - [ ] Organisation management UI - [ ] Role management UI (sysadmin only) - [ ] Access management UI - [ ] Contract management UI - [ ] Document upload/management UI - [ ] Position entry/management UI - [ ] Position-document linking UI #### 4.2 RBAC UI Integration - [ ] Hide UI elements based on `ui.trustee` access - [ ] Show/hide features based on roles - [ ] Add permission error messages ### Phase 5: Testing & Dokumentation (Woche 9-10) #### 5.1 Unit Tests - [ ] Test data models - [ ] Test interface methods - [ ] Test permission checking - [ ] Test RBAC integration #### 5.2 Integration Tests - [ ] Test API endpoints - [ ] Test RBAC scenarios - [ ] Test document upload/download - [ ] Test cross-reference operations #### 5.3 Documentation - [ ] API documentation - [ ] User guide - [ ] Admin guide - [ ] Developer guide --- ## Migrationsstrategie ### Datenbank-Migration 1. **Tabellen erstellen**: Interface-Initialisierung verwenden, um Tabellen zu erstellen 2. **Initialdaten**: Bootstrap-Skript zum Erstellen initialer Rollen 3. **RBAC-Regeln**: Migrationsskript zum Erstellen von AccessRules 4. **Datenmigration**: Falls Migration von bestehendem System, Migrationsskript erstellen ### Rollout-Plan 1. **Entwicklung**: Implementierung in Entwicklungsumgebung 2. **Testing**: Deployment in Testumgebung, Integrationstests durchführen 3. **Staging**: Deployment in Staging, User Acceptance Testing 4. **Produktion**: Schrittweiser Rollout mit Feature-Flag ### Rückwärtskompatibilität - Keine Breaking Changes am bestehenden System - Trustee Feature ist additiv - Bestehendes RBAC-System bleibt unverändert --- ## Testing Strategy ### Unit Tests ```python # Test data models def test_trustee_organisation_model(): org = TrusteeOrganisation( id="test-org", label="Test Organisation", enabled=True ) assert org.id == "test-org" # Test interface methods def test_create_organisation(): interface = TrusteeInterface(user) org = interface.createOrganisation(...) assert org.id is not None # Test permission checking def test_check_trustee_permission(): assert checkTrusteePermission(user, orgId, "read", "contract") == True ``` ### Integrations-Tests ```python # Test API endpoints def test_create_organisation_api(): response = client.post("/api/trustee/organisations/", json={...}) assert response.status_code == 201 # Test RBAC filtering def test_organisation_list_filtered_by_rbac(): # User should only see organisations they have access to response = client.get("/api/trustee/organisations/") assert all(org in allowed_orgs for org in response.json()) ``` ### RBAC-Test-Szenarien 1. **Sysadmin**: Kann auf alle Organisationen zugreifen 2. **Admin (Gruppe)**: Kann auf Organisationen in ihrer Gruppe zugreifen 3. **Admin (Trustee)**: Kann auf Organisationen zugreifen, denen sie zugewiesen sind 4. **Operate**: Kann CRUD für Verträge/Dokumente/Positionen in zugewiesenen Organisationen 5. **Userreport**: Kann nur eigene Datensätze CRUD 6. **Kein Zugriff**: Kann nicht auf Trustee Feature zugreifen --- ## Implementierungsdetails ### DatabaseConnector - Automatische Tabellenerstellung Der `DatabaseConnector` erstellt Tabellen automatisch aus Pydantic-Modellen: #### Prozess 1. **Tabellen-Erstellung**: `_ensureTableExists(model_class)` wird bei jedem Datenbankzugriff aufgerufen 2. **Spalten-Generierung**: Spalten werden aus Modell-Feldern generiert: - `id`: VARCHAR(255) PRIMARY KEY - Andere Felder: Automatische Typ-Konvertierung (str → TEXT, int → INTEGER, float → DOUBLE PRECISION, bool → BOOLEAN, date → DATE, datetime → TIMESTAMP, bytes → BYTEA) - Systemattribute: Automatisch hinzugefügt (`_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy`) 3. **Index-Erstellung**: Automatische Indizes für Foreign Keys (Felder die auf `Id` enden) 4. **Migration**: Fehlende Spalten werden automatisch hinzugefügt (additive Migration, keine Spalten-Löschung) #### Beispiel Trustee-Modell ```python class TrusteeOrganisation(BaseModel): id: str = Field(...) # VARCHAR(255) PRIMARY KEY label: str = Field(...) # TEXT enabled: bool = Field(...) # BOOLEAN mandate: str = Field(...) # TEXT # Systemattribute werden automatisch hinzugefügt: # _createdAt: DOUBLE PRECISION # _modifiedAt: DOUBLE PRECISION # _createdBy: VARCHAR(255) # _modifiedBy: VARCHAR(255) ``` **Ergebnis**: Tabelle `TrusteeOrganisation` wird automatisch erstellt mit allen Spalten. ### RBAC-Filterung auf DB-Ebene Das RBAC-System filtert Daten automatisch auf Datenbank-Ebene: #### Funktion: `getRecordsetWithRBAC()` ```python records = getRecordsetWithRBAC( connector=db, modelClass=TrusteeOrganisation, currentUser=user, recordFilter={"enabled": True} # Zusätzliche Filter ) ``` #### Filterung basierend auf AccessLevel 1. **View-Permission prüfen**: Wenn `permissions.view = false`, werden keine Records zurückgegeben 2. **Read-Level Filterung**: - `ALL`: Keine WHERE-Clause (alle Records) - `MY`: `WHERE "_createdBy" = %s` (nur eigene Records) - `GROUP`: `WHERE "mandateId" = %s` (nur Records der eigenen Gruppe) - `NONE`: `WHERE 1 = 0` (keine Records) #### Trustee-spezifische Filterung Für Trustee-Feature müssen zusätzliche Filter implementiert werden: 1. **Organisation-Filterung**: Basierend auf `trustee.access` Tabelle - User mit `admin` Rolle: Nur Records mit `organisationId` aus eigenen Zugriffen - User mit `operate` Rolle: Nur Records mit `organisationId` aus eigenen Zugriffen - User mit `userreport` Rolle: Nur eigene Records (`_createdBy = userId`) 2. **Contract-Filterung**: Basierend auf `contractId` in `trustee.access` Tabelle - **Ohne `contractId` (None)**: User hat Zugriff auf alle Contracts der Organisation - **Mit `contractId`**: User hat Zugriff nur auf diesen spezifischen Contract - **Filter-Logik**: - Wenn User Access ohne `contractId` hat → Zugriff auf alle Contracts der Organisation - Wenn User Access mit spezifischem `contractId` hat → Zugriff nur auf diesen Contract - Wenn User beide hat → Zugriff auf alle Contracts (weil Access ohne contractId übergeordnet ist) - **Anwendung**: Bei Queries für `contract`, `document`, `position` wird zusätzlich nach `contractId` gefiltert: - Records mit `contractId` werden nur angezeigt wenn User Access für diesen Contract hat (oder Access ohne contractId) - Records ohne `contractId` werden angezeigt wenn User Access ohne contractId hat **Implementierung**: - **Zwei-Stufen-Filterung**: Zuerst System-RBAC (`getRecordsetWithRBAC()`), dann zusätzliche Trustee-Filterung in der Interface-Schicht - Diese Filterung muss in der Trustee-Interface-Schicht implementiert werden, da sie feature-spezifisch ist - **Wichtig**: Für das UI ist diese Logik nicht relevant, da RBAC automatisch alle Daten korrekt gefiltert liefert ### FormGenerator - Automatische UI-Generierung **FormGenerator** bietet folgende automatische Features: #### Tabellen-Features (automatisch) - ✅ Auto-Spalten-Erkennung (wenn keine `columns` übergeben) - ✅ Sortierung (Klick auf Header: asc → desc → keine) - ✅ Filter (Text, Boolean, Enum, Date) - ✅ Pagination (konfigurierbare `pageSize`) - ✅ Spalten-Resize (Drag & Drop) - ✅ Row Selection (Checkboxen) - ✅ Custom Actions (Buttons pro Zeile) - ✅ Custom Formatter (pro Spalte) - ✅ Loading State #### Formular-Features (automatisch) - ✅ Feld-Typen basierend auf `frontend_type` - ✅ Validierung basierend auf `frontend_required` - ✅ Readonly-Felder basierend auf `frontend_readonly` - ✅ Select-Optionen aus `frontend_options` - ✅ Floating Labels #### Custom Logic (manuell implementieren) - 🔧 Custom Validierungen (z.B. MwSt-Berechnung) - 🔧 Custom Formatter (z.B. Links zu verknüpften Records) - 🔧 Custom Actions (z.B. Download-Button) - 🔧 Custom Business Logic ## Implementierungsdetails ### Dokumentenspeicherung **Entscheidung**: Dokument-Binärdaten werden in PostgreSQL BYTEA-Spalte gespeichert. - Einfach, transaktional, einfaches Backup - Für Phase 1 ausreichend ### Organisation ID Format **Entscheidung**: String-Label (z.B. "acme-corp") - Menschenlesbar, benutzerfreundlich - Validierung: Alphanumerisch, Bindestriche, Unterstriche - Längenbegrenzung: 3-50 Zeichen - Groß-/Kleinschreibung-unabhängige Eindeutigkeitsprüfung ### Rollen-Management **Entscheidung**: Rollen sind bearbeitbar - Rollen werden in `trustee.role` Tabelle gespeichert - Initiale Rollen werden beim Bootstrap erstellt - Rollen können bearbeitet werden, aber nicht gelöscht werden, wenn sie in Verwendung sind ### Access-Record Eindeutigkeit **Entscheidung**: Mehrere Access-Records erlaubt - Ein Benutzer kann mehrere Rollen für dieselbe Organisation haben - Unique Constraint auf `(organisationId, roleId, userId)` verhindert Duplikate - Separate Records für jede Rollenkombination ### Vertrags-Organisations-Beziehung **Entscheidung**: Verträge sind unveränderlich - `contract.organisationId` kann nach der Erstellung NICHT mehr geändert werden - Datenintegrität und Audit-Trail werden dadurch gewährleistet ### Position-Dokument-Beziehung **Entscheidung**: Verknüpfungen sind optional - Positionen können ohne Dokumente existieren - Dokumente können ohne Positionen existieren - Viele-zu-viele-Beziehung über `xpositiondocument` Tabelle ### Währungsumrechnung **Entscheidung**: Phase 1 - Manuelle Eingabe, keine automatische Umrechnung - Beide Währungen werden gespeichert: `bookingCurrency/bookingAmount` und `originalCurrency/originalAmount` - Manuelle Eingabe durch Benutzer - Wechselkurstabelle und automatische Umrechnung können in Phase 2 hinzugefügt werden ### MwSt-Berechnung **Entscheidung**: Automatische Berechnung mit manueller Überschreibung - `vatAmount = bookingAmount * vatPercentage / 100` wird automatisch berechnet - Manuelle Überschreibung erlaubt, falls erforderlich - Validierungswarnung, wenn Werte nicht übereinstimmen --- ## Nächste Schritte 1. **Dokument mit Stakeholdern final überprüfen** 2. **Detaillierte technische Spezifikationen** für jede Komponente erstellen 3. **Entwicklungsumgebung** und Projektstruktur einrichten 4. **Phase 1 Implementierung beginnen** --- ## Anhang ### A. Datenbank-Schema SQL ```sql -- Organisation table CREATE TABLE trustee_organisation ( id VARCHAR(255) PRIMARY KEY, label VARCHAR(255) NOT NULL, enabled BOOLEAN DEFAULT TRUE, mandate VARCHAR(255) NOT NULL, _created_at FLOAT NOT NULL, -- Systemattribut _modified_at FLOAT NOT NULL, -- Systemattribut _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt _modified_by VARCHAR(255) -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt ); CREATE INDEX idx_trustee_organisation_mandate ON trustee_organisation(mandate); -- Role table (Rollen sind bearbeitbar) CREATE TABLE trustee_role ( id VARCHAR(255) PRIMARY KEY, -- String-Label (z.B. "userreport", "admin", "operate") desc TEXT NOT NULL, mandate VARCHAR(255) NOT NULL, _created_at FLOAT NOT NULL, -- Systemattribut _modified_at FLOAT NOT NULL, -- Systemattribut _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt _modified_by VARCHAR(255) -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt ); CREATE INDEX idx_trustee_role_mandate ON trustee_role(mandate); -- Access table (ein Benutzer kann mehrere Rollen für dieselbe Organisation haben) CREATE TABLE trustee_access ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), role_id VARCHAR(255) NOT NULL REFERENCES trustee_role(id), user_id VARCHAR(255) NOT NULL, mandate VARCHAR(255) NOT NULL, _created_at FLOAT NOT NULL, -- Systemattribut _modified_at FLOAT NOT NULL, -- Systemattribut _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt _modified_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt UNIQUE(organisation_id, role_id, user_id) -- Verhindert Duplikate, erlaubt aber mehrere Rollen pro Benutzer-Organisation ); CREATE INDEX idx_trustee_access_organisation ON trustee_access(organisation_id); CREATE INDEX idx_trustee_access_user ON trustee_access(user_id); CREATE INDEX idx_trustee_access_role ON trustee_access(role_id); CREATE INDEX idx_trustee_access_mandate ON trustee_access(mandate); -- Contract table (Verträge sind unveränderlich: organisation_id kann nicht geändert werden) CREATE TABLE trustee_contract ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), -- Immutable nach Erstellung label VARCHAR(255) NOT NULL, enabled BOOLEAN DEFAULT TRUE, mandate VARCHAR(255) NOT NULL, _created_at FLOAT NOT NULL, -- Systemattribut _modified_at FLOAT NOT NULL, -- Systemattribut _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt _modified_by VARCHAR(255) -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt ); CREATE INDEX idx_trustee_contract_organisation ON trustee_contract(organisation_id); CREATE INDEX idx_trustee_contract_mandate ON trustee_contract(mandate); -- Document table CREATE TABLE trustee_document ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), contract_id UUID NOT NULL REFERENCES trustee_contract(id), document_data BYTEA NOT NULL, -- Binärdaten werden direkt in Datenbank gespeichert document_name VARCHAR(255) NOT NULL, document_mime_type VARCHAR(100) NOT NULL, mandate VARCHAR(255) NOT NULL, created FLOAT NOT NULL, updated FLOAT NOT NULL, created_by VARCHAR(255) NOT NULL ); CREATE INDEX idx_trustee_document_organisation ON trustee_document(organisation_id); CREATE INDEX idx_trustee_document_contract ON trustee_document(contract_id); CREATE INDEX idx_trustee_document_mandate ON trustee_document(mandate); CREATE INDEX idx_trustee_document_created_by ON trustee_document(_created_by); -- Position table CREATE TABLE trustee_position ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), contract_id UUID NOT NULL REFERENCES trustee_contract(id), valuta DATE NOT NULL, transaction_date_time TIMESTAMP NOT NULL, company VARCHAR(255), desc TEXT, tags VARCHAR(255), booking_currency VARCHAR(10) NOT NULL, booking_amount FLOAT NOT NULL, original_currency VARCHAR(10) NOT NULL, original_amount FLOAT NOT NULL, vat_percentage FLOAT DEFAULT 0.0, vat_amount FLOAT DEFAULT 0.0, mandate VARCHAR(255) NOT NULL, created FLOAT NOT NULL, updated FLOAT NOT NULL, created_by VARCHAR(255) NOT NULL ); CREATE INDEX idx_trustee_position_organisation ON trustee_position(organisation_id); CREATE INDEX idx_trustee_position_contract ON trustee_position(contract_id); CREATE INDEX idx_trustee_position_valuta ON trustee_position(valuta); CREATE INDEX idx_trustee_position_transaction_date_time ON trustee_position(transaction_date_time); CREATE INDEX idx_trustee_position_mandate ON trustee_position(mandate); CREATE INDEX idx_trustee_position_created_by ON trustee_position(_created_by); -- Für userreport Filterung -- Position-Document cross-reference table (Tabellenname in Kleinbuchstaben: xpositiondocument) CREATE TABLE trustee_xpositiondocument ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), contract_id UUID NOT NULL REFERENCES trustee_contract(id), document_id UUID NOT NULL REFERENCES trustee_document(id), position_id UUID NOT NULL REFERENCES trustee_position(id), mandate VARCHAR(255) NOT NULL, _created_at FLOAT NOT NULL, -- Systemattribut _modified_at FLOAT NOT NULL, -- Systemattribut _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt _modified_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt UNIQUE(position_id, document_id) -- Verknüpfungen sind optional: Positionen/Dokumente können unabhängig existieren ); CREATE INDEX idx_trustee_xpd_organisation ON trustee_xpositiondocument(organisation_id); CREATE INDEX idx_trustee_xpd_contract ON trustee_xpositiondocument(contract_id); CREATE INDEX idx_trustee_xpd_document ON trustee_xpositiondocument(document_id); CREATE INDEX idx_trustee_xpd_position ON trustee_xpositiondocument(position_id); CREATE INDEX idx_trustee_xpd_mandate ON trustee_xpositiondocument(mandate); CREATE INDEX idx_trustee_xpd_created_by ON trustee_xpositiondocument(_created_by); -- Für userreport Filterung ``` ### B. Initial Roles Bootstrap **Implementierung**: Bootstrap-Script erstellt initiale Rollen automatisch beim ersten Start des Trustee-Features (ähnlich wie `initBootstrap()` für andere System-Tabellen). ```python # Bootstrap script to create initial roles initial_roles = [ { "id": "userreport", "desc": "Can deliver user documents to the system" }, { "id": "admin", "desc": "Can administrate the access" }, { "id": "operate", "desc": "Can use data for operations" } ] ``` ### C. RBAC Access Rules Bootstrap ```python # Bootstrap script to create initial AccessRules initial_access_rules = [ # Feature access { "roleLabel": "sysadmin", "context": "RESOURCE", "item": "trustee", "view": True }, # UI access { "roleLabel": "sysadmin", "context": "UI", "item": "trustee", "view": True }, # Table access rules for each table... ] ``` --- **Dokumentversion**: 1.0 **Letzte Aktualisierung**: 2025-01-03 **Autor**: Architektur-Review **Status**: Ausstehende Überprüfung