""" FastAPI routes for Legal Documents — Rechtliche Texte mit Versionierung und Approval-Workflow. Extended with: Public endpoints, User Consents, Consent Audit Log, Cookie Categories. Phase 1 Step 4 refactor: handlers delegate to LegalDocumentService and LegalDocumentConsentService. Module-level helpers re-exported for legacy tests. """ import logging from typing import Any, Optional from fastapi import APIRouter, Depends, Header, Query, UploadFile, File from sqlalchemy.orm import Session from classroom_engine.database import get_db from compliance.api._http_errors import translate_domain_errors from compliance.schemas.legal_document import ( ActionRequest, CookieCategoryCreate, CookieCategoryUpdate, DocumentCreate, DocumentResponse, UserConsentCreate, VersionCreate, VersionResponse, VersionUpdate, ) from compliance.services.legal_document_consent_service import ( LegalDocumentConsentService, ) from compliance.services.legal_document_service import ( LegalDocumentService, _doc_to_response, # re-exported for legacy test imports _log_approval, # re-exported for legacy test imports _transition, # re-exported for legacy test imports _version_to_response, # re-exported for legacy test imports ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/legal-documents", tags=["legal-documents"]) 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_doc_service(db: Session = Depends(get_db)) -> LegalDocumentService: return LegalDocumentService(db) def _get_consent_service( db: Session = Depends(get_db), ) -> LegalDocumentConsentService: return LegalDocumentConsentService(db) # ============================================================================ # Documents # ============================================================================ @router.get("/documents", response_model=dict[str, Any]) async def list_documents( tenant_id: Optional[str] = Query(None), type: Optional[str] = Query(None), service: LegalDocumentService = Depends(_get_doc_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.list_documents(tenant_id, type) @router.post("/documents", response_model=DocumentResponse, status_code=201) async def create_document( request: DocumentCreate, service: LegalDocumentService = Depends(_get_doc_service), ) -> DocumentResponse: with translate_domain_errors(): return service.create_document(request) @router.get("/documents/{document_id}", response_model=DocumentResponse) async def get_document( document_id: str, service: LegalDocumentService = Depends(_get_doc_service), ) -> DocumentResponse: with translate_domain_errors(): return service.get_document(document_id) @router.delete("/documents/{document_id}", status_code=204) async def delete_document( document_id: str, service: LegalDocumentService = Depends(_get_doc_service), ) -> None: with translate_domain_errors(): service.delete_document(document_id) @router.get("/documents/{document_id}/versions", response_model=list[VersionResponse]) async def list_versions( document_id: str, service: LegalDocumentService = Depends(_get_doc_service), ) -> list[VersionResponse]: with translate_domain_errors(): return service.list_versions_for(document_id) # ============================================================================ # Versions # ============================================================================ @router.post("/versions", response_model=VersionResponse, status_code=201) async def create_version( request: VersionCreate, service: LegalDocumentService = Depends(_get_doc_service), ) -> VersionResponse: with translate_domain_errors(): return service.create_version(request) @router.put("/versions/{version_id}", response_model=VersionResponse) async def update_version( version_id: str, request: VersionUpdate, service: LegalDocumentService = Depends(_get_doc_service), ) -> VersionResponse: with translate_domain_errors(): return service.update_version(version_id, request) @router.get("/versions/{version_id}", response_model=VersionResponse) async def get_version( version_id: str, service: LegalDocumentService = Depends(_get_doc_service), ) -> VersionResponse: with translate_domain_errors(): return service.get_version(version_id) @router.post("/versions/upload-word", response_model=dict[str, Any]) async def upload_word( file: UploadFile = File(...), service: LegalDocumentService = Depends(_get_doc_service), ) -> dict[str, Any]: content_bytes = await file.read() with translate_domain_errors(): return await service.upload_word(file.filename, content_bytes) # ============================================================================ # Approval Workflow Actions # ============================================================================ @router.post("/versions/{version_id}/submit-review", response_model=VersionResponse) async def submit_review( version_id: str, request: ActionRequest, service: LegalDocumentService = Depends(_get_doc_service), ) -> VersionResponse: with translate_domain_errors(): return service.submit_review(version_id, request) @router.post("/versions/{version_id}/approve", response_model=VersionResponse) async def approve_version( version_id: str, request: ActionRequest, service: LegalDocumentService = Depends(_get_doc_service), ) -> VersionResponse: with translate_domain_errors(): return service.approve(version_id, request) @router.post("/versions/{version_id}/reject", response_model=VersionResponse) async def reject_version( version_id: str, request: ActionRequest, service: LegalDocumentService = Depends(_get_doc_service), ) -> VersionResponse: with translate_domain_errors(): return service.reject(version_id, request) @router.post("/versions/{version_id}/publish", response_model=VersionResponse) async def publish_version( version_id: str, request: ActionRequest, service: LegalDocumentService = Depends(_get_doc_service), ) -> VersionResponse: with translate_domain_errors(): return service.publish(version_id, request) # ============================================================================ # Approval History # ============================================================================ @router.get("/versions/{version_id}/approval-history") async def get_approval_history( version_id: str, service: LegalDocumentService = Depends(_get_doc_service), ) -> list[dict[str, Any]]: with translate_domain_errors(): entries = service.approval_history(version_id) return [e.dict() for e in entries] # ============================================================================ # Public Endpoints (for end users) # ============================================================================ @router.get("/public") async def list_public_documents( tenant_id: str = Depends(_get_tenant), service: LegalDocumentService = Depends(_get_doc_service), ) -> list[dict[str, Any]]: with translate_domain_errors(): return service.list_public(tenant_id) @router.get("/public/{document_type}/latest") async def get_latest_published( document_type: str, language: str = Query("de"), tenant_id: str = Depends(_get_tenant), service: LegalDocumentService = Depends(_get_doc_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.get_latest_published(tenant_id, document_type, language) # ============================================================================ # User Consents # ============================================================================ @router.post("/consents") async def record_consent( body: UserConsentCreate, tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.record_consent(tenant_id, body) @router.get("/consents/my") async def get_my_consents( user_id: str = Query(...), tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> list[dict[str, Any]]: with translate_domain_errors(): return service.get_my_consents(tenant_id, user_id) @router.get("/consents/check/{document_type}") async def check_consent( document_type: str, user_id: str = Query(...), tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.check_consent(tenant_id, document_type, user_id) @router.delete("/consents/{consent_id}") async def withdraw_consent( consent_id: str, tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.withdraw_consent(tenant_id, consent_id) # ============================================================================ # Consent Statistics # ============================================================================ @router.get("/stats/consents") async def get_consent_stats( tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.get_consent_stats(tenant_id) # ============================================================================ # Audit Log # ============================================================================ @router.get("/audit-log") async def get_audit_log( limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), action: Optional[str] = Query(None), entity_type: Optional[str] = Query(None), tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.get_audit_log( tenant_id, limit=limit, offset=offset, action=action, entity_type=entity_type, ) # ============================================================================ # Cookie Categories CRUD # ============================================================================ @router.get("/cookie-categories") async def list_cookie_categories( tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> list[dict[str, Any]]: with translate_domain_errors(): return service.list_cookie_categories(tenant_id) @router.post("/cookie-categories") async def create_cookie_category( body: CookieCategoryCreate, tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.create_cookie_category(tenant_id, body) @router.put("/cookie-categories/{category_id}") async def update_cookie_category( category_id: str, body: CookieCategoryUpdate, tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.update_cookie_category(tenant_id, category_id, body) @router.delete("/cookie-categories/{category_id}", status_code=204) async def delete_cookie_category( category_id: str, tenant_id: str = Depends(_get_tenant), service: LegalDocumentConsentService = Depends(_get_consent_service), ) -> None: with translate_domain_errors(): service.delete_cookie_category(tenant_id, category_id) # Legacy re-exports so existing tests can still import from this module. __all__ = [ "router", "DEFAULT_TENANT", "DocumentCreate", "VersionCreate", "VersionUpdate", "ActionRequest", "_doc_to_response", "_version_to_response", "_transition", "_log_approval", ]