66 KiB
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
- Übersicht
- Architektur-Analyse
- Architektur-Entscheidungen
- System-Architektur
- Datenmodell
- RBAC-Integration
- API-Design
- Implementierungsplan
- Migrationsstrategie
- Teststrategie
- 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
- Feature-basiertes System: Das System definiert Zugriffe für Features. "Trustee" ist das erste Feature.
- Datenbank-Interface: Jedes Feature hat ein Datenbank-Interface (wie "chat") mit automatisierten Systemattributen (mandate, created, updated, etc.)
- RBAC-Integration: Feature-Zugriff wird über RBAC-Ressourcen gesteuert
- 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:
-
Interface Pattern: Custom interfaces follow the pattern of
interfaceDbChatObjects.py:- Use
DatabaseConnectorfor database operations - Implement CRUD operations
- Integrate with RBAC through
interfaceRbac.py - Support standard attributes (mandate, _createdAt, _modifiedAt, _createdBy, _modifiedBy) - automatisch vom DatabaseConnector gesetzt
- Use
-
RBAC System:
- Uses
AccessRuleContextenum: 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 nachmandateId = user.mandateId(nur Records der eigenen Gruppe)NONE: Keine Records sichtbar (1 = 0WHERE-Clause)
- View-Permission: Wird zuerst geprüft - wenn
view=false, werden keine Records zurückgegeben
- Uses
-
Route Pattern: Routes follow
routeData*.pypattern:- Use FastAPI routers
- Integrate with authentication via
getCurrentUser - Support pagination
- Use interface methods for data access
-
Data Model Pattern: Models in
datamodels/datamodel*.py:- Use Pydantic BaseModel
- Register labels with
registerModelLabels - Include frontend metadata in
json_schema_extra
-
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
Idenden) - Migration: Fehlende Spalten werden automatisch hinzugefügt (additive Migration)
- SQL-Typ-Mapping: Automatische Konvertierung von Python-Typen zu PostgreSQL-Typen
- Automatische Tabellenerstellung:
Architektur-Entscheidungen
- Feature-Registrierung: Das Trustee Feature wird als neues Interface ähnlich wie Chat registriert
- RBAC-Ressourcen:
ui.trustee- UI-Zugriffskontrolleresource.trustee- Feature-Ressourcen-Zugriff
- Datenbank-Isolation: Jedes Feature hat sein eigenes Datenbank-Interface, teilt aber dieselbe Datenbankinstanz
- 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
mandatekann mehrereorganisationIds haben - Eine
organisationIdgehört zu genau einermandate mandatewird automatisch auscurrentUser.mandateIdgesetztorganisationIdDropdown zeigt alle gelieferten Organisationen (RBAC filtert automatisch)organisationIds können nicht übermandate-Grenzen hinweg geteilt werden
- Eine
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 organisationsadmin: Can manage organisations for their group (mandate)
Indexes:
- Primary key on
id - Index on
mandatefor 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 liefernadmin: Kann den Zugriff administrierenoperate: Kann Daten für Operationen verwenden
RBAC Rules:
sysadmin: Can manage all roles
Indexes:
- Primary key on
id - Index on
mandatefor 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 recordsadmin: Can manage access for their group (mandate)- Users with
adminrole 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 contractsadmin: Can manage contracts for their group (mandate)- Users with
adminrole in trustee.access: Can CRUD contracts for their organisationId- Wenn
contractIdin trustee.access leer: Zugriff auf alle Contracts der Organisation - Wenn
contractIdgesetzt: Zugriff nur auf diesen spezifischen Contract - New records default to their own organisationId
- Dropdown to select from granted organisationIds (und optional Contracts)
- Wenn
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: WennorganisationIdim Update vorhanden und unterschiedlich zum bestehenden Wert → Fehler - Frontend-Readonly:
organisationIdwird auf readonly gesetzt wennidvorhanden (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:
organisationIdist immutable nach Erstellung - keine Updates erlaubt
Validierung:
idFormat: 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 documentsadmin: Can manage documents for their group (mandate)- Users with
operaterole in trustee.access: Can CRUD documents in their organisationId - Users with
userreportrole 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 positionsadmin: Can manage positions for their group (mandate)- Users with
operaterole in trustee.access: Can CRUD positions in their organisationId- Wenn
contractIdin trustee.access leer: Zugriff auf alle Positions der Organisation - Wenn
contractIdgesetzt: Zugriff nur auf Positions dieses Contracts
- Wenn
- Users with
userreportrole 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-referencesadmin: Can manage cross-references for their group (mandate)- Users with
operaterole in trustee.access: Can CRUD cross-references in their organisationId- Wenn
contractIdin trustee.access leer: Zugriff auf alle Cross-References der Organisation - Wenn
contractIdgesetzt: Zugriff nur auf Cross-References dieses Contracts
- Wenn
- Users with
userreportrole 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:
- Rollen-basierter Zugriff: Benutzern werden Rollen (
userreport,admin,operate) für spezifische Organisationen zugewiesen - Contract-basierter Zugriff (optional):
- Wenn
contractIdnicht gesetzt (None): Zugriff gilt für die gesamte Organisation - Wenn
contractIdgesetzt: Zugriff ist auf diesen spezifischen Contract beschränkt
- Wenn
- 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/allbeim Laden der Anwendung - Spezifische Prüfung:
/api/rbac/permissions?context=UI&item=trustee.organisationfü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.pywith 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.trusteeui.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.pyrouteDataTrusteeRoles.pyrouteDataTrusteeAccess.pyrouteDataTrusteeContracts.pyrouteDataTrusteeDocuments.pyrouteDataTrusteePositions.pyrouteDataTrusteePositionDocuments.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.trusteeaccess - 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
- Tabellen erstellen: Interface-Initialisierung verwenden, um Tabellen zu erstellen
- Initialdaten: Bootstrap-Skript zum Erstellen initialer Rollen
- RBAC-Regeln: Migrationsskript zum Erstellen von AccessRules
- Datenmigration: Falls Migration von bestehendem System, Migrationsskript erstellen
Rollout-Plan
- Entwicklung: Implementierung in Entwicklungsumgebung
- Testing: Deployment in Testumgebung, Integrationstests durchführen
- Staging: Deployment in Staging, User Acceptance Testing
- 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
- Sysadmin: Kann auf alle Organisationen zugreifen
- Admin (Gruppe): Kann auf Organisationen in ihrer Gruppe zugreifen
- Admin (Trustee): Kann auf Organisationen zugreifen, denen sie zugewiesen sind
- Operate: Kann CRUD für Verträge/Dokumente/Positionen in zugewiesenen Organisationen
- Userreport: Kann nur eigene Datensätze CRUD
- Kein Zugriff: Kann nicht auf Trustee Feature zugreifen
Implementierungsdetails
DatabaseConnector - Automatische Tabellenerstellung
Der DatabaseConnector erstellt Tabellen automatisch aus Pydantic-Modellen:
Prozess
- Tabellen-Erstellung:
_ensureTableExists(model_class)wird bei jedem Datenbankzugriff aufgerufen - 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)
- Index-Erstellung: Automatische Indizes für Foreign Keys (Felder die auf
Idenden) - 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
- View-Permission prüfen: Wenn
permissions.view = false, werden keine Records zurückgegeben - 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:
-
Organisation-Filterung: Basierend auf
trustee.accessTabelle- User mit
adminRolle: Nur Records mitorganisationIdaus eigenen Zugriffen - User mit
operateRolle: Nur Records mitorganisationIdaus eigenen Zugriffen - User mit
userreportRolle: Nur eigene Records (_createdBy = userId)
- User mit
-
Contract-Filterung: Basierend auf
contractIdintrustee.accessTabelle- 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
contractIdhat → Zugriff auf alle Contracts der Organisation - Wenn User Access mit spezifischem
contractIdhat → Zugriff nur auf diesen Contract - Wenn User beide hat → Zugriff auf alle Contracts (weil Access ohne contractId übergeordnet ist)
- Wenn User Access ohne
- Anwendung: Bei Queries für
contract,document,positionwird zusätzlich nachcontractIdgefiltert:- Records mit
contractIdwerden nur angezeigt wenn User Access für diesen Contract hat (oder Access ohne contractId) - Records ohne
contractIdwerden angezeigt wenn User Access ohne contractId hat
- Records mit
- Ohne
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.roleTabelle 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.organisationIdkann 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
xpositiondocumentTabelle
Währungsumrechnung
Entscheidung: Phase 1 - Manuelle Eingabe, keine automatische Umrechnung
- Beide Währungen werden gespeichert:
bookingCurrency/bookingAmountundoriginalCurrency/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 / 100wird automatisch berechnet- Manuelle Überschreibung erlaubt, falls erforderlich
- Validierungswarnung, wenn Werte nicht übereinstimmen
Nächste Schritte
- Dokument mit Stakeholdern final überprüfen
- Detaillierte technische Spezifikationen für jede Komponente erstellen
- Entwicklungsumgebung und Projektstruktur einrichten
- 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