Files
breakpilot-compliance/backend-compliance/compliance/api/legal_document_routes.py
Benjamin Admin 95fcba34cd
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
fix(quality): Ruff/CVE/TS-Fixes, 104 neue Tests, Complexity-Refactoring
- Ruff: 144 auto-fixes (unused imports, == None → is None), F821/F811/F841 manuell
- CVEs: python-multipart>=0.0.22, weasyprint>=68.0, pillow>=12.1.1, npm audit fix (0 vulns)
- TS: 5 tote Drafting-Engine-Dateien entfernt, allowed-facts/sanitizer/StepHeader/context fixes
- Tests: +104 (ISMS 58, Evidence 18, VVT 14, Generation 14) → 1449 passed
- Refactoring: collect_ci_evidence (F→A), row_to_response (E→A), extract_requirements (E→A)
- Dead Code: pca-platform, 7 Go-Handler, dsr_api.py, duplicate Schemas entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:00:33 +01:00

934 lines
29 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.
"""
import uuid as uuid_mod
import logging
from datetime import datetime
from typing import Optional, List, Any, Dict
from fastapi import APIRouter, Depends, HTTPException, Query, Header, UploadFile, File
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy import func
from classroom_engine.database import get_db
from ..db.legal_document_models import (
LegalDocumentDB,
LegalDocumentVersionDB,
LegalDocumentApprovalDB,
)
from ..db.legal_document_extend_models import (
UserConsentDB,
ConsentAuditLogDB,
CookieCategoryDB,
)
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
# ============================================================================
# Pydantic Schemas
# ============================================================================
class DocumentCreate(BaseModel):
type: str
name: str
description: Optional[str] = None
mandatory: bool = False
tenant_id: Optional[str] = None
class DocumentResponse(BaseModel):
id: str
tenant_id: Optional[str]
type: str
name: str
description: Optional[str]
mandatory: bool
created_at: datetime
updated_at: Optional[datetime]
class VersionCreate(BaseModel):
document_id: str
version: str
language: str = 'de'
title: str
content: str
summary: Optional[str] = None
created_by: Optional[str] = None
class VersionUpdate(BaseModel):
title: Optional[str] = None
content: Optional[str] = None
summary: Optional[str] = None
version: Optional[str] = None
language: Optional[str] = None
class VersionResponse(BaseModel):
id: str
document_id: str
version: str
language: str
title: str
content: str
summary: Optional[str]
status: str
created_by: Optional[str]
approved_by: Optional[str]
approved_at: Optional[datetime]
rejection_reason: Optional[str]
created_at: datetime
updated_at: Optional[datetime]
class ApprovalHistoryEntry(BaseModel):
id: str
version_id: str
action: str
approver: Optional[str]
comment: Optional[str]
created_at: datetime
class ActionRequest(BaseModel):
approver: Optional[str] = None
comment: Optional[str] = None
# ============================================================================
# Helpers
# ============================================================================
def _doc_to_response(doc: LegalDocumentDB) -> DocumentResponse:
return DocumentResponse(
id=str(doc.id),
tenant_id=doc.tenant_id,
type=doc.type,
name=doc.name,
description=doc.description,
mandatory=doc.mandatory or False,
created_at=doc.created_at,
updated_at=doc.updated_at,
)
def _version_to_response(v: LegalDocumentVersionDB) -> VersionResponse:
return VersionResponse(
id=str(v.id),
document_id=str(v.document_id),
version=v.version,
language=v.language or 'de',
title=v.title,
content=v.content,
summary=v.summary,
status=v.status or 'draft',
created_by=v.created_by,
approved_by=v.approved_by,
approved_at=v.approved_at,
rejection_reason=v.rejection_reason,
created_at=v.created_at,
updated_at=v.updated_at,
)
def _log_approval(
db: Session,
version_id: Any,
action: str,
approver: Optional[str] = None,
comment: Optional[str] = None,
) -> LegalDocumentApprovalDB:
entry = LegalDocumentApprovalDB(
version_id=version_id,
action=action,
approver=approver,
comment=comment,
)
db.add(entry)
return entry
# ============================================================================
# Documents
# ============================================================================
@router.get("/documents", response_model=Dict[str, Any])
async def list_documents(
tenant_id: Optional[str] = Query(None),
type: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
"""List all legal documents, optionally filtered by tenant or type."""
query = db.query(LegalDocumentDB)
if tenant_id:
query = query.filter(LegalDocumentDB.tenant_id == tenant_id)
if type:
query = query.filter(LegalDocumentDB.type == type)
docs = query.order_by(LegalDocumentDB.created_at.desc()).all()
return {"documents": [_doc_to_response(d).dict() for d in docs]}
@router.post("/documents", response_model=DocumentResponse, status_code=201)
async def create_document(
request: DocumentCreate,
db: Session = Depends(get_db),
):
"""Create a new legal document type."""
doc = LegalDocumentDB(
tenant_id=request.tenant_id,
type=request.type,
name=request.name,
description=request.description,
mandatory=request.mandatory,
)
db.add(doc)
db.commit()
db.refresh(doc)
return _doc_to_response(doc)
@router.get("/documents/{document_id}", response_model=DocumentResponse)
async def get_document(document_id: str, db: Session = Depends(get_db)):
"""Get a single legal document by ID."""
doc = db.query(LegalDocumentDB).filter(LegalDocumentDB.id == document_id).first()
if not doc:
raise HTTPException(status_code=404, detail=f"Document {document_id} not found")
return _doc_to_response(doc)
@router.delete("/documents/{document_id}", status_code=204)
async def delete_document(document_id: str, db: Session = Depends(get_db)):
"""Delete a legal document and all its versions."""
doc = db.query(LegalDocumentDB).filter(LegalDocumentDB.id == document_id).first()
if not doc:
raise HTTPException(status_code=404, detail=f"Document {document_id} not found")
db.delete(doc)
db.commit()
@router.get("/documents/{document_id}/versions", response_model=List[VersionResponse])
async def list_versions(document_id: str, db: Session = Depends(get_db)):
"""List all versions for a legal document."""
doc = db.query(LegalDocumentDB).filter(LegalDocumentDB.id == document_id).first()
if not doc:
raise HTTPException(status_code=404, detail=f"Document {document_id} not found")
versions = (
db.query(LegalDocumentVersionDB)
.filter(LegalDocumentVersionDB.document_id == document_id)
.order_by(LegalDocumentVersionDB.created_at.desc())
.all()
)
return [_version_to_response(v) for v in versions]
# ============================================================================
# Versions
# ============================================================================
@router.post("/versions", response_model=VersionResponse, status_code=201)
async def create_version(
request: VersionCreate,
db: Session = Depends(get_db),
):
"""Create a new version for a legal document."""
doc = db.query(LegalDocumentDB).filter(LegalDocumentDB.id == request.document_id).first()
if not doc:
raise HTTPException(status_code=404, detail=f"Document {request.document_id} not found")
version = LegalDocumentVersionDB(
document_id=request.document_id,
version=request.version,
language=request.language,
title=request.title,
content=request.content,
summary=request.summary,
created_by=request.created_by,
status='draft',
)
db.add(version)
db.flush()
_log_approval(db, version.id, action='created', approver=request.created_by)
db.commit()
db.refresh(version)
return _version_to_response(version)
@router.put("/versions/{version_id}", response_model=VersionResponse)
async def update_version(
version_id: str,
request: VersionUpdate,
db: Session = Depends(get_db),
):
"""Update a draft legal document version."""
version = db.query(LegalDocumentVersionDB).filter(LegalDocumentVersionDB.id == version_id).first()
if not version:
raise HTTPException(status_code=404, detail=f"Version {version_id} not found")
if version.status not in ('draft', 'rejected'):
raise HTTPException(status_code=400, detail=f"Only draft/rejected versions can be edited (current: {version.status})")
for field, value in request.dict(exclude_none=True).items():
setattr(version, field, value)
version.updated_at = datetime.utcnow()
db.commit()
db.refresh(version)
return _version_to_response(version)
@router.get("/versions/{version_id}", response_model=VersionResponse)
async def get_version(version_id: str, db: Session = Depends(get_db)):
"""Get a single version by ID."""
v = db.query(LegalDocumentVersionDB).filter(LegalDocumentVersionDB.id == version_id).first()
if not v:
raise HTTPException(status_code=404, detail=f"Version {version_id} not found")
return _version_to_response(v)
@router.post("/versions/upload-word", response_model=Dict[str, Any])
async def upload_word(file: UploadFile = File(...)):
"""Convert DOCX to HTML using mammoth (if available) or return raw text."""
if not file.filename or not file.filename.lower().endswith('.docx'):
raise HTTPException(status_code=400, detail="Only .docx files are supported")
content_bytes = await file.read()
html_content = ""
try:
import mammoth # type: ignore
import io
result = mammoth.convert_to_html(io.BytesIO(content_bytes))
html_content = result.value
except ImportError:
# Fallback: return placeholder if mammoth not installed
html_content = f"<p>[DOCX-Import: {file.filename}]</p><p>Bitte installieren Sie 'mammoth' fuer DOCX-Konvertierung.</p>"
return {"html": html_content, "filename": file.filename}
# ============================================================================
# Approval Workflow Actions
# ============================================================================
def _transition(
db: Session,
version_id: str,
from_statuses: List[str],
to_status: str,
action: str,
approver: Optional[str],
comment: Optional[str],
extra_updates: Optional[Dict] = None,
) -> VersionResponse:
version = db.query(LegalDocumentVersionDB).filter(LegalDocumentVersionDB.id == version_id).first()
if not version:
raise HTTPException(status_code=404, detail=f"Version {version_id} not found")
if version.status not in from_statuses:
raise HTTPException(
status_code=400,
detail=f"Cannot perform '{action}' on version with status '{version.status}' (expected: {from_statuses})"
)
version.status = to_status
version.updated_at = datetime.utcnow()
if extra_updates:
for k, v in extra_updates.items():
setattr(version, k, v)
_log_approval(db, version.id, action=action, approver=approver, comment=comment)
db.commit()
db.refresh(version)
return _version_to_response(version)
@router.post("/versions/{version_id}/submit-review", response_model=VersionResponse)
async def submit_review(
version_id: str,
request: ActionRequest,
db: Session = Depends(get_db),
):
"""Submit a draft version for review."""
return _transition(db, version_id, ['draft', 'rejected'], 'review', 'submitted', request.approver, request.comment)
@router.post("/versions/{version_id}/approve", response_model=VersionResponse)
async def approve_version(
version_id: str,
request: ActionRequest,
db: Session = Depends(get_db),
):
"""Approve a version under review."""
return _transition(
db, version_id, ['review'], 'approved', 'approved',
request.approver, request.comment,
extra_updates={'approved_by': request.approver, 'approved_at': datetime.utcnow()}
)
@router.post("/versions/{version_id}/reject", response_model=VersionResponse)
async def reject_version(
version_id: str,
request: ActionRequest,
db: Session = Depends(get_db),
):
"""Reject a version under review."""
return _transition(
db, version_id, ['review'], 'rejected', 'rejected',
request.approver, request.comment,
extra_updates={'rejection_reason': request.comment}
)
@router.post("/versions/{version_id}/publish", response_model=VersionResponse)
async def publish_version(
version_id: str,
request: ActionRequest,
db: Session = Depends(get_db),
):
"""Publish an approved version."""
return _transition(db, version_id, ['approved'], 'published', 'published', request.approver, request.comment)
# ============================================================================
# Approval History
# ============================================================================
@router.get("/versions/{version_id}/approval-history", response_model=List[ApprovalHistoryEntry])
async def get_approval_history(version_id: str, db: Session = Depends(get_db)):
"""Get the full approval audit trail for a version."""
version = db.query(LegalDocumentVersionDB).filter(LegalDocumentVersionDB.id == version_id).first()
if not version:
raise HTTPException(status_code=404, detail=f"Version {version_id} not found")
entries = (
db.query(LegalDocumentApprovalDB)
.filter(LegalDocumentApprovalDB.version_id == version_id)
.order_by(LegalDocumentApprovalDB.created_at.asc())
.all()
)
return [
ApprovalHistoryEntry(
id=str(e.id),
version_id=str(e.version_id),
action=e.action,
approver=e.approver,
comment=e.comment,
created_at=e.created_at,
)
for e in entries
]
# ============================================================================
# Extended Schemas
# ============================================================================
class UserConsentCreate(BaseModel):
user_id: str
document_id: str
document_version_id: Optional[str] = None
document_type: str
consented: bool = True
ip_address: Optional[str] = None
user_agent: Optional[str] = None
class CookieCategoryCreate(BaseModel):
name_de: str
name_en: Optional[str] = None
description_de: Optional[str] = None
description_en: Optional[str] = None
is_required: bool = False
sort_order: int = 0
class CookieCategoryUpdate(BaseModel):
name_de: Optional[str] = None
name_en: Optional[str] = None
description_de: Optional[str] = None
description_en: Optional[str] = None
is_required: Optional[bool] = None
sort_order: Optional[int] = None
is_active: Optional[bool] = None
# ============================================================================
# Extended Helpers
# ============================================================================
def _log_consent_audit(
db: Session,
tenant_id,
action: str,
entity_type: str,
entity_id=None,
user_id: Optional[str] = None,
details: Optional[dict] = None,
ip_address: Optional[str] = None,
):
entry = ConsentAuditLogDB(
tenant_id=tenant_id,
action=action,
entity_type=entity_type,
entity_id=entity_id,
user_id=user_id,
details=details or {},
ip_address=ip_address,
)
db.add(entry)
return entry
def _consent_to_dict(c: UserConsentDB) -> dict:
return {
"id": str(c.id),
"tenant_id": str(c.tenant_id),
"user_id": c.user_id,
"document_id": str(c.document_id),
"document_version_id": str(c.document_version_id) if c.document_version_id else None,
"document_type": c.document_type,
"consented": c.consented,
"ip_address": c.ip_address,
"user_agent": c.user_agent,
"consented_at": c.consented_at.isoformat() if c.consented_at else None,
"withdrawn_at": c.withdrawn_at.isoformat() if c.withdrawn_at else None,
"created_at": c.created_at.isoformat() if c.created_at else None,
}
def _cookie_cat_to_dict(c: CookieCategoryDB) -> dict:
return {
"id": str(c.id),
"tenant_id": str(c.tenant_id),
"name_de": c.name_de,
"name_en": c.name_en,
"description_de": c.description_de,
"description_en": c.description_en,
"is_required": c.is_required,
"sort_order": c.sort_order,
"is_active": c.is_active,
"created_at": c.created_at.isoformat() if c.created_at else None,
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
}
# ============================================================================
# Public Endpoints (for end users)
# ============================================================================
@router.get("/public")
async def list_public_documents(
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Active documents for end-user display."""
docs = (
db.query(LegalDocumentDB)
.filter(LegalDocumentDB.tenant_id == tenant_id)
.order_by(LegalDocumentDB.created_at.desc())
.all()
)
result = []
for doc in docs:
# Find latest published version
published = (
db.query(LegalDocumentVersionDB)
.filter(
LegalDocumentVersionDB.document_id == doc.id,
LegalDocumentVersionDB.status == "published",
)
.order_by(LegalDocumentVersionDB.created_at.desc())
.first()
)
if published:
result.append({
"id": str(doc.id),
"type": doc.type,
"name": doc.name,
"version": published.version,
"title": published.title,
"content": published.content,
"language": published.language,
"published_at": published.approved_at.isoformat() if published.approved_at else None,
})
return result
@router.get("/public/{document_type}/latest")
async def get_latest_published(
document_type: str,
language: str = Query("de"),
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Get the latest published version of a document type."""
doc = (
db.query(LegalDocumentDB)
.filter(
LegalDocumentDB.tenant_id == tenant_id,
LegalDocumentDB.type == document_type,
)
.first()
)
if not doc:
raise HTTPException(status_code=404, detail=f"No document of type '{document_type}' found")
version = (
db.query(LegalDocumentVersionDB)
.filter(
LegalDocumentVersionDB.document_id == doc.id,
LegalDocumentVersionDB.status == "published",
LegalDocumentVersionDB.language == language,
)
.order_by(LegalDocumentVersionDB.created_at.desc())
.first()
)
if not version:
raise HTTPException(status_code=404, detail=f"No published version for type '{document_type}' in language '{language}'")
return {
"document_id": str(doc.id),
"type": doc.type,
"name": doc.name,
"version_id": str(version.id),
"version": version.version,
"title": version.title,
"content": version.content,
"language": version.language,
}
# ============================================================================
# User Consents
# ============================================================================
@router.post("/consents")
async def record_consent(
body: UserConsentCreate,
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Record user consent for a legal document."""
tid = uuid_mod.UUID(tenant_id)
doc_id = uuid_mod.UUID(body.document_id)
doc = db.query(LegalDocumentDB).filter(LegalDocumentDB.id == doc_id).first()
if not doc:
raise HTTPException(status_code=404, detail="Document not found")
consent = UserConsentDB(
tenant_id=tid,
user_id=body.user_id,
document_id=doc_id,
document_version_id=uuid_mod.UUID(body.document_version_id) if body.document_version_id else None,
document_type=body.document_type,
consented=body.consented,
ip_address=body.ip_address,
user_agent=body.user_agent,
)
db.add(consent)
db.flush()
_log_consent_audit(
db, tid, "consent_given", "user_consent",
entity_id=consent.id, user_id=body.user_id,
details={"document_type": body.document_type, "document_id": body.document_id},
ip_address=body.ip_address,
)
db.commit()
db.refresh(consent)
return _consent_to_dict(consent)
@router.get("/consents/my")
async def get_my_consents(
user_id: str = Query(...),
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Get all consents for a specific user."""
tid = uuid_mod.UUID(tenant_id)
consents = (
db.query(UserConsentDB)
.filter(
UserConsentDB.tenant_id == tid,
UserConsentDB.user_id == user_id,
UserConsentDB.withdrawn_at is None,
)
.order_by(UserConsentDB.consented_at.desc())
.all()
)
return [_consent_to_dict(c) for c in consents]
@router.get("/consents/check/{document_type}")
async def check_consent(
document_type: str,
user_id: str = Query(...),
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Check if user has active consent for a document type."""
tid = uuid_mod.UUID(tenant_id)
consent = (
db.query(UserConsentDB)
.filter(
UserConsentDB.tenant_id == tid,
UserConsentDB.user_id == user_id,
UserConsentDB.document_type == document_type,
UserConsentDB.consented,
UserConsentDB.withdrawn_at is None,
)
.order_by(UserConsentDB.consented_at.desc())
.first()
)
return {
"has_consent": consent is not None,
"consent": _consent_to_dict(consent) if consent else None,
}
@router.delete("/consents/{consent_id}")
async def withdraw_consent(
consent_id: str,
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Withdraw a consent (DSGVO Art. 7 Abs. 3)."""
tid = uuid_mod.UUID(tenant_id)
try:
cid = uuid_mod.UUID(consent_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid consent ID")
consent = db.query(UserConsentDB).filter(
UserConsentDB.id == cid,
UserConsentDB.tenant_id == tid,
).first()
if not consent:
raise HTTPException(status_code=404, detail="Consent not found")
if consent.withdrawn_at:
raise HTTPException(status_code=400, detail="Consent already withdrawn")
consent.withdrawn_at = datetime.utcnow()
consent.consented = False
_log_consent_audit(
db, tid, "consent_withdrawn", "user_consent",
entity_id=cid, user_id=consent.user_id,
details={"document_type": consent.document_type},
)
db.commit()
db.refresh(consent)
return _consent_to_dict(consent)
# ============================================================================
# Consent Statistics
# ============================================================================
@router.get("/stats/consents")
async def get_consent_stats(
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Consent statistics for dashboard."""
tid = uuid_mod.UUID(tenant_id)
base = db.query(UserConsentDB).filter(UserConsentDB.tenant_id == tid)
total = base.count()
active = base.filter(
UserConsentDB.consented,
UserConsentDB.withdrawn_at is None,
).count()
withdrawn = base.filter(UserConsentDB.withdrawn_at is not None).count()
# By document type
by_type = {}
type_counts = (
db.query(UserConsentDB.document_type, func.count(UserConsentDB.id))
.filter(UserConsentDB.tenant_id == tid)
.group_by(UserConsentDB.document_type)
.all()
)
for dtype, count in type_counts:
by_type[dtype] = count
# Unique users
unique_users = (
db.query(func.count(func.distinct(UserConsentDB.user_id)))
.filter(UserConsentDB.tenant_id == tid)
.scalar()
) or 0
return {
"total": total,
"active": active,
"withdrawn": withdrawn,
"unique_users": unique_users,
"by_type": by_type,
}
# ============================================================================
# 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),
db: Session = Depends(get_db),
):
"""Consent audit trail (paginated)."""
tid = uuid_mod.UUID(tenant_id)
query = db.query(ConsentAuditLogDB).filter(ConsentAuditLogDB.tenant_id == tid)
if action:
query = query.filter(ConsentAuditLogDB.action == action)
if entity_type:
query = query.filter(ConsentAuditLogDB.entity_type == entity_type)
total = query.count()
entries = query.order_by(ConsentAuditLogDB.created_at.desc()).offset(offset).limit(limit).all()
return {
"entries": [
{
"id": str(e.id),
"action": e.action,
"entity_type": e.entity_type,
"entity_id": str(e.entity_id) if e.entity_id else None,
"user_id": e.user_id,
"details": e.details or {},
"ip_address": e.ip_address,
"created_at": e.created_at.isoformat() if e.created_at else None,
}
for e in entries
],
"total": total,
"limit": limit,
"offset": offset,
}
# ============================================================================
# Cookie Categories CRUD
# ============================================================================
@router.get("/cookie-categories")
async def list_cookie_categories(
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""List all cookie categories."""
tid = uuid_mod.UUID(tenant_id)
cats = (
db.query(CookieCategoryDB)
.filter(CookieCategoryDB.tenant_id == tid)
.order_by(CookieCategoryDB.sort_order)
.all()
)
return [_cookie_cat_to_dict(c) for c in cats]
@router.post("/cookie-categories")
async def create_cookie_category(
body: CookieCategoryCreate,
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Create a cookie category."""
tid = uuid_mod.UUID(tenant_id)
cat = CookieCategoryDB(
tenant_id=tid,
name_de=body.name_de,
name_en=body.name_en,
description_de=body.description_de,
description_en=body.description_en,
is_required=body.is_required,
sort_order=body.sort_order,
)
db.add(cat)
db.commit()
db.refresh(cat)
return _cookie_cat_to_dict(cat)
@router.put("/cookie-categories/{category_id}")
async def update_cookie_category(
category_id: str,
body: CookieCategoryUpdate,
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Update a cookie category."""
tid = uuid_mod.UUID(tenant_id)
try:
cid = uuid_mod.UUID(category_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid category ID")
cat = db.query(CookieCategoryDB).filter(
CookieCategoryDB.id == cid,
CookieCategoryDB.tenant_id == tid,
).first()
if not cat:
raise HTTPException(status_code=404, detail="Cookie category not found")
for field in ["name_de", "name_en", "description_de", "description_en",
"is_required", "sort_order", "is_active"]:
val = getattr(body, field, None)
if val is not None:
setattr(cat, field, val)
cat.updated_at = datetime.utcnow()
db.commit()
db.refresh(cat)
return _cookie_cat_to_dict(cat)
@router.delete("/cookie-categories/{category_id}", status_code=204)
async def delete_cookie_category(
category_id: str,
tenant_id: str = Depends(_get_tenant),
db: Session = Depends(get_db),
):
"""Delete a cookie category."""
tid = uuid_mod.UUID(tenant_id)
try:
cid = uuid_mod.UUID(category_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid category ID")
cat = db.query(CookieCategoryDB).filter(
CookieCategoryDB.id == cid,
CookieCategoryDB.tenant_id == tid,
).first()
if not cat:
raise HTTPException(status_code=404, detail="Cookie category not found")
db.delete(cat)
db.commit()