""" FastAPI routes for Consent Email Templates + DSGVO Processes. Endpoints: GET /consent-templates — List email templates (filtered by tenant) POST /consent-templates — Create a new email template PUT /consent-templates/{id} — Update an email template DELETE /consent-templates/{id} — Delete an email template GET /gdpr-processes — List GDPR processes PUT /gdpr-processes/{id} — Update a GDPR process """ import logging from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Header from pydantic import BaseModel from sqlalchemy.orm import Session from sqlalchemy import text from classroom_engine.database import get_db logger = logging.getLogger(__name__) router = APIRouter(tags=["consent-templates"]) # ============================================================================ # Pydantic Schemas # ============================================================================ class ConsentTemplateCreate(BaseModel): template_key: str subject: str body: str language: str = 'de' is_active: bool = True class ConsentTemplateUpdate(BaseModel): subject: Optional[str] = None body: Optional[str] = None is_active: Optional[bool] = None class GDPRProcessUpdate(BaseModel): title: Optional[str] = None description: Optional[str] = None legal_basis: Optional[str] = None retention_days: Optional[int] = None is_active: Optional[bool] = None # ============================================================================ # Helpers # ============================================================================ def _get_tenant(x_tenant_id: Optional[str] = Header(None, alias='X-Tenant-ID')) -> str: return x_tenant_id or 'default' # ============================================================================ # Email Templates # ============================================================================ @router.get("/consent-templates") async def list_consent_templates( db: Session = Depends(get_db), tenant_id: str = Depends(_get_tenant), ): """List all email templates for a tenant.""" rows = db.execute( text(""" SELECT id, tenant_id, template_key, subject, body, language, is_active, created_at, updated_at FROM compliance_consent_email_templates WHERE tenant_id = :tenant_id ORDER BY template_key, language """), {"tenant_id": tenant_id}, ).fetchall() return [ { "id": str(r.id), "tenant_id": r.tenant_id, "template_key": r.template_key, "subject": r.subject, "body": r.body, "language": r.language, "is_active": r.is_active, "created_at": r.created_at.isoformat() if r.created_at else None, "updated_at": r.updated_at.isoformat() if r.updated_at else None, } for r in rows ] @router.post("/consent-templates", status_code=201) async def create_consent_template( request: ConsentTemplateCreate, db: Session = Depends(get_db), tenant_id: str = Depends(_get_tenant), ): """Create a new email template.""" existing = db.execute( text(""" SELECT id FROM compliance_consent_email_templates WHERE tenant_id = :tenant_id AND template_key = :template_key AND language = :language """), {"tenant_id": tenant_id, "template_key": request.template_key, "language": request.language}, ).fetchone() if existing: raise HTTPException( status_code=409, detail=f"Template '{request.template_key}' for language '{request.language}' already exists for this tenant", ) row = db.execute( text(""" INSERT INTO compliance_consent_email_templates (tenant_id, template_key, subject, body, language, is_active) VALUES (:tenant_id, :template_key, :subject, :body, :language, :is_active) RETURNING id, tenant_id, template_key, subject, body, language, is_active, created_at, updated_at """), { "tenant_id": tenant_id, "template_key": request.template_key, "subject": request.subject, "body": request.body, "language": request.language, "is_active": request.is_active, }, ).fetchone() db.commit() return { "id": str(row.id), "tenant_id": row.tenant_id, "template_key": row.template_key, "subject": row.subject, "body": row.body, "language": row.language, "is_active": row.is_active, "created_at": row.created_at.isoformat() if row.created_at else None, "updated_at": row.updated_at.isoformat() if row.updated_at else None, } @router.put("/consent-templates/{template_id}") async def update_consent_template( template_id: str, request: ConsentTemplateUpdate, db: Session = Depends(get_db), tenant_id: str = Depends(_get_tenant), ): """Update an existing email template.""" existing = db.execute( text(""" SELECT id FROM compliance_consent_email_templates WHERE id = :id AND tenant_id = :tenant_id """), {"id": template_id, "tenant_id": tenant_id}, ).fetchone() if not existing: raise HTTPException(status_code=404, detail=f"Template {template_id} not found") updates = request.dict(exclude_none=True) if not updates: raise HTTPException(status_code=400, detail="No fields to update") set_clauses = ", ".join(f"{k} = :{k}" for k in updates) updates["id"] = template_id updates["tenant_id"] = tenant_id updates["now"] = datetime.utcnow() row = db.execute( text(f""" UPDATE compliance_consent_email_templates SET {set_clauses}, updated_at = :now WHERE id = :id AND tenant_id = :tenant_id RETURNING id, tenant_id, template_key, subject, body, language, is_active, created_at, updated_at """), updates, ).fetchone() db.commit() return { "id": str(row.id), "tenant_id": row.tenant_id, "template_key": row.template_key, "subject": row.subject, "body": row.body, "language": row.language, "is_active": row.is_active, "created_at": row.created_at.isoformat() if row.created_at else None, "updated_at": row.updated_at.isoformat() if row.updated_at else None, } @router.delete("/consent-templates/{template_id}") async def delete_consent_template( template_id: str, db: Session = Depends(get_db), tenant_id: str = Depends(_get_tenant), ): """Delete an email template.""" existing = db.execute( text(""" SELECT id FROM compliance_consent_email_templates WHERE id = :id AND tenant_id = :tenant_id """), {"id": template_id, "tenant_id": tenant_id}, ).fetchone() if not existing: raise HTTPException(status_code=404, detail=f"Template {template_id} not found") db.execute( text("DELETE FROM compliance_consent_email_templates WHERE id = :id AND tenant_id = :tenant_id"), {"id": template_id, "tenant_id": tenant_id}, ) db.commit() return {"success": True, "message": f"Template {template_id} deleted"} # ============================================================================ # GDPR Processes # ============================================================================ @router.get("/gdpr-processes") async def list_gdpr_processes( db: Session = Depends(get_db), tenant_id: str = Depends(_get_tenant), ): """List all GDPR processes for a tenant.""" rows = db.execute( text(""" SELECT id, tenant_id, process_key, title, description, legal_basis, retention_days, is_active, created_at FROM compliance_consent_gdpr_processes WHERE tenant_id = :tenant_id ORDER BY process_key """), {"tenant_id": tenant_id}, ).fetchall() return [ { "id": str(r.id), "tenant_id": r.tenant_id, "process_key": r.process_key, "title": r.title, "description": r.description, "legal_basis": r.legal_basis, "retention_days": r.retention_days, "is_active": r.is_active, "created_at": r.created_at.isoformat() if r.created_at else None, } for r in rows ] @router.put("/gdpr-processes/{process_id}") async def update_gdpr_process( process_id: str, request: GDPRProcessUpdate, db: Session = Depends(get_db), tenant_id: str = Depends(_get_tenant), ): """Update an existing GDPR process.""" existing = db.execute( text(""" SELECT id FROM compliance_consent_gdpr_processes WHERE id = :id AND tenant_id = :tenant_id """), {"id": process_id, "tenant_id": tenant_id}, ).fetchone() if not existing: raise HTTPException(status_code=404, detail=f"GDPR process {process_id} not found") updates = request.dict(exclude_none=True) if not updates: raise HTTPException(status_code=400, detail="No fields to update") set_clauses = ", ".join(f"{k} = :{k}" for k in updates) updates["id"] = process_id updates["tenant_id"] = tenant_id row = db.execute( text(f""" UPDATE compliance_consent_gdpr_processes SET {set_clauses} WHERE id = :id AND tenant_id = :tenant_id RETURNING id, tenant_id, process_key, title, description, legal_basis, retention_days, is_active, created_at """), updates, ).fetchone() db.commit() return { "id": str(row.id), "tenant_id": row.tenant_id, "process_key": row.process_key, "title": row.title, "description": row.description, "legal_basis": row.legal_basis, "retention_days": row.retention_days, "is_active": row.is_active, "created_at": row.created_at.isoformat() if row.created_at else None, }