""" Letters API - Elternbrief-Verwaltung fuer BreakPilot. Bietet Endpoints fuer: - Speichern und Laden von Elternbriefen - PDF-Export von Briefen - Versenden per Email - GFK-Integration fuer Textverbesserung Split into: - letters_models.py: Enums, Pydantic models, helper functions - letters_api.py (this file): API endpoints and in-memory store """ import logging import os import uuid from datetime import datetime from typing import Optional, Dict, Any from fastapi import APIRouter, HTTPException, Response, Query import httpx # PDF service requires WeasyPrint with system libraries - make optional for CI try: from services.pdf_service import generate_letter_pdf, SchoolInfo _pdf_available = True except (ImportError, OSError): generate_letter_pdf = None # type: ignore SchoolInfo = None # type: ignore _pdf_available = False from letters_models import ( LetterType, LetterTone, LetterStatus, LetterCreateRequest, LetterUpdateRequest, LetterResponse, LetterListResponse, ExportPDFRequest, ImproveRequest, ImproveResponse, SendEmailRequest, SendEmailResponse, get_type_label as _get_type_label, get_tone_label as _get_tone_label, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/letters", tags=["letters"]) # ============================================================================= # In-Memory Storage (Prototyp - spaeter durch DB ersetzen) # ============================================================================= _letters_store: Dict[str, Dict[str, Any]] = {} def _get_letter(letter_id: str) -> Dict[str, Any]: """Holt Brief aus dem Store.""" if letter_id not in _letters_store: raise HTTPException(status_code=404, detail=f"Brief mit ID {letter_id} nicht gefunden") return _letters_store[letter_id] def _save_letter(letter_data: Dict[str, Any]) -> str: """Speichert Brief und gibt ID zurueck.""" letter_id = letter_data.get("id") or str(uuid.uuid4()) letter_data["id"] = letter_id letter_data["updated_at"] = datetime.now() if "created_at" not in letter_data: letter_data["created_at"] = datetime.now() _letters_store[letter_id] = letter_data return letter_id # ============================================================================= # API Endpoints # ============================================================================= @router.post("/", response_model=LetterResponse) async def create_letter(request: LetterCreateRequest): """Erstellt einen neuen Elternbrief.""" logger.info(f"Creating new letter for student: {request.student_name}") letter_data = { "recipient_name": request.recipient_name, "recipient_address": request.recipient_address, "student_name": request.student_name, "student_class": request.student_class, "subject": request.subject, "content": request.content, "letter_type": request.letter_type, "tone": request.tone, "teacher_name": request.teacher_name, "teacher_title": request.teacher_title, "school_info": request.school_info.model_dump() if request.school_info else None, "legal_references": [ref.model_dump() for ref in request.legal_references] if request.legal_references else None, "gfk_principles_applied": request.gfk_principles_applied, "gfk_score": None, "status": LetterStatus.DRAFT, "pdf_path": None, "dsms_cid": None, "sent_at": None, } letter_id = _save_letter(letter_data) letter_data["id"] = letter_id logger.info(f"Letter created with ID: {letter_id}") return LetterResponse(**letter_data) # NOTE: Static routes must come BEFORE dynamic routes like /{letter_id} @router.get("/types") async def get_letter_types(): """Gibt alle verfuegbaren Brieftypen zurueck.""" return {"types": [{"value": t.value, "label": _get_type_label(t)} for t in LetterType]} @router.get("/tones") async def get_letter_tones(): """Gibt alle verfuegbaren Tonalitaeten zurueck.""" return {"tones": [{"value": t.value, "label": _get_tone_label(t)} for t in LetterTone]} @router.get("/{letter_id}", response_model=LetterResponse) async def get_letter(letter_id: str): """Laedt einen gespeicherten Brief.""" logger.info(f"Getting letter: {letter_id}") letter_data = _get_letter(letter_id) return LetterResponse(**letter_data) @router.get("/", response_model=LetterListResponse) async def list_letters( student_id: Optional[str] = Query(None), class_name: Optional[str] = Query(None), letter_type: Optional[LetterType] = Query(None), status: Optional[LetterStatus] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100) ): """Listet alle gespeicherten Briefe mit optionalen Filtern.""" logger.info("Listing letters with filters") filtered_letters = list(_letters_store.values()) if class_name: filtered_letters = [l for l in filtered_letters if l.get("student_class") == class_name] if letter_type: filtered_letters = [l for l in filtered_letters if l.get("letter_type") == letter_type] if status: filtered_letters = [l for l in filtered_letters if l.get("status") == status] filtered_letters.sort(key=lambda x: x.get("created_at", datetime.min), reverse=True) total = len(filtered_letters) start = (page - 1) * page_size paginated_letters = filtered_letters[start:start + page_size] return LetterListResponse( letters=[LetterResponse(**l) for l in paginated_letters], total=total, page=page, page_size=page_size ) @router.put("/{letter_id}", response_model=LetterResponse) async def update_letter(letter_id: str, request: LetterUpdateRequest): """Aktualisiert einen bestehenden Brief.""" logger.info(f"Updating letter: {letter_id}") letter_data = _get_letter(letter_id) update_data = request.model_dump(exclude_unset=True) for key, value in update_data.items(): if value is not None: if key == "school_info" and value: letter_data[key] = value if isinstance(value, dict) else value.model_dump() elif key == "legal_references" and value: letter_data[key] = [ref if isinstance(ref, dict) else ref.model_dump() for ref in value] else: letter_data[key] = value _save_letter(letter_data) return LetterResponse(**letter_data) @router.delete("/{letter_id}") async def delete_letter(letter_id: str): """Loescht einen Brief.""" logger.info(f"Deleting letter: {letter_id}") if letter_id not in _letters_store: raise HTTPException(status_code=404, detail=f"Brief mit ID {letter_id} nicht gefunden") del _letters_store[letter_id] return {"message": f"Brief {letter_id} wurde geloescht"} @router.post("/export-pdf") async def export_letter_pdf(request: ExportPDFRequest): """Exportiert einen Brief als PDF.""" logger.info("Exporting letter as PDF") if request.letter_id: letter_data = _get_letter(request.letter_id) elif request.letter_data: letter_data = request.letter_data.model_dump() else: raise HTTPException(status_code=400, detail="Entweder letter_id oder letter_data muss angegeben werden") if "date" not in letter_data: letter_data["date"] = datetime.now().strftime("%d.%m.%Y") try: pdf_bytes = generate_letter_pdf(letter_data) except Exception as e: logger.error(f"Error generating PDF: {e}") raise HTTPException(status_code=500, detail=f"Fehler bei PDF-Generierung: {str(e)}") student_name = letter_data.get("student_name", "Brief").replace(" ", "_") date_str = datetime.now().strftime("%Y%m%d") filename = f"Elternbrief_{student_name}_{date_str}.pdf" return Response( content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": f"attachment; filename={filename}", "Content-Length": str(len(pdf_bytes))} ) @router.post("/{letter_id}/export-pdf") async def export_saved_letter_pdf(letter_id: str): """Exportiert einen gespeicherten Brief als PDF (Kurzform).""" return await export_letter_pdf(ExportPDFRequest(letter_id=letter_id)) @router.post("/improve", response_model=ImproveResponse) async def improve_letter_content(request: ImproveRequest): """Verbessert den Briefinhalt nach GFK-Prinzipien.""" logger.info("Improving letter content with GFK principles") comm_service_url = os.getenv("COMMUNICATION_SERVICE_URL", "http://localhost:8000/v1/communication") try: async with httpx.AsyncClient() as client: validate_response = await client.post( f"{comm_service_url}/validate", json={"text": request.content}, timeout=30.0 ) if validate_response.status_code != 200: logger.warning(f"Validation service returned {validate_response.status_code}") return ImproveResponse( improved_content=request.content, changes=["Verbesserungsservice nicht verfuegbar"], gfk_score=0.5, gfk_principles_applied=[] ) validation_data = validate_response.json() if validation_data.get("is_valid", False) and validation_data.get("gfk_score", 0) > 0.8: return ImproveResponse( improved_content=request.content, changes=["Text entspricht bereits GFK-Standards"], gfk_score=validation_data.get("gfk_score", 0.8), gfk_principles_applied=validation_data.get("positive_elements", []) ) return ImproveResponse( improved_content=request.content, changes=validation_data.get("suggestions", []), gfk_score=validation_data.get("gfk_score", 0.5), gfk_principles_applied=validation_data.get("positive_elements", []) ) except httpx.TimeoutException: logger.error("Timeout while calling communication service") return ImproveResponse( improved_content=request.content, changes=["Zeitueberschreitung beim Verbesserungsservice"], gfk_score=0.5, gfk_principles_applied=[] ) except Exception as e: logger.error(f"Error improving content: {e}") return ImproveResponse( improved_content=request.content, changes=[f"Fehler: {str(e)}"], gfk_score=0.5, gfk_principles_applied=[] ) @router.post("/{letter_id}/send", response_model=SendEmailResponse) async def send_letter_email(letter_id: str, request: SendEmailRequest): """Versendet einen Brief per Email.""" logger.info(f"Sending letter {letter_id} to {request.recipient_email}") letter_data = _get_letter(letter_id) try: pdf_attachment = None if request.include_pdf: letter_data["date"] = datetime.now().strftime("%d.%m.%Y") pdf_bytes = generate_letter_pdf(letter_data) pdf_attachment = { "filename": f"Elternbrief_{letter_data.get('student_name', 'Brief').replace(' ', '_')}.pdf", "content": pdf_bytes.hex(), "content_type": "application/pdf" } async with httpx.AsyncClient() as client: logger.info(f"Would send email: {letter_data.get('subject')} to {request.recipient_email}") letter_data["status"] = LetterStatus.SENT letter_data["sent_at"] = datetime.now() _save_letter(letter_data) return SendEmailResponse( success=True, message=f"Brief wurde an {request.recipient_email} gesendet", sent_at=datetime.now() ) except Exception as e: logger.error(f"Error sending email: {e}") return SendEmailResponse(success=False, message=f"Fehler beim Versenden: {str(e)}", sent_at=None) @router.get("/student/{student_id}", response_model=LetterListResponse) async def get_letters_for_student( student_id: str, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100) ): """Laedt alle Briefe fuer einen bestimmten Schueler.""" logger.info(f"Getting letters for student: {student_id}") filtered_letters = [ l for l in _letters_store.values() if student_id.lower() in l.get("student_name", "").lower() ] filtered_letters.sort(key=lambda x: x.get("created_at", datetime.min), reverse=True) total = len(filtered_letters) start = (page - 1) * page_size paginated_letters = filtered_letters[start:start + page_size] return LetterListResponse( letters=[LetterResponse(**l) for l in paginated_letters], total=total, page=page, page_size=page_size )