Files
breakpilot-compliance/backend-compliance/compliance/api/legal_document_routes.py
Sharang Parnerkar d2c94619d8 refactor(backend/api): extract LegalDocumentConsentService (Step 4 — file 12 of 18)
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>
2026-04-09 08:47:56 +02:00

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",
]