# Messaging Service Konzept ## Übersicht Das Messaging-System ermöglicht es, Nachrichten über verschiedene Kanäle (E-Mail, SMS, WhatsApp, Teams Chat, etc.) an registrierte Benutzer zu senden, basierend auf Subscriptions. Es ist mandantenbasiert und unterstützt mehrere Kanäle pro Subscription. ## Architektur-Überlegungen ### Kernkonzept Das System besteht aus zwei Hauptkomponenten: 1. **Subscription-Management**: Users können sich für Subscriptions registrieren und ihre bevorzugten Kanäle wählen 2. **Subscription-Funktionen**: Jede Subscription hat eine eigene Funktion (`subSubscriptionXxxx.py`), die komplett flexibel ist und die Nachrichten vorbereitet ### Setup-Architektur **Wichtig**: Die Datenbank mit den Subscriptions ist die **stabile Basis-Referenz** und die Grundlage des Systems. Es gibt zwei Seiten, die an die Datenbank andocken: 1. **User-Seite**: Users können sich für Subscriptions registrieren (subscribe) - Dies ist unabhängig davon, ob bereits eine Subscription-Funktion existiert - Users können sich bereits subscriben, bevor eine Funktion implementiert ist 2. **Funktions-Seite**: Subscription-Funktionen können später hinzugefügt werden - Eine Subscription-Funktion ist **optional** und kann nachträglich implementiert werden - Wenn eine Subscription-Funktion fehlt, wird beim Trigger ein Fehler geloggt, aber das System bleibt stabil - Die Datenbank-Struktur ist unabhängig von der Existenz der Funktionen **Workflow**: 1. Admin erstellt Subscription in der Datenbank (z.B. "SystemErrors") 2. Users können sich sofort für diese Subscription registrieren 3. Später kann die Subscription-Funktion (`subSubscriptionSystemErrors.py`) hinzugefügt werden 4. Erst dann können Trigger die Subscription ausführen ### Trigger-Mechanismus Subscriptions können über verschiedene Trigger ausgeführt werden: - **Trigger-Route-Endpunkt**: API-Endpunkt, der eine Subscription triggert - **Workflow-Action**: Automatischer Workflow, der eine Subscription als Event auslöst - **Scheduled Job**: Zeitgesteuerte Ausführung - **Event-basiert**: System-Events (z.B. Audit-Log-Events) **Wichtig**: Eine Subscription kann über **alle** Trigger-Typen ausgeführt werden. Es gibt keine Einschränkung pro Subscription. ### Subscription-Funktionen Jede Subscription hat eine eigene Funktion im Format `subSubscriptionXxxx.py` (z.B. `subSubscriptionSystemErrors.py`, `subSubscriptionAuditLogin.py`). **Naming-Regel**: Die `subscriptionId` muss nur Buchstaben und Unterstriche (`_`) enthalten. Sie wird direkt als Dateiname verwendet: - `subscriptionId`: "SystemErrors" → Datei: `subSubscriptionSystemErrors.py` - `subscriptionId`: "audit_login" → Datei: `subSubscriptionAuditLogin.py` **Validierung**: Bei der Erstellung einer Subscription wird geprüft, dass `subscriptionId` nur Buchstaben und `_` enthält. Diese Funktionen: - Erhalten Event-Parameter vom Trigger (als Pydantic Model) - Erhalten bereits die Registrierungen (werden vor dem Funktionsaufruf geholt) - Bereiten die Nachrichten vor (können pro Kanal unterschiedlich sein) - Rufen `sendMessage` für jeden Channel auf - Haben vollständige Flexibilität bei der Nachrichtenerstellung **Beispiel-Struktur:** ```python # modules/services/serviceMessaging/subscriptions/subSubscriptionSystemErrors.py from typing import List from modules.datamodels.datamodelMessaging import ( MessagingEventParameters, MessagingSubscriptionExecutionResult, MessagingSubscriptionRegistration, MessagingChannel ) def execute( eventParameters: MessagingEventParameters, registrations: List[MessagingSubscriptionRegistration], messagingService ) -> MessagingSubscriptionExecutionResult: """ Subscription-Funktion für System-Errors. Erhält eventParameters vom Trigger und registrations bereits geholt. """ # Gruppiere nach Channel emailRegistrations = [r for r in registrations if r.channel == MessagingChannel.EMAIL] smsRegistrations = [r for r in registrations if r.channel == MessagingChannel.SMS] # Bereite Nachrichten vor (können pro Channel unterschiedlich sein) emailSubject = "System Error Report" errors = eventParameters.triggerData.get('errors', []) emailMessage = f"System errors detected: {errors}" smsMessage = f"System Error: {len(errors)} errors detected" messagesSent = 0 # Versende über sendMessage for reg in emailRegistrations: sendResult = messagingService.sendMessage( subject=emailSubject, message=emailMessage, registration=reg ) if sendResult.success: messagesSent += 1 for reg in smsRegistrations: sendResult = messagingService.sendMessage( subject="", # SMS hat kein Subject message=smsMessage, registration=reg ) if sendResult.success: messagesSent += 1 return MessagingSubscriptionExecutionResult( success=True, messagesSent=messagesSent ) ``` ## Datenmodell ### 1. MessagingChannel (Enum) ```python class MessagingChannel(str, Enum): EMAIL = "email" SMS = "sms" WHATSAPP = "whatsapp" TEAMS_CHAT = "teams_chat" # Weitere Kanäle können hier hinzugefügt werden ``` ### 2. MessagingSubscription - `id`: UUID - `subscriptionId`: String (eindeutiger Identifier, z.B. "SystemErrors", "audit_login") - **Validierung**: Nur Buchstaben und `_` erlaubt - `subscriptionLabel`: String (Anzeigename) - `mandateId`: String (Mandanten-ID - wird automatisch vom Interface gesetzt) - `description`: Optional[String] - `isSystemSubscription`: Boolean (nur Admin kann System-Subscriptions erstellen) - `enabled`: Boolean - System-Felder: `creationDate`, `lastModified`, `createdBy`, `modifiedBy` ### 3. MessagingSubscriptionRegistration - `id`: UUID - `subscriptionId`: String (Referenz zur Subscription) - `userId`: String (Referenz zum User) - `channel`: MessagingChannel - `channelConfig`: String (z.B. E-Mail-Adresse, Telefonnummer, Teams User ID) - `enabled`: Boolean (User kann sich temporär deaktivieren) - System-Felder: `creationDate`, `lastModified` ### 4. MessagingDelivery - `id`: UUID - `subscriptionId`: String (Referenz zur Subscription) - `userId`: String (Referenz zum User) - `channel`: MessagingChannel - `status`: Enum (PENDING, SENT, FAILED) - `errorMessage`: Optional[String] - `sentAt`: Optional[Float] (Timestamp wenn gesendet) - System-Felder: `creationDate` ### 5. MessagingEventParameters (Pydantic Model) - `triggerData`: dict - Event-Daten vom Trigger als Dictionary/JSON ### 6. MessagingSendResult (Pydantic Model) - `success`: Boolean - `deliveryId`: Optional[String] (ID des MessagingDelivery Records) - `errorMessage`: Optional[String] ### 7. MessagingSubscriptionExecutionResult (Pydantic Model) - `success`: Boolean - `messagesSent`: Integer - `errorMessage`: Optional[String] - `extra="allow"` für zusätzliche Felder ## RBAC-Berechtigungsmodell ### Access Rules für MessagingSubscription - **Context**: `DATA` - **Item**: `MessagingSubscription` - **Permissions**: - **Admin**: Alle CRUD-Operationen auf alle Subscriptions - **Mandate-Admin**: CRUD auf Subscriptions des eigenen Mandanten (außer System-Subscriptions) - **User**: Read auf Subscriptions des eigenen Mandanten, Create/Update/Delete nur auf eigene Registrierungen ### Access Rules für MessagingSubscriptionRegistration - **Context**: `DATA` - **Item**: `MessagingSubscriptionRegistration` - **Permissions**: - **User**: CRUD auf eigene Registrierungen - **Admin/Mandate-Admin**: Read auf alle Registrierungen des Mandanten ### Access Rules für MessagingDelivery - **Context**: `DATA` - **Item**: `MessagingDelivery` - **Permissions**: - **Admin/Mandate-Admin**: Read auf alle Deliveries - **User**: Read auf eigene Deliveries ## Service-Architektur ### serviceMessaging/mainServiceMessaging.py ```python class MessagingService: def __init__(self, services): self.services = services self._messagingInterface = None # interfaceMessaging # Core Messaging def sendMessage( self, subject: str, message: str, registration: MessagingSubscriptionRegistration ) -> MessagingSendResult: """ Sendet eine Nachricht über einen Channel an einen User. Erstellt MessagingDelivery Record. Args: subject: Subject der Nachricht (für E-Mail, leer für SMS) message: Nachrichtentext registration: MessagingSubscriptionRegistration mit Channel-Info und userId Returns: MessagingSendResult mit Status und Delivery-ID """ # Erstelle Delivery Record delivery = MessagingDelivery( subscriptionId=registration.subscriptionId, userId=registration.userId, channel=registration.channel, status=DeliveryStatus.PENDING ) # Speichere Delivery Record deliveryRecord = self.services.interfaceDbComponent.createDelivery(delivery) try: # Versende über interfaceMessaging success = self._getMessagingInterface().send( channel=registration.channel, recipient=registration.channelConfig, subject=subject, message=message ) if success: # Update Delivery Record self.services.interfaceDbComponent.updateDelivery( deliveryRecord["id"], { "status": DeliveryStatus.SENT, "sentAt": getUtcTimestamp() } ) return MessagingSendResult( success=True, deliveryId=deliveryRecord["id"] ) else: # Update Delivery Record mit Fehler self.services.interfaceDbComponent.updateDelivery( deliveryRecord["id"], { "status": DeliveryStatus.FAILED, "errorMessage": "Failed to send message" } ) return MessagingSendResult( success=False, deliveryId=deliveryRecord["id"], errorMessage="Failed to send message" ) except Exception as e: logger.error(f"Error sending message: {str(e)}") # Update Delivery Record mit Fehler self.services.interfaceDbComponent.updateDelivery( deliveryRecord["id"], { "status": DeliveryStatus.FAILED, "errorMessage": str(e) } ) return MessagingSendResult( success=False, deliveryId=deliveryRecord["id"], errorMessage=str(e) ) def executeSubscription( self, subscriptionId: str, eventParameters: MessagingEventParameters ) -> MessagingSubscriptionExecutionResult: """ Führt eine Subscription-Funktion aus. Args: subscriptionId: ID der Subscription eventParameters: Parameter vom Trigger (als Pydantic Model) Returns: MessagingSubscriptionExecutionResult Raises: ValueError: Wenn Subscription nicht existiert oder nicht enabled ist FileNotFoundError: Wenn Subscription-Funktion nicht gefunden wird """ # Prüfe ob Subscription existiert und enabled ist subscription = self.services.interfaceDbComponent.getSubscription(subscriptionId) if not subscription: raise ValueError(f"Subscription {subscriptionId} not found") if not subscription.enabled: logger.warning(f"Subscription {subscriptionId} is disabled, skipping execution") return MessagingSubscriptionExecutionResult( success=False, messagesSent=0, errorMessage="Subscription is disabled" ) # Hole alle aktiven Registrierungen für diese Subscription registrations = self._getSubscribers(subscriptionId) if not registrations: logger.info(f"No active registrations for subscription {subscriptionId}") return MessagingSubscriptionExecutionResult( success=True, messagesSent=0 ) # Lade Subscription-Funktion dynamisch subscriptionFunction = self._loadSubscriptionFunction(subscriptionId) if not subscriptionFunction: errorMsg = f"Subscription function not found for {subscriptionId}" logger.error(errorMsg) raise FileNotFoundError(errorMsg) # Führe Funktion aus mit Registrierungen try: return subscriptionFunction.execute(eventParameters, registrations, self) except Exception as e: logger.error(f"Error executing subscription {subscriptionId}: {str(e)}", exc_info=True) return MessagingSubscriptionExecutionResult( success=False, messagesSent=0, errorMessage=str(e) ) # Helper Methods def _getSubscribers( self, subscriptionId: str, channel: Optional[MessagingChannel] = None ) -> List[MessagingSubscriptionRegistration]: """Holt alle aktiven Subscriber einer Subscription""" return self.services.interfaceDbComponent.getAllRegistrations( subscriptionId=subscriptionId, filters={"enabled": True} if not channel else {"enabled": True, "channel": channel.value} ) def _loadSubscriptionFunction(self, subscriptionId: str) -> Optional[Callable]: """ Lädt die Subscription-Funktion dynamisch. Returns: Callable mit execute-Methode oder None wenn nicht gefunden Note: subscriptionId wird direkt als Dateiname verwendet (z.B. "SystemErrors" -> subSubscriptionSystemErrors.py) """ # Format: subSubscription{subscriptionId}.py functionName = f"subSubscription{subscriptionId}" moduleName = f"modules.services.serviceMessaging.subscriptions.{functionName}" try: # Dynamisches Import import importlib subscriptionModule = importlib.import_module(moduleName) return subscriptionModule except ImportError: # Funktion existiert noch nicht - das ist OK logger.debug(f"Subscription function {moduleName} not found (this is OK if not yet implemented)") return None def _getMessagingInterface(self): """Holt das Messaging-Interface (interfaceMessaging)""" if not self._messagingInterface: from modules.interfaces.interfaceMessaging import getInterface self._messagingInterface = getInterface() return self._messagingInterface ``` ## Connector-Architektur ### Überblick Für jeden Channel gibt es einen separaten Connector in `modules/connectors/`: - `connectorMessagingEmail.py` - Azure Communication Services - `connectorMessagingSms.py` - Twilio - `connectorMessagingWhatsapp.py` - WhatsApp API (zukünftig) - `connectorMessagingTeams.py` - Microsoft Teams API (zukünftig) ### Interface: interfaceMessaging.py Das Interface `modules/interfaces/interfaceMessaging.py` stellt eine einheitliche Schnittstelle bereit, die alle Connectors nach dem gleichen Schema verwendet: ```python class MessagingInterface: def send( self, channel: MessagingChannel, recipient: str, subject: str, message: str ) -> bool: """ Sendet eine Nachricht über den angegebenen Channel. Args: channel: MessagingChannel Enum recipient: Empfänger-Adresse (E-Mail, Telefonnummer, etc.) subject: Betreff (für E-Mail, leer für SMS) message: Nachrichtentext Returns: bool: True wenn erfolgreich, False bei Fehler """ # Wähle Connector basierend auf Channel if channel == MessagingChannel.EMAIL: connector = ConnectorMessagingEmail() elif channel == MessagingChannel.SMS: connector = ConnectorMessagingSms() elif channel == MessagingChannel.WHATSAPP: connector = ConnectorMessagingWhatsapp() elif channel == MessagingChannel.TEAMS_CHAT: connector = ConnectorMessagingTeams() else: logger.error(f"Unknown channel: {channel}") return False # Rufe Connector mit einheitlichem Schema auf return connector.send(recipient=recipient, subject=subject, message=message) ``` ### Connector-Struktur Jeder Connector implementiert die gleiche Schnittstelle: ```python # modules/connectors/connectorMessagingEmail.py class ConnectorMessagingEmail: def __init__(self): # Initialisiere Azure Communication Services Client pass def send(self, recipient: str, subject: str, message: str) -> bool: """ Sendet E-Mail über Azure Communication Services. Args: recipient: E-Mail-Adresse subject: Betreff message: Nachrichtentext (kann HTML enthalten) Returns: bool: True wenn erfolgreich """ # Implementierung hier pass ``` ```python # modules/connectors/connectorMessagingSms.py class ConnectorMessagingSms: def __init__(self): # Initialisiere Twilio Client pass def send(self, recipient: str, subject: str, message: str) -> bool: """ Sendet SMS über Twilio. Args: recipient: Telefonnummer (mit Ländercode) subject: Wird ignoriert (SMS hat kein Subject) message: Nachrichtentext Returns: bool: True wenn erfolgreich """ # Implementierung hier pass ``` **Vorteile**: - Einheitliches Schema für alle Channels - Einfache Erweiterung um neue Channels - Klare Trennung zwischen Service-Logik und Channel-Implementierung - Connectors können unabhängig getestet werden ## Interface-Methoden (interfaceDbComponentObjects.py) ```python # Subscription Management def getAllSubscriptions(self, pagination: Optional[PaginationParams] = None) -> Union[List[MessagingSubscription], PaginatedResult] def getSubscription(self, subscriptionId: str) -> Optional[MessagingSubscription] def getSubscriptionById(self, id: str) -> Optional[MessagingSubscription] # By UUID def createSubscription(self, subscriptionData: Dict[str, Any]) -> Dict[str, Any] def updateSubscription(self, subscriptionId: str, updateData: Dict[str, Any]) -> Dict[str, Any] def deleteSubscription(self, subscriptionId: str) -> bool # Registration Management def getAllRegistrations(self, subscriptionId: Optional[str] = None, userId: Optional[str] = None, pagination: Optional[PaginationParams] = None) -> Union[List[MessagingSubscriptionRegistration], PaginatedResult] def getRegistration(self, registrationId: str) -> Optional[MessagingSubscriptionRegistration] def createRegistration(self, registrationData: Dict[str, Any]) -> Dict[str, Any] def updateRegistration(self, registrationId: str, updateData: Dict[str, Any]) -> Dict[str, Any] def deleteRegistration(self, registrationId: str) -> bool def subscribeUser(self, subscriptionId: str, userId: str, channel: MessagingChannel, channelConfig: str) -> Dict[str, Any] def unsubscribeUser(self, subscriptionId: str, userId: str, channel: MessagingChannel) -> bool # Delivery Management def createDelivery(self, delivery: MessagingDelivery) -> Dict[str, Any] def updateDelivery(self, deliveryId: str, updateData: Dict[str, Any]) -> Dict[str, Any] def getDeliveries(self, subscriptionId: Optional[str] = None, userId: Optional[str] = None, pagination: Optional[PaginationParams] = None) -> Union[List[MessagingDelivery], PaginatedResult] def getDelivery(self, deliveryId: str) -> Optional[MessagingDelivery] ``` ## Route-Struktur (routeMessaging.py) ### Rate Limits - **Subscription Endpoints**: 60 requests/minute pro Session - **Registration Endpoints**: 60 requests/minute pro Session - **Trigger Endpoints**: 60 requests/minute pro `subscriptionId` ### Subscription Endpoints - `GET /api/messaging/subscriptions` - Liste aller Subscriptions - `POST /api/messaging/subscriptions` - Neue Subscription erstellen - `GET /api/messaging/subscriptions/{subscriptionId}` - Subscription abrufen - `PUT /api/messaging/subscriptions/{subscriptionId}` - Subscription aktualisieren - `DELETE /api/messaging/subscriptions/{subscriptionId}` - Subscription löschen ### Registration Endpoints - `GET /api/messaging/subscriptions/{subscriptionId}/registrations` - Registrierungen einer Subscription - `POST /api/messaging/subscriptions/{subscriptionId}/subscribe` - User zu Subscription hinzufügen - `DELETE /api/messaging/subscriptions/{subscriptionId}/unsubscribe` - User von Subscription entfernen - `GET /api/messaging/registrations` - Eigene Registrierungen des Users - `PUT /api/messaging/registrations/{registrationId}` - Registrierung aktualisieren (z.B. enabled/disabled) - `DELETE /api/messaging/registrations/{registrationId}` - Registrierung löschen ### Trigger Endpoints - `POST /api/messaging/trigger/{subscriptionId}` - Trigger-Endpunkt für externe Systeme/Workflows - Body: `{"eventParameters": {...}}` - Führt `executeSubscription` aus - Rate Limit: 60 requests/minute pro `subscriptionId` ### Delivery Endpoints - `GET /api/messaging/deliveries` - Delivery-Historie - `GET /api/messaging/deliveries/{deliveryId}` - Delivery abrufen ## Use Cases ### 1. Trigger-Route-Endpunkt ```python @router.post("/api/messaging/trigger/{subscriptionId}") @limiter.limit("60/minute", key_func=lambda: f"{request.path_params['subscriptionId']}") async def trigger_subscription( request: Request, subscriptionId: str, eventParameters: Dict[str, Any] = Body(...), currentUser: User = Depends(getCurrentUser) ): """Trigger-Endpunkt für externe Systeme""" # RBAC-Check: Nur Admin/Mandate-Admin kann triggern messagingService = request.app.state.services.messaging # Konvertiere Dict zu Pydantic Model eventParams = MessagingEventParameters(triggerData=eventParameters) executionResult = messagingService.executeSubscription(subscriptionId, eventParams) return executionResult ``` ### 2. Workflow-Action Workflow kann `messaging.executeSubscription` Action aufrufen mit: - `subscriptionId`: String - `eventParameters`: Dict (wird zu MessagingEventParameters konvertiert) ### 3. Scheduled Job (System Errors) ```python def _sendSystemErrorsJob(self): """Tägliches Mail an Admin mit Log-Errors""" # Sammle Errors aus Log errors = self._collectLogErrors() if errors: messagingService = self.services.messaging eventParams = MessagingEventParameters(triggerData={"errors": errors, "timestamp": getUtcTimestamp()}) messagingService.executeSubscription( subscriptionId="SystemErrors", eventParameters=eventParams ) ``` ### 4. Audit Log Events ```python # In audit_logger.py def logAuditEvent(eventType: str, userId: str, details: Dict): # ... existing audit logging ... # Trigger messaging if subscription exists if eventType == "login": messagingService = getMessagingService(getAdminUser()) eventParams = MessagingEventParameters( triggerData={ "eventType": eventType, "userId": userId, "details": details, "timestamp": getUtcTimestamp() } ) messagingService.executeSubscription( subscriptionId="audit_login", eventParameters=eventParams ) ``` ## Error Handling Fehler werden wie in anderen Modulen behandelt: - Normale Logger-Ausgabe mit `logger.error()`, `logger.warning()`, `logger.info()` - Exceptions werden geloggt mit `exc_info=True` für Stack-Traces - Keine speziellen Error-Handler, Standard-Python-Exception-Handling ## Konfiguration ### Environment Variables ```env # Email (Azure Communication Services) MESSAGING_ACS_CONNECTION_STRING=... MESSAGING_ACS_SENDER_EMAIL=... # SMS (Twilio) MESSAGING_TWILIO_ACCOUNT_SID=... MESSAGING_TWILIO_AUTH_TOKEN=... MESSAGING_TWILIO_FROM_NUMBER=... # WhatsApp (zukünftig) MESSAGING_WHATSAPP_API_KEY=... # Teams Chat (zukünftig) MESSAGING_TEAMS_APP_ID=... MESSAGING_TEAMS_APP_SECRET=... ``` ## Implementierungsreihenfolge 1. **Datenmodelle** (`datamodelMessaging.py`) ✅ 2. **Connectors** (`connectorMessagingEmail.py`, `connectorMessagingSms.py`) 3. **Interface** (`interfaceMessaging.py`) 4. **Interface-Methoden** (`interfaceDbComponentObjects.py`) 5. **Service-Implementierung** (`serviceMessaging/mainServiceMessaging.py`) 6. **Routes** (`routeMessaging.py`) 7. **Integration** (Service in `__init__.py` registrieren, Routes registrieren) 8. **Subscription-Funktionen** (`serviceMessaging/subscriptions/`) - Können nachträglich hinzugefügt werden - System funktioniert auch ohne Funktionen (Users können sich subscriben) 9. **Tests** (Unit-Tests für Service, Integration-Tests für Routes) **Wichtig**: Die Datenbank-Struktur ist die Basis. Subscription-Funktionen sind optional und können später hinzugefügt werden.