""" FastAPI routes for Document Generation from Stammdaten. Endpoints: GET /generation/preview/{doc_type} — Markdown preview from Stammdaten POST /generation/apply/{doc_type} — Generate drafts → create Change-Requests """ import json import logging from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Header from sqlalchemy import text from sqlalchemy.orm import Session from classroom_engine.database import get_db from .tenant_utils import get_tenant_id from .document_templates import ( generate_dsfa_draft, generate_vvt_drafts, generate_loeschfristen_drafts, generate_tom_drafts, generate_obligation_drafts, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/generation", tags=["generation"]) VALID_DOC_TYPES = {"dsfa", "vvt", "tom", "loeschfristen", "obligation"} def _get_template_context(db, tid: str) -> dict: """Fetch company profile and build template context.""" from .company_profile_routes import _BASE_COLUMNS, row_to_response from database import SessionLocal # Use a fresh session for company_profile (different DB import pattern) cp_db = SessionLocal() try: result = cp_db.execute( text(f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid"), {"tid": tid}, ) row = result.fetchone() if not row: return None resp = row_to_response(row) # Build flat context return { "company_name": f"{resp.company_name} {resp.legal_form}".strip() if resp.legal_form else resp.company_name, "company_name_short": resp.company_name, "legal_form": resp.legal_form, "industry": resp.industry, "business_model": resp.business_model, "company_size": resp.company_size, "employee_count": resp.employee_count, "headquarters_country": resp.headquarters_country, "headquarters_city": resp.headquarters_city, "primary_jurisdiction": resp.primary_jurisdiction, "is_data_controller": resp.is_data_controller, "is_data_processor": resp.is_data_processor, "uses_ai": resp.uses_ai, "dpo_name": resp.dpo_name or "", "dpo_email": resp.dpo_email or "", "supervisory_authority": resp.supervisory_authority or "", "review_cycle_months": resp.review_cycle_months, "subject_to_nis2": resp.subject_to_nis2, "subject_to_ai_act": resp.subject_to_ai_act, "subject_to_iso27001": resp.subject_to_iso27001, "offerings": resp.offerings, "target_markets": resp.target_markets, "ai_use_cases": resp.ai_use_cases, "repos": resp.repos, "document_sources": resp.document_sources, "processing_systems": resp.processing_systems, "ai_systems": resp.ai_systems, "technical_contacts": resp.technical_contacts, "has_ai_systems": len(resp.ai_systems) > 0, "processing_system_count": len(resp.processing_systems), "ai_system_count": len(resp.ai_systems), "is_complete": resp.is_complete, } finally: cp_db.close() def _generate_for_type(doc_type: str, ctx: dict): """Call the appropriate template generator.""" if doc_type == "dsfa": return [generate_dsfa_draft(ctx)] elif doc_type == "vvt": return generate_vvt_drafts(ctx) elif doc_type == "tom": return generate_tom_drafts(ctx) elif doc_type == "loeschfristen": return generate_loeschfristen_drafts(ctx) elif doc_type == "obligation": return generate_obligation_drafts(ctx) else: raise ValueError(f"Unknown doc_type: {doc_type}") @router.get("/preview/{doc_type}") async def preview_generation( doc_type: str, tid: str = Depends(get_tenant_id), db: Session = Depends(get_db), ): """Preview what documents would be generated (no DB writes).""" if doc_type not in VALID_DOC_TYPES: raise HTTPException(status_code=400, detail=f"Invalid doc_type: {doc_type}. Valid: {VALID_DOC_TYPES}") ctx = _get_template_context(db, tid) if not ctx: raise HTTPException(status_code=404, detail="Company profile not found — fill Stammdaten first") drafts = _generate_for_type(doc_type, ctx) return { "doc_type": doc_type, "count": len(drafts), "drafts": drafts, "company_name": ctx.get("company_name"), "is_preview": True, } @router.post("/apply/{doc_type}") async def apply_generation( doc_type: str, tid: str = Depends(get_tenant_id), db: Session = Depends(get_db), x_user_id: Optional[str] = Header(None, alias="X-User-ID"), ): """Generate drafts and create Change-Requests for each. Does NOT create documents directly — all go through the CR inbox. """ if doc_type not in VALID_DOC_TYPES: raise HTTPException(status_code=400, detail=f"Invalid doc_type: {doc_type}. Valid: {VALID_DOC_TYPES}") ctx = _get_template_context(db, tid) if not ctx: raise HTTPException(status_code=404, detail="Company profile not found — fill Stammdaten first") drafts = _generate_for_type(doc_type, ctx) user = x_user_id or "system" cr_ids = [] for draft in drafts: title = draft.get("title") or draft.get("name") or draft.get("data_category") or f"Neues {doc_type}-Dokument" try: result = db.execute( text(""" INSERT INTO compliance_change_requests (tenant_id, trigger_type, target_document_type, proposal_title, proposal_body, proposed_changes, priority, created_by) VALUES (:tid, 'generation', :doc_type, :title, :body, CAST(:changes AS jsonb), 'normal', :user) RETURNING id """), { "tid": tid, "doc_type": doc_type, "title": f"[Generiert] {title}", "body": f"Automatisch aus Stammdaten generiert für {ctx.get('company_name', '')}", "changes": json.dumps(draft), "user": user, }, ) row = result.fetchone() if row: cr_ids.append(str(row[0])) except Exception as e: logger.error(f"Failed to create CR for draft: {e}") db.commit() return { "doc_type": doc_type, "drafts_generated": len(drafts), "change_requests_created": len(cr_ids), "change_request_ids": cr_ids, }