Files
breakpilot-compliance/backend-compliance/compliance/api/email_template_routes.py
Sharang Parnerkar 0c2e03f294 refactor(backend/api): extract Email Template services (Step 4 — file 10 of 18)
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>
2026-04-08 22:39:19 +02:00

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",
]