# mypy: disable-error-code="arg-type,assignment,union-attr" """ Email Template service — templates CRUD, settings, logs, stats, initialization. Phase 1 Step 4: extracted from ``compliance.api.email_template_routes``. Version workflow (draft/review/approve/reject/publish/preview/send-test) lives in ``compliance.services.email_template_version_service``. """ import uuid from datetime import datetime, timezone from typing import Any, Optional from sqlalchemy.orm import Session from compliance.db.email_template_models import ( EmailSendLogDB, EmailTemplateDB, EmailTemplateSettingsDB, EmailTemplateVersionDB, ) from compliance.domain import ConflictError, NotFoundError, ValidationError from compliance.schemas.email_template import SettingsUpdate, TemplateCreate # Template type catalog — shared across both services and the route module. TEMPLATE_TYPES: dict[str, dict[str, Any]] = { "welcome": {"name": "Willkommen", "category": "general", "variables": ["user_name", "company_name", "login_url"]}, "verification": {"name": "E-Mail-Verifizierung", "category": "general", "variables": ["user_name", "verification_url", "expiry_hours"]}, "password_reset": {"name": "Passwort zuruecksetzen", "category": "general", "variables": ["user_name", "reset_url", "expiry_hours"]}, "dsr_receipt": {"name": "DSR Eingangsbestaetigung", "category": "dsr", "variables": ["requester_name", "reference_number", "request_type", "deadline"]}, "dsr_identity_request": {"name": "DSR Identitaetsanfrage", "category": "dsr", "variables": ["requester_name", "reference_number"]}, "dsr_completion": {"name": "DSR Abschluss", "category": "dsr", "variables": ["requester_name", "reference_number", "request_type", "completion_date"]}, "dsr_rejection": {"name": "DSR Ablehnung", "category": "dsr", "variables": ["requester_name", "reference_number", "rejection_reason", "legal_basis"]}, "dsr_extension": {"name": "DSR Fristverlaengerung", "category": "dsr", "variables": ["requester_name", "reference_number", "new_deadline", "extension_reason"]}, "consent_request": {"name": "Einwilligungsanfrage", "category": "consent", "variables": ["user_name", "purpose", "consent_url"]}, "consent_confirmation": {"name": "Einwilligungsbestaetigung", "category": "consent", "variables": ["user_name", "purpose", "consent_date"]}, "consent_withdrawal": {"name": "Widerruf bestaetigt", "category": "consent", "variables": ["user_name", "purpose", "withdrawal_date"]}, "consent_reminder": {"name": "Einwilligungs-Erinnerung", "category": "consent", "variables": ["user_name", "purpose", "expiry_date"]}, "breach_notification_authority": {"name": "Datenpanne Aufsichtsbehoerde", "category": "breach", "variables": ["incident_date", "incident_description", "affected_count", "measures_taken", "authority_name"]}, "breach_notification_affected": {"name": "Datenpanne Betroffene", "category": "breach", "variables": ["user_name", "incident_date", "incident_description", "measures_taken", "contact_info"]}, "breach_internal": {"name": "Datenpanne intern", "category": "breach", "variables": ["reporter_name", "incident_date", "incident_description", "severity"]}, "vendor_dpa_request": {"name": "AVV-Anfrage", "category": "vendor", "variables": ["vendor_name", "contact_name", "deadline", "requirements"]}, "vendor_review_reminder": {"name": "Vendor-Pruefung Erinnerung", "category": "vendor", "variables": ["vendor_name", "review_due_date", "last_review_date"]}, "training_invitation": {"name": "Schulungseinladung", "category": "training", "variables": ["user_name", "training_title", "training_date", "training_url"]}, "training_reminder": {"name": "Schulungs-Erinnerung", "category": "training", "variables": ["user_name", "training_title", "deadline"]}, "training_completion": {"name": "Schulung abgeschlossen", "category": "training", "variables": ["user_name", "training_title", "completion_date", "certificate_url"]}, } VALID_STATUSES = ["draft", "review", "approved", "published"] VALID_CATEGORIES = ["general", "dsr", "consent", "breach", "vendor", "training"] def _template_to_dict( t: EmailTemplateDB, latest_version: Optional[EmailTemplateVersionDB] = None ) -> dict[str, Any]: result: dict[str, Any] = { "id": str(t.id), "tenant_id": str(t.tenant_id), "template_type": t.template_type, "name": t.name, "description": t.description, "category": t.category, "is_active": t.is_active, "sort_order": t.sort_order, "variables": t.variables or [], "created_at": t.created_at.isoformat() if t.created_at else None, "updated_at": t.updated_at.isoformat() if t.updated_at else None, } if latest_version: result["latest_version"] = _version_to_dict(latest_version) return result def _version_to_dict(v: EmailTemplateVersionDB) -> dict[str, Any]: return { "id": str(v.id), "template_id": str(v.template_id), "version": v.version, "language": v.language, "subject": v.subject, "body_html": v.body_html, "body_text": v.body_text, "status": v.status, "submitted_at": v.submitted_at.isoformat() if v.submitted_at else None, "submitted_by": v.submitted_by, "published_at": v.published_at.isoformat() if v.published_at else None, "published_by": v.published_by, "created_at": v.created_at.isoformat() if v.created_at else None, "created_by": v.created_by, } def _render_template(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 EmailTemplateService: """Business logic for templates, settings, logs, stats, initialization.""" def __init__(self, db: Session) -> None: self.db = db # ------------------------------------------------------------------ # Type catalog + defaults # ------------------------------------------------------------------ @staticmethod def list_types() -> list[dict[str, Any]]: return [ { "type": ttype, "name": info["name"], "category": info["category"], "variables": info["variables"], } for ttype, info in TEMPLATE_TYPES.items() ] @staticmethod def default_content(template_type: str) -> dict[str, Any]: if template_type not in TEMPLATE_TYPES: raise NotFoundError(f"Unknown template type: {template_type}") info = TEMPLATE_TYPES[template_type] vars_html = " ".join( f'{{{{{v}}}}}' for v in info["variables"] ) return { "template_type": template_type, "name": info["name"], "category": info["category"], "variables": info["variables"], "default_subject": f"{info['name']} - {{{{company_name}}}}", "default_body_html": ( f"
Sehr geehrte(r) {{{{user_name}}}},
\n" f"[Inhalt hier einfuegen]
\n" f"Verfuegbare Variablen: {vars_html}
\n" f"Mit freundlichen Gruessen
{{{{sender_name}}}}