gateway/modules/services/serviceMessaging/CONCEPT.md
2025-12-09 23:25:06 +01:00

25 KiB

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:

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

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

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:

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:

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

# 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

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

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

# 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

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