Extract consent, audit log, cookie category, and consent stats endpoints from legal_document_routes into LegalDocumentConsentService. The route file is now a thin handler layer delegating to LegalDocumentService and LegalDocumentConsentService with translate_domain_errors(). Legacy helpers (_doc_to_response, _version_to_response, _transition, _log_approval) and schemas are re-exported for existing tests. Two transition tests updated to expect domain errors instead of HTTPException. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
380 lines
12 KiB
Python
380 lines
12 KiB
Python
"""
|
|
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",
|
|
]
|