# Copyright (c) 2025 Patrick Motsch # All rights reserved. """Trustee models: TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition.""" from enum import Enum from typing import Optional from pydantic import BaseModel, Field from modules.shared.attributeUtils import registerModelLabels import uuid class TrusteeOrganisation(BaseModel): """Represents trustee organisations (companies) within the Trustee feature.""" id: str = Field( # Unique string label (PK), not UUID description="Unique organisation identifier (label)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, # Editable at creation, then 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 } ) mandateId: Optional[str] = Field( default=None, description="Mandate ID (system-level organisation)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) featureInstanceId: Optional[str] = Field( default=None, description="Feature Instance ID for instance-level isolation", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # System attributes are automatically set by DatabaseConnector: # _createdAt, _modifiedAt, _createdBy, _modifiedBy registerModelLabels( "TrusteeOrganisation", {"en": "Organisation", "fr": "Organisation", "de": "Organisation"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"}, "enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"}, "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"}, "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"}, }, ) class TrusteeRole(BaseModel): """Defines roles within the Trustee feature.""" 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 } ) mandateId: Optional[str] = Field( default=None, description="Mandate ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) featureInstanceId: Optional[str] = Field( default=None, description="Feature Instance ID for instance-level isolation", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # System attributes are automatically set by DatabaseConnector registerModelLabels( "TrusteeRole", {"en": "Role", "fr": "Rôle", "de": "Rolle"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"}, "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"}, "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"}, }, ) class TrusteeAccess(BaseModel): """Defines user access to organisations with specific roles.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Unique access ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) organisationId: str = Field( description="Reference to TrusteeOrganisation.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "/api/trustee/{instanceId}/organisations/options" } ) roleId: str = Field( description="Reference to TrusteeRole.id", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "/api/trustee/{instanceId}/roles/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": "/api/users/options" } ) contractId: Optional[str] = Field( default=None, description="Optional reference to TrusteeContract.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": "/api/trustee/{instanceId}/contracts/options", "frontend_depends_on": "organisationId" } ) mandateId: Optional[str] = Field( default=None, description="Mandate ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) featureInstanceId: Optional[str] = Field( default=None, description="Feature Instance ID for instance-level isolation", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # System attributes are automatically set by DatabaseConnector registerModelLabels( "TrusteeAccess", {"en": "Access", "fr": "Accès", "de": "Zugriff"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"}, "roleId": {"en": "Role", "fr": "Rôle", "de": "Rolle"}, "userId": {"en": "User", "fr": "Utilisateur", "de": "Benutzer"}, "contractId": {"en": "Contract (optional)", "fr": "Contrat (optionnel)", "de": "Vertrag (optional)"}, "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"}, "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"}, }, ) class TrusteeContract(BaseModel): """Defines customer contracts within organisations.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Unique contract ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) organisationId: str = Field( description="Reference to TrusteeOrganisation.id (immutable after creation)", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, # Editable at creation, then readonly "frontend_required": True, "frontend_options": "/api/trustee/{instanceId}/organisations/options" } ) 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 } ) mandateId: Optional[str] = Field( default=None, description="Mandate ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) featureInstanceId: Optional[str] = Field( default=None, description="Feature Instance ID for instance-level isolation", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) # System attributes are automatically set by DatabaseConnector registerModelLabels( "TrusteeContract", {"en": "Contract", "fr": "Contrat", "de": "Vertrag"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"}, "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"}, "enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"}, "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"}, "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"}, }, ) class TrusteeDocumentTypeEnum(str, Enum): """Document type for trustee documents (expense extraction, ingest, sync).""" INVOICE = "invoice" EXPENSE_RECEIPT = "expense_receipt" BANK_DOCUMENT = "bank_document" CONTRACT = "contract" UNKNOWN = "unknown" AUTO = "auto" class TrusteeDocument(BaseModel): """Contains document references for bookings. Documents reference files in the central Files table via fileId. This allows file content to be stored once and referenced by multiple features. Note: organisationId and contractId removed as per architecture decision: - The feature instance IS the organisation - Contracts are eliminated from the model """ id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Unique document ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) fileId: Optional[str] = Field( default=None, description="Reference to central Files table (Files.id)", json_schema_extra={ "frontend_type": "file_reference", "frontend_readonly": False, "frontend_required": False } ) 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( default="application/octet-stream", description="MIME type of the document", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": "/api/trustee/mime-types/options" } ) sourceType: Optional[str] = Field( default=None, description="Source type (e.g., 'sharepoint', 'upload', 'email')", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) sourceLocation: Optional[str] = Field( default=None, description="Original source location (e.g., SharePoint path)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) mandateId: Optional[str] = Field( default=None, description="Mandate ID (auto-set from context)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_hidden": True } ) featureInstanceId: Optional[str] = Field( default=None, description="Feature Instance ID for instance-level isolation (auto-set from context)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_hidden": True } ) documentType: Optional[str] = Field( default=None, description="Document type (e.g. invoice, expense_receipt, bank_document, contract); use TrusteeDocumentTypeEnum values", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": False } ) externalBelegId: Optional[str] = Field( default=None, description="External Beleg-ID in accounting system (e.g. RMA); set on first successful upload, reused on re-sync", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_hidden": True } ) # System attributes are automatically set by DatabaseConnector registerModelLabels( "TrusteeDocument", {"en": "Document", "fr": "Document", "de": "Dokument"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "fileId": {"en": "File Reference", "fr": "Référence du fichier", "de": "Datei-Referenz"}, "documentName": {"en": "Document Name", "fr": "Nom du document", "de": "Dokumentname"}, "documentMimeType": {"en": "MIME Type", "fr": "Type MIME", "de": "MIME-Typ"}, "sourceType": {"en": "Source Type", "fr": "Type de source", "de": "Quelltyp"}, "sourceLocation": {"en": "Source Location", "fr": "Emplacement source", "de": "Quellort"}, "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"}, "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"}, "documentType": {"en": "Document Type", "fr": "Type de document", "de": "Dokumenttyp"}, "externalBelegId": {"en": "Beleg ID (Accounting)", "fr": "ID Beleg (Comptabilité)", "de": "Beleg-ID (Buchhaltung)"}, }, ) class TrusteePosition(BaseModel): """Contains booking positions (expense entries). A position can have up to two document references: documentId (Beleg) and bankDocumentId (Bank-Referenz). One document (e.g. bank statement) can generate many positions. """ id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Unique position ID", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False } ) documentId: Optional[str] = Field( default=None, description="Reference to TrusteeDocument.id (Beleg / primary document)", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": "/api/trustee/{instanceId}/documents/options" } ) bankDocumentId: Optional[str] = Field( default=None, description="Reference to TrusteeDocument.id (Bank-Referenz / second document)", json_schema_extra={ "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": "/api/trustee/{instanceId}/documents/options" } ) valuta: Optional[str] = Field( default=None, description="Value date (ISO format: YYYY-MM-DD)", json_schema_extra={ "frontend_type": "date", "frontend_readonly": False, "frontend_required": True } ) transactionDateTime: Optional[float] = Field( default=None, description="Transaction timestamp (UTC timestamp in seconds)", 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)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": False } ) bookingCurrency: str = Field( default="CHF", 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", "de": "CHF"}}, {"value": "EUR", "label": {"en": "EUR", "fr": "EUR", "de": "EUR"}}, {"value": "USD", "label": {"en": "USD", "fr": "USD", "de": "USD"}}, {"value": "GBP", "label": {"en": "GBP", "fr": "GBP", "de": "GBP"}}, ] } ) bookingAmount: float = Field( default=0.0, description="Booking amount", json_schema_extra={ "frontend_type": "number", "frontend_readonly": False, "frontend_required": True } ) originalCurrency: str = Field( default="CHF", 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", "de": "CHF"}}, {"value": "EUR", "label": {"en": "EUR", "fr": "EUR", "de": "EUR"}}, {"value": "USD", "label": {"en": "USD", "fr": "USD", "de": "USD"}}, {"value": "GBP", "label": {"en": "GBP", "fr": "GBP", "de": "GBP"}}, ] } ) originalAmount: float = Field( default=0.0, description="Original amount (manual input, no automatic currency conversion)", 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 (calculated: bookingAmount * vatPercentage / 100, can be manually overridden)", json_schema_extra={ "frontend_type": "number", "frontend_readonly": False, "frontend_required": False } ) debitAccountNumber: Optional[str] = Field( default=None, description="Debit account number (e.g. '4200' for expenses)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": False } ) creditAccountNumber: Optional[str] = Field( default=None, description="Credit account number (e.g. '1020' for bank)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": False } ) taxCode: Optional[str] = Field( default=None, description="Tax code for the accounting system", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": False } ) costCenter: Optional[str] = Field( default=None, description="Cost center identifier", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": False } ) bookingReference: Optional[str] = Field( default=None, description="Booking reference (e.g. voucher number)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": False, "frontend_required": False } ) mandateId: Optional[str] = Field( default=None, description="Mandate ID (auto-set from context)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_hidden": True } ) featureInstanceId: Optional[str] = Field( default=None, description="Feature Instance ID for instance-level isolation (auto-set from context)", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_hidden": True } ) accountingSyncId: Optional[str] = Field( default=None, description="External ID (UUID) of the synced record in the accounting system; set by sync, used for duplicate check", json_schema_extra={ "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_hidden": True } ) # Allow extra fields like _createdAt from database model_config = {"extra": "allow"} registerModelLabels( "TrusteePosition", {"en": "Position", "fr": "Position", "de": "Position"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "documentId": {"en": "Document", "fr": "Document", "de": "Dokument"}, "bankDocumentId": {"en": "Bank Reference", "fr": "Référence bancaire", "de": "Bank-Referenz"}, "valuta": {"en": "Value Date", "fr": "Date de valeur", "de": "Valutadatum"}, "transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction", "de": "Transaktionszeitpunkt"}, "company": {"en": "Company", "fr": "Entreprise", "de": "Firma"}, "desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"}, "tags": {"en": "Tags", "fr": "Tags", "de": "Tags"}, "bookingCurrency": {"en": "Booking Currency", "fr": "Devise de comptabilisation", "de": "Buchungswährung"}, "bookingAmount": {"en": "Booking Amount", "fr": "Montant de comptabilisation", "de": "Buchungsbetrag"}, "originalCurrency": {"en": "Original Currency", "fr": "Devise d'origine", "de": "Originalwährung"}, "originalAmount": {"en": "Original Amount", "fr": "Montant d'origine", "de": "Originalbetrag"}, "vatPercentage": {"en": "VAT Percentage", "fr": "Pourcentage TVA", "de": "MwSt-Prozentsatz"}, "vatAmount": {"en": "VAT Amount", "fr": "Montant TVA", "de": "MwSt-Betrag"}, "debitAccountNumber": {"en": "Debit Account", "fr": "Compte débit", "de": "Soll-Konto"}, "creditAccountNumber": {"en": "Credit Account", "fr": "Compte crédit", "de": "Haben-Konto"}, "taxCode": {"en": "Tax Code", "fr": "Code TVA", "de": "Steuercode"}, "costCenter": {"en": "Cost Center", "fr": "Centre de coûts", "de": "Kostenstelle"}, "bookingReference": {"en": "Booking Reference", "fr": "Référence de réservation", "de": "Buchungsreferenz"}, "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"}, "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"}, "accountingSyncId": {"en": "Accounting Sync ID", "fr": "ID sync comptabilité", "de": "Buha-Sync-ID"}, }, ) class TrusteeAccountingConfig(BaseModel): """Per-instance accounting system configuration with encrypted credentials. Each feature instance can connect to exactly one accounting system. Credentials are stored encrypted (decrypted at runtime by the AccountingBridge). """ id: str = Field(default_factory=lambda: str(uuid.uuid4())) featureInstanceId: str = Field(description="FK -> FeatureInstance.id (1:1)") connectorType: str = Field(description="Connector type key, e.g. 'rma', 'bexio', 'abacus'") displayLabel: str = Field(default="", description="User-visible label for this integration") encryptedConfig: str = Field(default="", description="Encrypted JSON blob with connector credentials") isActive: bool = Field(default=True) lastSyncAt: Optional[float] = Field(default=None, description="Timestamp of last sync attempt") lastSyncStatus: Optional[str] = Field(default=None, description="Last sync result: success, error, partial") lastSyncErrorMessage: Optional[str] = Field(default=None, description="Error message when lastSyncStatus is error") mandateId: Optional[str] = Field(default=None) registerModelLabels( "TrusteeAccountingConfig", {"en": "Accounting Configuration", "de": "Buchhaltungs-Konfiguration", "fr": "Configuration comptable"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "featureInstanceId": {"en": "Feature Instance", "fr": "Instance", "de": "Feature-Instanz"}, "connectorType": {"en": "System", "fr": "Système", "de": "System"}, "displayLabel": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"}, "isActive": {"en": "Active", "fr": "Actif", "de": "Aktiv"}, "lastSyncAt": {"en": "Last Sync", "fr": "Dernière sync.", "de": "Letzte Synchronisation"}, "lastSyncStatus": {"en": "Status", "fr": "Statut", "de": "Status"}, "lastSyncErrorMessage": {"en": "Error", "fr": "Erreur", "de": "Fehlermeldung"}, "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"}, }, ) class TrusteeAccountingSync(BaseModel): """Tracks which position was synced to which external system and when. Used for duplicate prevention, audit trail, and retry logic. """ id: str = Field(default_factory=lambda: str(uuid.uuid4())) positionId: str = Field(description="FK -> TrusteePosition.id") featureInstanceId: str = Field(description="FK -> FeatureInstance.id") connectorType: str = Field(description="Connector type at time of sync") externalId: Optional[str] = Field(default=None, description="ID assigned by the external system") externalReference: Optional[str] = Field(default=None, description="Reference in the external system") syncStatus: str = Field(default="pending", description="pending | synced | error | cancelled") syncDirection: str = Field(default="push", description="push (local->ext) or pull (ext->local)") syncedAt: Optional[float] = Field(default=None, description="Timestamp of successful sync") errorMessage: Optional[str] = Field(default=None) bookingPayload: Optional[dict] = Field(default=None, description="Payload sent to the external system (audit)") mandateId: Optional[str] = Field(default=None) registerModelLabels( "TrusteeAccountingSync", {"en": "Accounting Sync", "de": "Buchhaltungs-Synchronisation", "fr": "Synchronisation comptable"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "positionId": {"en": "Position", "fr": "Position", "de": "Position"}, "connectorType": {"en": "System", "fr": "Système", "de": "System"}, "externalId": {"en": "External ID", "fr": "ID Externe", "de": "Externe ID"}, "syncStatus": {"en": "Status", "fr": "Statut", "de": "Status"}, "syncDirection": {"en": "Direction", "fr": "Direction", "de": "Richtung"}, "syncedAt": {"en": "Synced At", "fr": "Synchronisé à", "de": "Synchronisiert am"}, "errorMessage": {"en": "Error", "fr": "Erreur", "de": "Fehler"}, "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"}, }, )