diff --git a/backend-compliance/compliance/services/dsr_art11_service.py b/backend-compliance/compliance/services/dsr_art11_service.py
index 1791715..8215294 100644
--- a/backend-compliance/compliance/services/dsr_art11_service.py
+++ b/backend-compliance/compliance/services/dsr_art11_service.py
@@ -75,33 +75,26 @@ class DSRArt11Service:
if not dsr.requester_email:
return
try:
- from compliance.services.smtp_sender import send_email
- send_email(
+ from compliance.services.email_delivery_service import EmailDeliveryService
+ delivery = EmailDeliveryService(self._db)
+ variables = {
+ "requester_name": dsr.requester_name or "Antragsteller/in",
+ "reference_number": dsr.request_number or "",
+ "rejection_reason": "Identifikation nicht moeglich — Art. 11 Abs. 1 DSGVO",
+ "legal_basis": "Art. 11 Abs. 1 DSGVO",
+ "sender_name": "Datenschutzbeauftragter",
+ }
+ # Use published dsr_rejection template, fallback to inline
+ delivery.send(
+ tenant_id=str(dsr.tenant_id),
+ template_type="dsr_rejection",
recipient=dsr.requester_email,
- subject=f"Zu Ihrer Anfrage {dsr.request_number} — Art. 11 DSGVO",
- body_html=f"""
-
-
-
Mitteilung zu Ihrer Anfrage
-
-
-
Sehr geehrte/r Antragsteller/in,
-
wir haben Ihre Anfrage ({dsr.request_number}) gemaess Art. 15 DSGVO
- erhalten und geprueft.
-
Leider koennen wir die bei uns gespeicherten Daten (anonymisierte
- Cookies, IP-Hashes) keiner identifizierbaren Person zuordnen.
-
Gemaess Art. 11 Abs. 1 DSGVO sind wir nicht verpflichtet,
- zusaetzliche Informationen zu erheben, um Sie zu identifizieren.
- Eine Auskunftserteilung ist daher nicht moeglich.
-
Sollten Sie ueber ein Kundenkonto bei uns verfuegen, koennen Sie
- die Anfrage unter Angabe Ihrer Kundennummer erneut einreichen.
-
- Mit freundlichen Gruessen
- Datenschutzbeauftragter
-
-
-
- """,
+ variables=variables,
+ fallback_subject=f"Zu Ihrer Anfrage {dsr.request_number} — Art. 11 DSGVO",
+ fallback_html=f"""Sehr geehrte/r {dsr.requester_name or 'Antragsteller/in'},
+ wir koennen die bei uns gespeicherten Daten keiner identifizierbaren Person zuordnen.
+ Gemaess Art. 11 Abs. 1 DSGVO ist eine Auskunftserteilung nicht moeglich.
+ Mit freundlichen Gruessen
Datenschutzbeauftragter
""",
)
except Exception as e:
logger.warning("Art. 11 notification failed: %s", e)
diff --git a/backend-compliance/compliance/services/email_delivery_service.py b/backend-compliance/compliance/services/email_delivery_service.py
new file mode 100644
index 0000000..313028a
--- /dev/null
+++ b/backend-compliance/compliance/services/email_delivery_service.py
@@ -0,0 +1,122 @@
+"""
+Email Template Delivery Service — the missing integration layer.
+
+Combines: template loading → published version → variable rendering → SMTP → audit log.
+Used by DSR workflow, document reviews, and other modules that need to send
+templated emails.
+"""
+
+import logging
+import uuid
+from typing import Any, Optional
+
+from sqlalchemy.orm import Session
+
+from compliance.db.email_template_models import (
+ EmailSendLogDB,
+ EmailTemplateDB,
+ EmailTemplateVersionDB,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def _render(html: str, variables: dict[str, str]) -> str:
+ """Replace {{variable}} placeholders with values."""
+ result = html
+ for key, value in variables.items():
+ result = result.replace(f"{{{{{key}}}}}", str(value))
+ return result
+
+
+class EmailDeliveryService:
+ """Load template → render → send via SMTP → log."""
+
+ def __init__(self, db: Session) -> None:
+ self.db = db
+
+ def get_published_version(
+ self, tenant_id: str, template_type: str,
+ ) -> Optional[EmailTemplateVersionDB]:
+ """Get the latest published version of a template by type."""
+ tid = uuid.UUID(tenant_id)
+ template = (
+ self.db.query(EmailTemplateDB)
+ .filter(EmailTemplateDB.tenant_id == tid, EmailTemplateDB.template_type == template_type)
+ .first()
+ )
+ if not template:
+ return None
+ return (
+ self.db.query(EmailTemplateVersionDB)
+ .filter(
+ EmailTemplateVersionDB.template_id == template.id,
+ EmailTemplateVersionDB.status == "published",
+ )
+ .order_by(EmailTemplateVersionDB.created_at.desc())
+ .first()
+ )
+
+ def send(
+ self,
+ tenant_id: str,
+ template_type: str,
+ recipient: str,
+ variables: dict[str, str],
+ fallback_subject: Optional[str] = None,
+ fallback_html: Optional[str] = None,
+ ) -> dict[str, Any]:
+ """Send a templated email. Falls back to inline HTML if no published template.
+
+ Args:
+ tenant_id: Tenant UUID string.
+ template_type: E.g. 'dsr_receipt', 'dsr_completion'.
+ recipient: Email address.
+ variables: Dict of {{key}}: value for rendering.
+ fallback_subject: Subject if no template found.
+ fallback_html: HTML body if no template found.
+ """
+ from compliance.services.smtp_sender import send_email
+
+ tid = uuid.UUID(tenant_id)
+ version = self.get_published_version(tenant_id, template_type)
+
+ if version:
+ subject = _render(version.subject, variables)
+ body_html = _render(version.body_html, variables)
+ version_id = version.id
+ elif fallback_subject and fallback_html:
+ subject = _render(fallback_subject, variables)
+ body_html = _render(fallback_html, variables)
+ version_id = None
+ else:
+ logger.warning("No published template for '%s' and no fallback provided", template_type)
+ return {"success": False, "error": f"No template for {template_type}"}
+
+ result = send_email(recipient=recipient, subject=subject, body_html=body_html)
+
+ # Audit log
+ try:
+ log = EmailSendLogDB(
+ tenant_id=tid,
+ template_type=template_type,
+ version_id=version_id,
+ recipient=recipient,
+ subject=subject,
+ status=result.get("status", "unknown"),
+ variables=variables,
+ error_message=result.get("error"),
+ )
+ self.db.add(log)
+ self.db.commit()
+ except Exception as e:
+ logger.warning("Failed to log email send: %s", e)
+
+ return {
+ "success": result.get("status") == "sent",
+ "template_type": template_type,
+ "recipient": recipient,
+ "subject": subject,
+ "used_template": version is not None,
+ "status": result.get("status"),
+ }
diff --git a/backend-compliance/migrations/117_dsr_email_templates_content.sql b/backend-compliance/migrations/117_dsr_email_templates_content.sql
new file mode 100644
index 0000000..42f05d7
--- /dev/null
+++ b/backend-compliance/migrations/117_dsr_email_templates_content.sql
@@ -0,0 +1,164 @@
+-- Migration 117: Professional DSR email template content
+-- Updates published versions with proper HTML for all 5 DSR template types
+
+-- Note: This updates existing template versions. If no versions exist yet,
+-- the email_delivery_service falls back to inline HTML.
+
+-- dsr_receipt: Eingangsbestaetigung
+UPDATE compliance_email_template_versions SET
+ subject = 'Eingangsbestaetigung Ihrer Anfrage {{reference_number}} ({{request_type}})',
+ body_html = '
+
+
Eingangsbestaetigung
+
Ihre Anfrage nach {{request_type}}
+
+
+
Sehr geehrte/r {{requester_name}},
+
wir bestaetigen den Eingang Ihrer Anfrage auf Datenauskunft gemaess DSGVO.
+
+| Vorgangsnummer: | {{reference_number}} |
+| Art der Anfrage: | {{request_type}} |
+| Frist: | {{deadline}} |
+
+
Wir werden Ihre Anfrage schnellstmoeglich bearbeiten und Ihnen die angeforderten Informationen innerhalb der gesetzlichen Frist zukommen lassen.
+
Bei Rueckfragen wenden Sie sich bitte unter Angabe der Vorgangsnummer an unseren Datenschutzbeauftragten.
+
Mit freundlichen Gruessen
{{sender_name}}
+
',
+ body_text = 'Eingangsbestaetigung - {{reference_number}}
+
+Sehr geehrte/r {{requester_name}},
+
+wir bestaetigen den Eingang Ihrer Anfrage ({{reference_number}}) auf {{request_type}}.
+
+Frist: {{deadline}}
+
+Mit freundlichen Gruessen
+{{sender_name}}',
+ updated_at = NOW()
+WHERE template_id IN (
+ SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_receipt'
+) AND status = 'published';
+
+-- dsr_completion: Abschluss / Datenauskunft
+UPDATE compliance_email_template_versions SET
+ subject = 'Ihre Datenauskunft {{reference_number}} — abgeschlossen',
+ body_html = '
+
+
Datenauskunft
+
Vorgangsnummer {{reference_number}}
+
+
+
Sehr geehrte/r {{requester_name}},
+
Ihre Anfrage ({{reference_number}}) vom Typ {{request_type}} wurde am {{completion_date}} abgeschlossen.
+
Die angeforderten Daten stehen Ihnen in folgenden Formaten zur Verfuegung:
+
- PDF — druckbare Uebersicht
- JSON — maschinenlesbar (Art. 20 DSGVO)
- CSV — tabellarisch
+
Sollten Sie Fragen haben oder weitere Rechte ausueben wollen (Berichtigung Art. 16, Loeschung Art. 17, Einschraenkung Art. 18), wenden Sie sich bitte erneut an uns.
+
Mit freundlichen Gruessen
{{sender_name}}
+
',
+ body_text = 'Datenauskunft - {{reference_number}}
+
+Sehr geehrte/r {{requester_name}},
+
+Ihre Anfrage ({{reference_number}}) wurde am {{completion_date}} abgeschlossen.
+
+Die Daten stehen als PDF, JSON und CSV zur Verfuegung.
+
+Mit freundlichen Gruessen
+{{sender_name}}',
+ updated_at = NOW()
+WHERE template_id IN (
+ SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_completion'
+) AND status = 'published';
+
+-- dsr_rejection: Ablehnung
+UPDATE compliance_email_template_versions SET
+ subject = 'Zu Ihrer Anfrage {{reference_number}} — Entscheidung',
+ body_html = '
+
+
Mitteilung zu Ihrer Anfrage
+
+
+
Sehr geehrte/r {{requester_name}},
+
wir haben Ihre Anfrage ({{reference_number}}) geprueft und muessen Ihnen leider mitteilen, dass wir dieser nicht nachkommen koennen.
+
+
Grund:
+
{{rejection_reason}}
+
Rechtsgrundlage: {{legal_basis}}
+
+
Sie haben das Recht, eine Beschwerde bei der zustaendigen Aufsichtsbehoerde einzureichen.
+
Mit freundlichen Gruessen
{{sender_name}}
+
',
+ body_text = 'Mitteilung zu {{reference_number}}
+
+Sehr geehrte/r {{requester_name}},
+
+wir koennen Ihrer Anfrage leider nicht nachkommen.
+
+Grund: {{rejection_reason}}
+Rechtsgrundlage: {{legal_basis}}
+
+Mit freundlichen Gruessen
+{{sender_name}}',
+ updated_at = NOW()
+WHERE template_id IN (
+ SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_rejection'
+) AND status = 'published';
+
+-- dsr_identity_request: Identitaetspruefung
+UPDATE compliance_email_template_versions SET
+ subject = 'Identitaetspruefung erforderlich — {{reference_number}}',
+ body_html = '
+
+
Identitaetspruefung erforderlich
+
+
+
Sehr geehrte/r {{requester_name}},
+
fuer die Bearbeitung Ihrer Anfrage ({{reference_number}}) benoetigen wir gemaess Art. 12 Abs. 6 DSGVO eine Bestaetigung Ihrer Identitaet.
+
Bitte senden Sie uns eines der folgenden Dokumente:
+
- Kopie Ihres Personalausweises (Vorder-/Rueckseite, Adresse + Foto duerfen geschwärzt werden)
- Screenshot Ihres Kundenkontos mit sichtbarer E-Mail-Adresse
- Antwort von der bei uns registrierten E-Mail-Adresse
+
Mit freundlichen Gruessen
{{sender_name}}
+
',
+ body_text = 'Identitaetspruefung - {{reference_number}}
+
+Sehr geehrte/r {{requester_name}},
+
+fuer Ihre Anfrage ({{reference_number}}) benoetigen wir eine Identitaetsbestaetigung.
+
+Mit freundlichen Gruessen
+{{sender_name}}',
+ updated_at = NOW()
+WHERE template_id IN (
+ SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_identity_request'
+) AND status = 'published';
+
+-- dsr_extension: Fristverlaengerung
+UPDATE compliance_email_template_versions SET
+ subject = 'Fristverlaengerung Ihrer Anfrage {{reference_number}}',
+ body_html = '
+
+
Fristverlaengerung
+
+
+
Sehr geehrte/r {{requester_name}},
+
wir informieren Sie, dass die Bearbeitung Ihrer Anfrage ({{reference_number}}) mehr Zeit in Anspruch nimmt als urspruenglich geplant.
+
Gemaess Art. 12 Abs. 3 DSGVO verlaengern wir die Frist:
+
+| Neue Frist: | {{new_deadline}} |
+| Grund: | {{extension_reason}} |
+
+
Mit freundlichen Gruessen
{{sender_name}}
+
',
+ body_text = 'Fristverlaengerung - {{reference_number}}
+
+Sehr geehrte/r {{requester_name}},
+
+die Frist fuer Ihre Anfrage wurde verlaengert.
+Neue Frist: {{new_deadline}}
+Grund: {{extension_reason}}
+
+Mit freundlichen Gruessen
+{{sender_name}}',
+ updated_at = NOW()
+WHERE template_id IN (
+ SELECT id FROM compliance_email_templates WHERE template_type = 'dsr_extension'
+) AND status = 'published';