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>
This commit is contained in:
@@ -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"),
|
||||
}
|
||||
Reference in New Issue
Block a user