# mypy: disable-error-code="arg-type,assignment,union-attr" """ Email Template version service — version workflow (draft/review/approve/ reject/publish/preview/send-test). Phase 1 Step 4: extracted from ``compliance.api.email_template_routes``. Template-level CRUD + settings + stats live in ``compliance.services.email_template_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, EmailTemplateApprovalDB, EmailTemplateDB, EmailTemplateVersionDB, ) from compliance.domain import ConflictError, NotFoundError, ValidationError from compliance.schemas.email_template import ( PreviewRequest, SendTestRequest, VersionCreate, VersionUpdate, ) from compliance.services.email_template_service import ( _render_template, _version_to_dict, ) class EmailTemplateVersionService: """Business logic for email-template version workflow + preview + test-send.""" def __init__(self, db: Session) -> None: self.db = db # ------------------------------------------------------------------ # Internal lookups # ------------------------------------------------------------------ @staticmethod def _parse_template_uuid(template_id: str) -> uuid.UUID: try: return uuid.UUID(template_id) except ValueError as exc: raise ValidationError("Invalid template ID") from exc @staticmethod def _parse_version_uuid(version_id: str) -> uuid.UUID: try: return uuid.UUID(version_id) except ValueError as exc: raise ValidationError("Invalid version ID") from exc def _template_or_raise( self, tenant_id: str, tid: uuid.UUID ) -> EmailTemplateDB: template = ( self.db.query(EmailTemplateDB) .filter( EmailTemplateDB.id == tid, EmailTemplateDB.tenant_id == uuid.UUID(tenant_id), ) .first() ) if not template: raise NotFoundError("Template not found") return template def _version_or_raise(self, vid: uuid.UUID) -> EmailTemplateVersionDB: v = ( self.db.query(EmailTemplateVersionDB) .filter(EmailTemplateVersionDB.id == vid) .first() ) if not v: raise NotFoundError("Version not found") return v # ------------------------------------------------------------------ # Create / list / get versions # ------------------------------------------------------------------ def create_version( self, tenant_id: str, template_id: str, body: VersionCreate ) -> dict[str, Any]: tid = self._parse_template_uuid(template_id) self._template_or_raise(tenant_id, tid) v = EmailTemplateVersionDB( template_id=tid, version=body.version, language=body.language, subject=body.subject, body_html=body.body_html, body_text=body.body_text, status="draft", ) self.db.add(v) self.db.commit() self.db.refresh(v) return _version_to_dict(v) def list_versions( self, tenant_id: str, template_id: str ) -> list[dict[str, Any]]: tid = self._parse_template_uuid(template_id) self._template_or_raise(tenant_id, tid) versions = ( self.db.query(EmailTemplateVersionDB) .filter(EmailTemplateVersionDB.template_id == tid) .order_by(EmailTemplateVersionDB.created_at.desc()) .all() ) return [_version_to_dict(v) for v in versions] def get_version(self, version_id: str) -> dict[str, Any]: vid = self._parse_version_uuid(version_id) return _version_to_dict(self._version_or_raise(vid)) def update_version( self, version_id: str, body: VersionUpdate ) -> dict[str, Any]: vid = self._parse_version_uuid(version_id) v = self._version_or_raise(vid) if v.status != "draft": raise ValidationError("Only draft versions can be edited") if body.subject is not None: v.subject = body.subject if body.body_html is not None: v.body_html = body.body_html if body.body_text is not None: v.body_text = body.body_text self.db.commit() self.db.refresh(v) return _version_to_dict(v) # ------------------------------------------------------------------ # Workflow transitions # ------------------------------------------------------------------ def submit(self, version_id: str) -> dict[str, Any]: vid = self._parse_version_uuid(version_id) v = self._version_or_raise(vid) if v.status != "draft": raise ValidationError("Only draft versions can be submitted") v.status = "review" v.submitted_at = datetime.now(timezone.utc) v.submitted_by = "admin" self.db.commit() self.db.refresh(v) return _version_to_dict(v) def approve(self, version_id: str, comment: Optional[str]) -> dict[str, Any]: vid = self._parse_version_uuid(version_id) v = self._version_or_raise(vid) if v.status != "review": raise ValidationError("Only review versions can be approved") v.status = "approved" self.db.add( EmailTemplateApprovalDB( version_id=vid, action="approve", comment=comment, approved_by="admin", ) ) self.db.commit() self.db.refresh(v) return _version_to_dict(v) def reject(self, version_id: str, comment: Optional[str]) -> dict[str, Any]: vid = self._parse_version_uuid(version_id) v = self._version_or_raise(vid) if v.status != "review": raise ValidationError("Only review versions can be rejected") v.status = "draft" self.db.add( EmailTemplateApprovalDB( version_id=vid, action="reject", comment=comment, approved_by="admin", ) ) self.db.commit() self.db.refresh(v) return _version_to_dict(v) def publish(self, version_id: str) -> dict[str, Any]: vid = self._parse_version_uuid(version_id) v = self._version_or_raise(vid) if v.status not in ("approved", "review", "draft"): raise ValidationError("Version cannot be published") v.status = "published" v.published_at = datetime.now(timezone.utc) v.published_by = "admin" self.db.commit() self.db.refresh(v) return _version_to_dict(v) # ------------------------------------------------------------------ # Preview + test send # ------------------------------------------------------------------ def preview(self, version_id: str, body: PreviewRequest) -> dict[str, Any]: vid = self._parse_version_uuid(version_id) v = self._version_or_raise(vid) variables = dict(body.variables or {}) template = ( self.db.query(EmailTemplateDB) .filter(EmailTemplateDB.id == v.template_id) .first() ) if template and template.variables: for var in list(template.variables): if var not in variables: variables[var] = f"[{var}]" return { "subject": _render_template(v.subject, variables), "body_html": _render_template(v.body_html, variables), "variables_used": variables, } def send_test( self, tenant_id: str, version_id: str, body: SendTestRequest ) -> dict[str, Any]: vid = self._parse_version_uuid(version_id) v = self._version_or_raise(vid) template = ( self.db.query(EmailTemplateDB) .filter(EmailTemplateDB.id == v.template_id) .first() ) variables = body.variables or {} rendered_subject = _render_template(v.subject, variables) self.db.add( EmailSendLogDB( tenant_id=uuid.UUID(tenant_id), template_type=template.template_type if template else "unknown", version_id=vid, recipient=body.recipient, subject=rendered_subject, status="test_sent", variables=variables, ) ) self.db.commit() return { "success": True, "message": f"Test-E-Mail an {body.recipient} gesendet (Simulation)", "subject": rendered_subject, }