compliance/api/email_template_routes.py (823 LOC) -> 295 LOC thin routes
+ 402-line EmailTemplateService + 241-line EmailTemplateVersionService +
61-line schemas file.
Two-service split along natural responsibility seam:
email_template_service.py (402 LOC):
- Template type catalog (TEMPLATE_TYPES constant)
- Template CRUD (list, create, get)
- Stats, settings, send logs, initialization, default content
- Shared _template_to_dict / _version_to_dict / _render_template helpers
email_template_version_service.py (241 LOC):
- Version CRUD (create, list, get, update)
- Workflow transitions (submit, approve, reject, publish)
- Preview and test-send
TEMPLATE_TYPES, VALID_CATEGORIES, VALID_STATUSES re-exported from the
route module for any legacy consumers.
State-transition errors use ValidationError (-> HTTPException 400) to
preserve the original handler's 400 status for "Only draft/review
versions can be ..." checks, since the existing TestClient integration
tests (47 tests) assert status_code == 400.
Verified:
- 47/47 tests/test_email_template_routes.py pass
- OpenAPI 360/484 unchanged
- mypy compliance/ -> Success on 138 source files
- email_template_routes.py 823 -> 295 LOC
- Hard-cap violations: 9 -> 8
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
303 lines
9.7 KiB
Python
303 lines
9.7 KiB
Python
"""
|
|
E-Mail-Template Routes — Benachrichtigungsvorlagen fuer DSGVO-Compliance.
|
|
|
|
Verwaltet Templates fuer DSR, Consent, Breach, Vendor und Training E-Mails.
|
|
Inklusive Versionierung, Approval-Workflow, Vorschau und Send-Logging.
|
|
|
|
Phase 1 Step 4 refactor: handlers delegate to EmailTemplateService
|
|
(templates/settings/logs/stats/initialize) and EmailTemplateVersionService
|
|
(version workflow + preview + test-send). Template types catalog is
|
|
re-exported for any legacy callers.
|
|
"""
|
|
|
|
from typing import Any, Optional
|
|
|
|
from fastapi import APIRouter, Depends, Header, Query
|
|
from sqlalchemy.orm import Session
|
|
|
|
from classroom_engine.database import get_db
|
|
from compliance.api._http_errors import translate_domain_errors
|
|
from compliance.schemas.email_template import (
|
|
PreviewRequest,
|
|
SendTestRequest,
|
|
SettingsUpdate,
|
|
TemplateCreate,
|
|
VersionCreate,
|
|
VersionUpdate,
|
|
)
|
|
from compliance.services.email_template_service import (
|
|
TEMPLATE_TYPES,
|
|
VALID_CATEGORIES,
|
|
VALID_STATUSES,
|
|
EmailTemplateService,
|
|
)
|
|
from compliance.services.email_template_version_service import (
|
|
EmailTemplateVersionService,
|
|
)
|
|
|
|
router = APIRouter(prefix="/email-templates", tags=["compliance-email-templates"])
|
|
|
|
DEFAULT_TENANT = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
|
|
|
|
|
|
def _get_tenant(x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID")) -> str:
|
|
return x_tenant_id or DEFAULT_TENANT
|
|
|
|
|
|
def get_template_service(db: Session = Depends(get_db)) -> EmailTemplateService:
|
|
return EmailTemplateService(db)
|
|
|
|
|
|
def get_version_service(db: Session = Depends(get_db)) -> EmailTemplateVersionService:
|
|
return EmailTemplateVersionService(db)
|
|
|
|
|
|
# =============================================================================
|
|
# Template Type Info (MUST be before parameterized routes)
|
|
# =============================================================================
|
|
|
|
@router.get("/types")
|
|
async def get_template_types() -> list[dict[str, Any]]:
|
|
"""Gibt alle verfuegbaren Template-Typen mit Variablen zurueck."""
|
|
return EmailTemplateService.list_types()
|
|
|
|
|
|
@router.get("/stats")
|
|
async def get_stats(
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateService = Depends(get_template_service),
|
|
) -> dict[str, Any]:
|
|
"""Statistiken ueber E-Mail-Templates."""
|
|
with translate_domain_errors():
|
|
return service.stats(tenant_id)
|
|
|
|
|
|
@router.get("/settings")
|
|
async def get_settings(
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateService = Depends(get_template_service),
|
|
) -> dict[str, Any]:
|
|
"""Globale E-Mail-Einstellungen laden."""
|
|
with translate_domain_errors():
|
|
return service.get_settings(tenant_id)
|
|
|
|
|
|
@router.put("/settings")
|
|
async def update_settings(
|
|
body: SettingsUpdate,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateService = Depends(get_template_service),
|
|
) -> dict[str, Any]:
|
|
"""Globale E-Mail-Einstellungen speichern."""
|
|
with translate_domain_errors():
|
|
return service.update_settings(tenant_id, body)
|
|
|
|
|
|
@router.get("/logs")
|
|
async def get_send_logs(
|
|
limit: int = Query(20, ge=1, le=100),
|
|
offset: int = Query(0, ge=0),
|
|
template_type: Optional[str] = Query(None),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateService = Depends(get_template_service),
|
|
) -> dict[str, Any]:
|
|
"""Send-Logs (paginiert)."""
|
|
with translate_domain_errors():
|
|
return service.send_logs(tenant_id, limit, offset, template_type)
|
|
|
|
|
|
@router.post("/initialize")
|
|
async def initialize_defaults(
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateService = Depends(get_template_service),
|
|
) -> dict[str, Any]:
|
|
"""Default-Templates fuer einen Tenant initialisieren."""
|
|
with translate_domain_errors():
|
|
return service.initialize_defaults(tenant_id)
|
|
|
|
|
|
@router.get("/default/{template_type}")
|
|
async def get_default_content(template_type: str) -> dict[str, Any]:
|
|
"""Default-Content fuer einen Template-Typ."""
|
|
with translate_domain_errors():
|
|
return EmailTemplateService.default_content(template_type)
|
|
|
|
|
|
# =============================================================================
|
|
# Template CRUD
|
|
# =============================================================================
|
|
|
|
@router.get("")
|
|
async def list_templates(
|
|
category: Optional[str] = Query(None),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateService = Depends(get_template_service),
|
|
) -> list[dict[str, Any]]:
|
|
"""Alle Templates mit letzter publizierter Version."""
|
|
with translate_domain_errors():
|
|
return service.list_templates(tenant_id, category)
|
|
|
|
|
|
@router.post("")
|
|
async def create_template(
|
|
body: TemplateCreate,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateService = Depends(get_template_service),
|
|
) -> dict[str, Any]:
|
|
"""Template erstellen."""
|
|
with translate_domain_errors():
|
|
return service.create_template(tenant_id, body)
|
|
|
|
|
|
# =============================================================================
|
|
# Version Management (static path before parameterized)
|
|
# =============================================================================
|
|
|
|
@router.post("/versions")
|
|
async def create_version(
|
|
body: VersionCreate,
|
|
template_id: str = Query(..., alias="template_id"),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Neue Version erstellen (via query param template_id)."""
|
|
with translate_domain_errors():
|
|
return service.create_version(tenant_id, template_id, body)
|
|
|
|
|
|
# =============================================================================
|
|
# Single Template (parameterized — after all static paths)
|
|
# =============================================================================
|
|
|
|
@router.get("/{template_id}")
|
|
async def get_template(
|
|
template_id: str,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateService = Depends(get_template_service),
|
|
) -> dict[str, Any]:
|
|
"""Template-Detail."""
|
|
with translate_domain_errors():
|
|
return service.get_template(tenant_id, template_id)
|
|
|
|
|
|
@router.get("/{template_id}/versions")
|
|
async def get_versions(
|
|
template_id: str,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> list[dict[str, Any]]:
|
|
"""Versionen eines Templates."""
|
|
with translate_domain_errors():
|
|
return service.list_versions(tenant_id, template_id)
|
|
|
|
|
|
@router.post("/{template_id}/versions")
|
|
async def create_version_for_template(
|
|
template_id: str,
|
|
body: VersionCreate,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Neue Version fuer ein Template erstellen."""
|
|
with translate_domain_errors():
|
|
return service.create_version(tenant_id, template_id, body)
|
|
|
|
|
|
# =============================================================================
|
|
# Version Workflow (parameterized by version_id)
|
|
# =============================================================================
|
|
|
|
@router.get("/versions/{version_id}")
|
|
async def get_version(
|
|
version_id: str,
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Version-Detail."""
|
|
with translate_domain_errors():
|
|
return service.get_version(version_id)
|
|
|
|
|
|
@router.put("/versions/{version_id}")
|
|
async def update_version(
|
|
version_id: str,
|
|
body: VersionUpdate,
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Draft aktualisieren."""
|
|
with translate_domain_errors():
|
|
return service.update_version(version_id, body)
|
|
|
|
|
|
@router.post("/versions/{version_id}/submit")
|
|
async def submit_version(
|
|
version_id: str,
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Zur Pruefung einreichen."""
|
|
with translate_domain_errors():
|
|
return service.submit(version_id)
|
|
|
|
|
|
@router.post("/versions/{version_id}/approve")
|
|
async def approve_version(
|
|
version_id: str,
|
|
comment: Optional[str] = None,
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Genehmigen."""
|
|
with translate_domain_errors():
|
|
return service.approve(version_id, comment)
|
|
|
|
|
|
@router.post("/versions/{version_id}/reject")
|
|
async def reject_version(
|
|
version_id: str,
|
|
comment: Optional[str] = None,
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Ablehnen."""
|
|
with translate_domain_errors():
|
|
return service.reject(version_id, comment)
|
|
|
|
|
|
@router.post("/versions/{version_id}/publish")
|
|
async def publish_version(
|
|
version_id: str,
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Publizieren."""
|
|
with translate_domain_errors():
|
|
return service.publish(version_id)
|
|
|
|
|
|
@router.post("/versions/{version_id}/preview")
|
|
async def preview_version(
|
|
version_id: str,
|
|
body: PreviewRequest,
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Vorschau mit Test-Variablen."""
|
|
with translate_domain_errors():
|
|
return service.preview(version_id, body)
|
|
|
|
|
|
@router.post("/versions/{version_id}/send-test")
|
|
async def send_test_email(
|
|
version_id: str,
|
|
body: SendTestRequest,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
service: EmailTemplateVersionService = Depends(get_version_service),
|
|
) -> dict[str, Any]:
|
|
"""Test-E-Mail senden (Simulation — loggt nur)."""
|
|
with translate_domain_errors():
|
|
return service.send_test(tenant_id, version_id, body)
|
|
|
|
|
|
# Legacy re-exports
|
|
__all__ = [
|
|
"router",
|
|
"TEMPLATE_TYPES",
|
|
"VALID_CATEGORIES",
|
|
"VALID_STATUSES",
|
|
]
|