wiki/ui_nyla/feature-trustee/doc_trustee_feature_architecture.md
2026-01-04 01:35:18 +01:00

66 KiB
Raw Blame History

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
  2. Architektur-Analyse
  3. Architektur-Entscheidungen
  4. System-Architektur
  5. Datenmodell
  6. RBAC-Integration
  7. API-Design
  8. Implementierungsplan
  9. Migrationsstrategie
  10. Teststrategie
  11. Implementierungsdetails

Zugehörige Dokumente:

  • UI-Spezifikation - 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 organisationIds 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)
      • organisationIds 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:

# 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.

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.

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.

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.

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.

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).

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).

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:

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:

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:

# 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

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

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)

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:

{
  "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)

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)

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)

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)

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)

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)

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)

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

POST /api/trustee/organisations/
Content-Type: application/json

{
  "id": "acme-corp",
  "label": "ACME Corporation",
  "enabled": true
}

Response:

{
  "id": "acme-corp",
  "label": "ACME Corporation",
  "enabled": true,
  "mandate": "mandate-123",
  "created": 1704067200.0,
  "updated": 1704067200.0
}

Create Access Record

POST /api/trustee/access/
Content-Type: application/json

{
  "organisationId": "acme-corp",
  "roleId": "operate",
  "userId": "user-456"
}

Upload Document

POST /api/trustee/documents/
Content-Type: multipart/form-data

organisationId: acme-corp
contractId: contract-789
documentName: receipt.pdf
documentMimeType: application/pdf
file: <binary data>

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

# 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

# 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

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()

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

-- 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).

# 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

# 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