Files
breakpilot-compliance/backend-compliance/compliance/services/email_delivery_service.py
T
Benjamin Admin eb4ea8bc42 feat: EmailDeliveryService + professional DSR email templates
- EmailDeliveryService: load template → find published version →
  render {{variables}} → send via SMTP → audit log. Fallback to
  inline HTML when no published template exists.
- Migration 117: Professional HTML/text content for all 5 DSR
  templates (receipt, completion, rejection, identity, extension)
  with branded styling and proper Art. references
- DSRArt11Service now uses EmailDeliveryService with dsr_rejection
  template instead of hardcoded HTML

[migration-approved]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 23:38:32 +02:00

123 lines
4.0 KiB
Python

"""
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"),
}