# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Routes for Trustee feature data management. Implements CRUD operations for organisations, roles, access, contracts, documents, and positions. """ from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Query, Response from fastapi.responses import StreamingResponse from typing import List, Dict, Any, Optional from fastapi import status import logging import json import io from modules.auth import limiter, getCurrentUser from modules.interfaces.interfaceDbTrusteeObjects import getInterface from modules.datamodels.datamodelTrustee import ( TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition, TrusteePositionDocument, ) from modules.datamodels.datamodelUam import User from modules.datamodels.datamodelPagination import ( PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict, ) logger = logging.getLogger(__name__) router = APIRouter( prefix="/api/trustee", tags=["Trustee"], responses={404: {"description": "Not found"}} ) # ===== Helper Functions ===== def _parsePagination(pagination: Optional[str]) -> Optional[PaginationParams]: """Parse pagination parameter from JSON string.""" if not pagination: return None try: paginationDict = json.loads(pagination) if paginationDict: paginationDict = normalize_pagination_dict(paginationDict) return PaginationParams(**paginationDict) except (json.JSONDecodeError, ValueError) as e: raise HTTPException( status_code=400, detail=f"Invalid pagination parameter: {str(e)}" ) return None # ===== Organisation Routes ===== @router.get("/organisations", response_model=PaginatedResponse[TrusteeOrganisation]) @limiter.limit("30/minute") async def getOrganisations( request: Request, pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"), currentUser: User = Depends(getCurrentUser) ) -> PaginatedResponse[TrusteeOrganisation]: """Get all organisations with optional pagination.""" logger = logging.getLogger(__name__) logger.debug(f"getOrganisations called for user {currentUser.id}, roles: {currentUser.roleLabels}") paginationParams = _parsePagination(pagination) interface = getInterface(currentUser) result = interface.getAllOrganisations(paginationParams) logger.debug(f"getOrganisations returned {len(result.items)} items") if paginationParams: return PaginatedResponse( items=result.items, pagination=PaginationMetadata( currentPage=paginationParams.page or 1, pageSize=paginationParams.pageSize or 20, totalItems=result.totalItems, totalPages=result.totalPages, sort=paginationParams.sort if paginationParams else [], filters=paginationParams.filters if paginationParams else None ) ) return PaginatedResponse(items=result.items, pagination=None) @router.get("/organisations/{orgId}", response_model=TrusteeOrganisation) @limiter.limit("30/minute") async def getOrganisation( request: Request, orgId: str = Path(..., description="Organisation ID"), currentUser: User = Depends(getCurrentUser) ) -> TrusteeOrganisation: """Get a single organisation by ID.""" interface = getInterface(currentUser) org = interface.getOrganisation(orgId) if not org: raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found") return TrusteeOrganisation(**org) @router.post("/organisations", response_model=TrusteeOrganisation, status_code=201) @limiter.limit("10/minute") async def createOrganisation( request: Request, data: TrusteeOrganisation = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeOrganisation: """Create a new organisation.""" interface = getInterface(currentUser) result = interface.createOrganisation(data.model_dump()) if not result: raise HTTPException(status_code=400, detail="Failed to create organisation") return TrusteeOrganisation(**result) @router.put("/organisations/{orgId}", response_model=TrusteeOrganisation) @limiter.limit("10/minute") async def updateOrganisation( request: Request, orgId: str = Path(..., description="Organisation ID"), data: TrusteeOrganisation = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeOrganisation: """Update an organisation.""" interface = getInterface(currentUser) existing = interface.getOrganisation(orgId) if not existing: raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found") result = interface.updateOrganisation(orgId, data.model_dump(exclude={"id"})) if not result: raise HTTPException(status_code=400, detail="Failed to update organisation") return TrusteeOrganisation(**result) @router.delete("/organisations/{orgId}") @limiter.limit("10/minute") async def deleteOrganisation( request: Request, orgId: str = Path(..., description="Organisation ID"), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete an organisation.""" interface = getInterface(currentUser) existing = interface.getOrganisation(orgId) if not existing: raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found") success = interface.deleteOrganisation(orgId) if not success: raise HTTPException(status_code=400, detail="Failed to delete organisation") return {"message": f"Organisation {orgId} deleted"} # ===== Role Routes ===== @router.get("/roles", response_model=PaginatedResponse[TrusteeRole]) @limiter.limit("30/minute") async def getRoles( request: Request, pagination: Optional[str] = Query(None), currentUser: User = Depends(getCurrentUser) ) -> PaginatedResponse[TrusteeRole]: """Get all roles with optional pagination.""" paginationParams = _parsePagination(pagination) interface = getInterface(currentUser) result = interface.getAllRoles(paginationParams) if paginationParams: return PaginatedResponse( items=result.items, pagination=PaginationMetadata( currentPage=paginationParams.page or 1, pageSize=paginationParams.pageSize or 20, totalItems=result.totalItems, totalPages=result.totalPages, sort=paginationParams.sort if paginationParams else [], filters=paginationParams.filters if paginationParams else None ) ) return PaginatedResponse(items=result.items, pagination=None) @router.get("/roles/{roleId}", response_model=TrusteeRole) @limiter.limit("30/minute") async def getRole( request: Request, roleId: str = Path(..., description="Role ID"), currentUser: User = Depends(getCurrentUser) ) -> TrusteeRole: """Get a single role by ID.""" interface = getInterface(currentUser) role = interface.getRole(roleId) if not role: raise HTTPException(status_code=404, detail=f"Role {roleId} not found") return TrusteeRole(**role) @router.post("/roles", response_model=TrusteeRole, status_code=201) @limiter.limit("10/minute") async def createRole( request: Request, data: TrusteeRole = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeRole: """Create a new role (sysadmin only).""" interface = getInterface(currentUser) result = interface.createRole(data.model_dump()) if not result: raise HTTPException(status_code=400, detail="Failed to create role") return TrusteeRole(**result) @router.put("/roles/{roleId}", response_model=TrusteeRole) @limiter.limit("10/minute") async def updateRole( request: Request, roleId: str = Path(...), data: TrusteeRole = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeRole: """Update a role (sysadmin only).""" interface = getInterface(currentUser) existing = interface.getRole(roleId) if not existing: raise HTTPException(status_code=404, detail=f"Role {roleId} not found") result = interface.updateRole(roleId, data.model_dump(exclude={"id"})) if not result: raise HTTPException(status_code=400, detail="Failed to update role") return TrusteeRole(**result) @router.delete("/roles/{roleId}") @limiter.limit("10/minute") async def deleteRole( request: Request, roleId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a role (sysadmin only, fails if in use).""" interface = getInterface(currentUser) existing = interface.getRole(roleId) if not existing: raise HTTPException(status_code=404, detail=f"Role {roleId} not found") success = interface.deleteRole(roleId) if not success: raise HTTPException(status_code=400, detail="Failed to delete role (may be in use)") return {"message": f"Role {roleId} deleted"} # ===== Access Routes ===== @router.get("/access", response_model=PaginatedResponse[TrusteeAccess]) @limiter.limit("30/minute") async def getAllAccess( request: Request, pagination: Optional[str] = Query(None), currentUser: User = Depends(getCurrentUser) ) -> PaginatedResponse[TrusteeAccess]: """Get all access records with optional pagination.""" paginationParams = _parsePagination(pagination) interface = getInterface(currentUser) result = interface.getAllAccess(paginationParams) if paginationParams: return PaginatedResponse( items=result.items, pagination=PaginationMetadata( currentPage=paginationParams.page or 1, pageSize=paginationParams.pageSize or 20, totalItems=result.totalItems, totalPages=result.totalPages, sort=paginationParams.sort if paginationParams else [], filters=paginationParams.filters if paginationParams else None ) ) return PaginatedResponse(items=result.items, pagination=None) @router.get("/access/{accessId}", response_model=TrusteeAccess) @limiter.limit("30/minute") async def getAccess( request: Request, accessId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeAccess: """Get a single access record by ID.""" interface = getInterface(currentUser) access = interface.getAccess(accessId) if not access: raise HTTPException(status_code=404, detail=f"Access {accessId} not found") return TrusteeAccess(**access) @router.get("/access/organisation/{orgId}", response_model=List[TrusteeAccess]) @limiter.limit("30/minute") async def getAccessByOrganisation( request: Request, orgId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> List[TrusteeAccess]: """Get all access records for an organisation.""" interface = getInterface(currentUser) return [TrusteeAccess(**a) for a in interface.getAccessByOrganisation(orgId)] @router.get("/access/user/{userId}", response_model=List[TrusteeAccess]) @limiter.limit("30/minute") async def getAccessByUser( request: Request, userId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> List[TrusteeAccess]: """Get all access records for a user.""" interface = getInterface(currentUser) return [TrusteeAccess(**a) for a in interface.getAccessByUser(userId)] @router.post("/access", response_model=TrusteeAccess, status_code=201) @limiter.limit("10/minute") async def createAccess( request: Request, data: TrusteeAccess = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeAccess: """Create a new access record.""" interface = getInterface(currentUser) result = interface.createAccess(data.model_dump()) if not result: raise HTTPException(status_code=400, detail="Failed to create access") return TrusteeAccess(**result) @router.put("/access/{accessId}", response_model=TrusteeAccess) @limiter.limit("10/minute") async def updateAccess( request: Request, accessId: str = Path(...), data: TrusteeAccess = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeAccess: """Update an access record.""" interface = getInterface(currentUser) existing = interface.getAccess(accessId) if not existing: raise HTTPException(status_code=404, detail=f"Access {accessId} not found") result = interface.updateAccess(accessId, data.model_dump(exclude={"id"})) if not result: raise HTTPException(status_code=400, detail="Failed to update access") return TrusteeAccess(**result) @router.delete("/access/{accessId}") @limiter.limit("10/minute") async def deleteAccess( request: Request, accessId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete an access record.""" interface = getInterface(currentUser) existing = interface.getAccess(accessId) if not existing: raise HTTPException(status_code=404, detail=f"Access {accessId} not found") success = interface.deleteAccess(accessId) if not success: raise HTTPException(status_code=400, detail="Failed to delete access") return {"message": f"Access {accessId} deleted"} # ===== Contract Routes ===== @router.get("/contracts", response_model=PaginatedResponse[TrusteeContract]) @limiter.limit("30/minute") async def getContracts( request: Request, pagination: Optional[str] = Query(None), currentUser: User = Depends(getCurrentUser) ) -> PaginatedResponse[TrusteeContract]: """Get all contracts with optional pagination.""" paginationParams = _parsePagination(pagination) interface = getInterface(currentUser) result = interface.getAllContracts(paginationParams) if paginationParams: return PaginatedResponse( items=result.items, pagination=PaginationMetadata( currentPage=paginationParams.page or 1, pageSize=paginationParams.pageSize or 20, totalItems=result.totalItems, totalPages=result.totalPages, sort=paginationParams.sort if paginationParams else [], filters=paginationParams.filters if paginationParams else None ) ) return PaginatedResponse(items=result.items, pagination=None) @router.get("/contracts/{contractId}", response_model=TrusteeContract) @limiter.limit("30/minute") async def getContract( request: Request, contractId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeContract: """Get a single contract by ID.""" interface = getInterface(currentUser) contract = interface.getContract(contractId) if not contract: raise HTTPException(status_code=404, detail=f"Contract {contractId} not found") return TrusteeContract(**contract) @router.get("/contracts/organisation/{orgId}", response_model=List[TrusteeContract]) @limiter.limit("30/minute") async def getContractsByOrganisation( request: Request, orgId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> List[TrusteeContract]: """Get all contracts for an organisation.""" interface = getInterface(currentUser) return [TrusteeContract(**c) for c in interface.getContractsByOrganisation(orgId)] @router.post("/contracts", response_model=TrusteeContract, status_code=201) @limiter.limit("10/minute") async def createContract( request: Request, data: TrusteeContract = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeContract: """Create a new contract.""" interface = getInterface(currentUser) result = interface.createContract(data.model_dump()) if not result: raise HTTPException(status_code=400, detail="Failed to create contract") return TrusteeContract(**result) @router.put("/contracts/{contractId}", response_model=TrusteeContract) @limiter.limit("10/minute") async def updateContract( request: Request, contractId: str = Path(...), data: TrusteeContract = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeContract: """Update a contract (organisationId is immutable).""" interface = getInterface(currentUser) existing = interface.getContract(contractId) if not existing: raise HTTPException(status_code=404, detail=f"Contract {contractId} not found") result = interface.updateContract(contractId, data.model_dump(exclude={"id"})) if not result: raise HTTPException(status_code=400, detail="Failed to update contract (organisationId cannot be changed)") return TrusteeContract(**result) @router.delete("/contracts/{contractId}") @limiter.limit("10/minute") async def deleteContract( request: Request, contractId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a contract.""" interface = getInterface(currentUser) existing = interface.getContract(contractId) if not existing: raise HTTPException(status_code=404, detail=f"Contract {contractId} not found") success = interface.deleteContract(contractId) if not success: raise HTTPException(status_code=400, detail="Failed to delete contract") return {"message": f"Contract {contractId} deleted"} # ===== Document Routes ===== @router.get("/documents", response_model=PaginatedResponse[TrusteeDocument]) @limiter.limit("30/minute") async def getDocuments( request: Request, pagination: Optional[str] = Query(None), currentUser: User = Depends(getCurrentUser) ) -> PaginatedResponse[TrusteeDocument]: """Get all documents (metadata only) with optional pagination.""" paginationParams = _parsePagination(pagination) interface = getInterface(currentUser) result = interface.getAllDocuments(paginationParams) if paginationParams: return PaginatedResponse( items=result.items, pagination=PaginationMetadata( currentPage=paginationParams.page or 1, pageSize=paginationParams.pageSize or 20, totalItems=result.totalItems, totalPages=result.totalPages, sort=paginationParams.sort if paginationParams else [], filters=paginationParams.filters if paginationParams else None ) ) return PaginatedResponse(items=result.items, pagination=None) @router.get("/documents/{documentId}", response_model=TrusteeDocument) @limiter.limit("30/minute") async def getDocument( request: Request, documentId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeDocument: """Get document metadata by ID.""" interface = getInterface(currentUser) doc = interface.getDocument(documentId) if not doc: raise HTTPException(status_code=404, detail=f"Document {documentId} not found") return TrusteeDocument(**doc) @router.get("/documents/{documentId}/data") @limiter.limit("10/minute") async def getDocumentData( request: Request, documentId: str = Path(...), currentUser: User = Depends(getCurrentUser) ): """Download document binary data.""" interface = getInterface(currentUser) doc = interface.getDocument(documentId) if not doc: raise HTTPException(status_code=404, detail=f"Document {documentId} not found") data = interface.getDocumentData(documentId) if not data: raise HTTPException(status_code=404, detail="Document data not found") return StreamingResponse( io.BytesIO(data), media_type=doc.get("documentMimeType", "application/octet-stream"), headers={"Content-Disposition": f"attachment; filename={doc.get('documentName', 'document')}"} ) @router.get("/documents/contract/{contractId}", response_model=List[TrusteeDocument]) @limiter.limit("30/minute") async def getDocumentsByContract( request: Request, contractId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> List[TrusteeDocument]: """Get all documents for a contract.""" interface = getInterface(currentUser) return [TrusteeDocument(**d) for d in interface.getDocumentsByContract(contractId)] @router.post("/documents", response_model=TrusteeDocument, status_code=201) @limiter.limit("10/minute") async def createDocument( request: Request, data: TrusteeDocument = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeDocument: """Create a new document.""" interface = getInterface(currentUser) result = interface.createDocument(data.model_dump()) if not result: raise HTTPException(status_code=400, detail="Failed to create document") return TrusteeDocument(**result) @router.put("/documents/{documentId}", response_model=TrusteeDocument) @limiter.limit("10/minute") async def updateDocument( request: Request, documentId: str = Path(...), data: TrusteeDocument = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteeDocument: """Update document metadata.""" interface = getInterface(currentUser) existing = interface.getDocument(documentId) if not existing: raise HTTPException(status_code=404, detail=f"Document {documentId} not found") result = interface.updateDocument(documentId, data.model_dump(exclude={"id"})) if not result: raise HTTPException(status_code=400, detail="Failed to update document") return TrusteeDocument(**result) @router.delete("/documents/{documentId}") @limiter.limit("10/minute") async def deleteDocument( request: Request, documentId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a document.""" interface = getInterface(currentUser) existing = interface.getDocument(documentId) if not existing: raise HTTPException(status_code=404, detail=f"Document {documentId} not found") success = interface.deleteDocument(documentId) if not success: raise HTTPException(status_code=400, detail="Failed to delete document") return {"message": f"Document {documentId} deleted"} # ===== Position Routes ===== @router.get("/positions", response_model=PaginatedResponse[TrusteePosition]) @limiter.limit("30/minute") async def getPositions( request: Request, pagination: Optional[str] = Query(None), currentUser: User = Depends(getCurrentUser) ) -> PaginatedResponse[TrusteePosition]: """Get all positions with optional pagination.""" paginationParams = _parsePagination(pagination) interface = getInterface(currentUser) result = interface.getAllPositions(paginationParams) if paginationParams: return PaginatedResponse( items=result.items, pagination=PaginationMetadata( currentPage=paginationParams.page or 1, pageSize=paginationParams.pageSize or 20, totalItems=result.totalItems, totalPages=result.totalPages, sort=paginationParams.sort if paginationParams else [], filters=paginationParams.filters if paginationParams else None ) ) return PaginatedResponse(items=result.items, pagination=None) @router.get("/positions/{positionId}", response_model=TrusteePosition) @limiter.limit("30/minute") async def getPosition( request: Request, positionId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteePosition: """Get a single position by ID.""" interface = getInterface(currentUser) position = interface.getPosition(positionId) if not position: raise HTTPException(status_code=404, detail=f"Position {positionId} not found") return TrusteePosition(**position) @router.get("/positions/contract/{contractId}", response_model=List[TrusteePosition]) @limiter.limit("30/minute") async def getPositionsByContract( request: Request, contractId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> List[TrusteePosition]: """Get all positions for a contract.""" interface = getInterface(currentUser) return [TrusteePosition(**p) for p in interface.getPositionsByContract(contractId)] @router.get("/positions/organisation/{orgId}", response_model=List[TrusteePosition]) @limiter.limit("30/minute") async def getPositionsByOrganisation( request: Request, orgId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> List[TrusteePosition]: """Get all positions for an organisation.""" interface = getInterface(currentUser) return [TrusteePosition(**p) for p in interface.getPositionsByOrganisation(orgId)] @router.post("/positions", response_model=TrusteePosition, status_code=201) @limiter.limit("10/minute") async def createPosition( request: Request, data: TrusteePosition = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteePosition: """Create a new position.""" interface = getInterface(currentUser) result = interface.createPosition(data.model_dump()) if not result: raise HTTPException(status_code=400, detail="Failed to create position") return TrusteePosition(**result) @router.put("/positions/{positionId}", response_model=TrusteePosition) @limiter.limit("10/minute") async def updatePosition( request: Request, positionId: str = Path(...), data: TrusteePosition = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteePosition: """Update a position.""" interface = getInterface(currentUser) existing = interface.getPosition(positionId) if not existing: raise HTTPException(status_code=404, detail=f"Position {positionId} not found") result = interface.updatePosition(positionId, data.model_dump(exclude={"id"})) if not result: raise HTTPException(status_code=400, detail="Failed to update position") return TrusteePosition(**result) @router.delete("/positions/{positionId}") @limiter.limit("10/minute") async def deletePosition( request: Request, positionId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a position.""" interface = getInterface(currentUser) existing = interface.getPosition(positionId) if not existing: raise HTTPException(status_code=404, detail=f"Position {positionId} not found") success = interface.deletePosition(positionId) if not success: raise HTTPException(status_code=400, detail="Failed to delete position") return {"message": f"Position {positionId} deleted"} # ===== Position-Document Link Routes ===== @router.get("/position-documents", response_model=PaginatedResponse[TrusteePositionDocument]) @limiter.limit("30/minute") async def getPositionDocuments( request: Request, pagination: Optional[str] = Query(None), currentUser: User = Depends(getCurrentUser) ) -> PaginatedResponse[TrusteePositionDocument]: """Get all position-document links with optional pagination.""" paginationParams = _parsePagination(pagination) interface = getInterface(currentUser) result = interface.getAllPositionDocuments(paginationParams) if paginationParams: return PaginatedResponse( items=result.items, pagination=PaginationMetadata( currentPage=paginationParams.page or 1, pageSize=paginationParams.pageSize or 20, totalItems=result.totalItems, totalPages=result.totalPages, sort=paginationParams.sort if paginationParams else [], filters=paginationParams.filters if paginationParams else None ) ) return PaginatedResponse(items=result.items, pagination=None) @router.get("/position-documents/{linkId}", response_model=TrusteePositionDocument) @limiter.limit("30/minute") async def getPositionDocument( request: Request, linkId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteePositionDocument: """Get a single position-document link by ID.""" interface = getInterface(currentUser) link = interface.getPositionDocument(linkId) if not link: raise HTTPException(status_code=404, detail=f"Link {linkId} not found") return TrusteePositionDocument(**link) @router.get("/position-documents/position/{positionId}", response_model=List[TrusteePositionDocument]) @limiter.limit("30/minute") async def getDocumentsForPosition( request: Request, positionId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> List[TrusteePositionDocument]: """Get all document links for a position.""" interface = getInterface(currentUser) return [TrusteePositionDocument(**l) for l in interface.getDocumentsForPosition(positionId)] @router.get("/position-documents/document/{documentId}", response_model=List[TrusteePositionDocument]) @limiter.limit("30/minute") async def getPositionsForDocument( request: Request, documentId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> List[TrusteePositionDocument]: """Get all position links for a document.""" interface = getInterface(currentUser) return [TrusteePositionDocument(**l) for l in interface.getPositionsForDocument(documentId)] @router.post("/position-documents", response_model=TrusteePositionDocument, status_code=201) @limiter.limit("10/minute") async def createPositionDocument( request: Request, data: TrusteePositionDocument = Body(...), currentUser: User = Depends(getCurrentUser) ) -> TrusteePositionDocument: """Create a new position-document link.""" interface = getInterface(currentUser) result = interface.createPositionDocument(data.model_dump()) if not result: raise HTTPException(status_code=400, detail="Failed to create link") return TrusteePositionDocument(**result) @router.delete("/position-documents/{linkId}") @limiter.limit("10/minute") async def deletePositionDocument( request: Request, linkId: str = Path(...), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a position-document link.""" interface = getInterface(currentUser) existing = interface.getPositionDocument(linkId) if not existing: raise HTTPException(status_code=404, detail=f"Link {linkId} not found") success = interface.deletePositionDocument(linkId) if not success: raise HTTPException(status_code=400, detail="Failed to delete link") return {"message": f"Link {linkId} deleted"}