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

1815 lines
66 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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