gateway/modules/routes/routeFeatureTrustee.py
2026-01-17 02:17:58 +01:00

848 lines
31 KiB
Python

# 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, getRequestContext, RequestContext
from modules.interfaces.interfaceDbTrusteeObjects import getInterface
from modules.datamodels.datamodelTrustee import (
TrusteeOrganisation,
TrusteeRole,
TrusteeAccess,
TrusteeContract,
TrusteeDocument,
TrusteePosition,
TrusteePositionDocument,
)
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"),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeOrganisation]:
"""Get all organisations with optional pagination."""
logger = logging.getLogger(__name__)
logger.debug(f"getOrganisations called for user {context.user.id}, mandateId: {context.mandateId}")
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=context.mandateId)
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"),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeOrganisation:
"""Get a single organisation by ID."""
interface = getInterface(context.user, mandateId=context.mandateId)
org = interface.getOrganisation(orgId)
if not org:
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
return org
@router.post("/organisations", response_model=TrusteeOrganisation, status_code=201)
@limiter.limit("10/minute")
async def createOrganisation(
request: Request,
data: TrusteeOrganisation = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeOrganisation:
"""Create a new organisation."""
interface = getInterface(context.user, mandateId=context.mandateId)
result = interface.createOrganisation(data.model_dump())
if not result:
raise HTTPException(status_code=400, detail="Failed to create organisation")
return 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(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeOrganisation:
"""Update an organisation."""
interface = getInterface(context.user, mandateId=context.mandateId)
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 result
@router.delete("/organisations/{orgId}")
@limiter.limit("10/minute")
async def deleteOrganisation(
request: Request,
orgId: str = Path(..., description="Organisation ID"),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete an organisation."""
interface = getInterface(context.user, mandateId=context.mandateId)
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),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeRole]:
"""Get all roles with optional pagination."""
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=context.mandateId)
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"),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeRole:
"""Get a single role by ID."""
interface = getInterface(context.user, mandateId=context.mandateId)
role = interface.getRole(roleId)
if not role:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
return role
@router.post("/roles", response_model=TrusteeRole, status_code=201)
@limiter.limit("10/minute")
async def createRole(
request: Request,
data: TrusteeRole = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeRole:
"""Create a new role (sysadmin only)."""
interface = getInterface(context.user, mandateId=context.mandateId)
result = interface.createRole(data.model_dump())
if not result:
raise HTTPException(status_code=400, detail="Failed to create role")
return result
@router.put("/roles/{roleId}", response_model=TrusteeRole)
@limiter.limit("10/minute")
async def updateRole(
request: Request,
roleId: str = Path(...),
data: TrusteeRole = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeRole:
"""Update a role (sysadmin only)."""
interface = getInterface(context.user, mandateId=context.mandateId)
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 result
@router.delete("/roles/{roleId}")
@limiter.limit("10/minute")
async def deleteRole(
request: Request,
roleId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete a role (sysadmin only, fails if in use)."""
interface = getInterface(context.user, mandateId=context.mandateId)
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),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeAccess]:
"""Get all access records with optional pagination."""
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=context.mandateId)
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(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeAccess:
"""Get a single access record by ID."""
interface = getInterface(context.user, mandateId=context.mandateId)
access = interface.getAccess(accessId)
if not access:
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
return access
@router.get("/access/organisation/{orgId}", response_model=List[TrusteeAccess])
@limiter.limit("30/minute")
async def getAccessByOrganisation(
request: Request,
orgId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeAccess]:
"""Get all access records for an organisation."""
interface = getInterface(context.user, mandateId=context.mandateId)
return interface.getAccessByOrganisation(orgId)
@router.get("/access/user/{userId}", response_model=List[TrusteeAccess])
@limiter.limit("30/minute")
async def getAccessByUser(
request: Request,
userId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeAccess]:
"""Get all access records for a user."""
interface = getInterface(context.user, mandateId=context.mandateId)
return interface.getAccessByUser(userId)
@router.post("/access", response_model=TrusteeAccess, status_code=201)
@limiter.limit("10/minute")
async def createAccess(
request: Request,
data: TrusteeAccess = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeAccess:
"""Create a new access record."""
interface = getInterface(context.user, mandateId=context.mandateId)
result = interface.createAccess(data.model_dump())
if not result:
raise HTTPException(status_code=400, detail="Failed to create access")
return result
@router.put("/access/{accessId}", response_model=TrusteeAccess)
@limiter.limit("10/minute")
async def updateAccess(
request: Request,
accessId: str = Path(...),
data: TrusteeAccess = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeAccess:
"""Update an access record."""
interface = getInterface(context.user, mandateId=context.mandateId)
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 result
@router.delete("/access/{accessId}")
@limiter.limit("10/minute")
async def deleteAccess(
request: Request,
accessId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete an access record."""
interface = getInterface(context.user, mandateId=context.mandateId)
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),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeContract]:
"""Get all contracts with optional pagination."""
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=context.mandateId)
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(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeContract:
"""Get a single contract by ID."""
interface = getInterface(context.user, mandateId=context.mandateId)
contract = interface.getContract(contractId)
if not contract:
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
return contract
@router.get("/contracts/organisation/{orgId}", response_model=List[TrusteeContract])
@limiter.limit("30/minute")
async def getContractsByOrganisation(
request: Request,
orgId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeContract]:
"""Get all contracts for an organisation."""
interface = getInterface(context.user, mandateId=context.mandateId)
return interface.getContractsByOrganisation(orgId)
@router.post("/contracts", response_model=TrusteeContract, status_code=201)
@limiter.limit("10/minute")
async def createContract(
request: Request,
data: TrusteeContract = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeContract:
"""Create a new contract."""
interface = getInterface(context.user, mandateId=context.mandateId)
result = interface.createContract(data.model_dump())
if not result:
raise HTTPException(status_code=400, detail="Failed to create contract")
return result
@router.put("/contracts/{contractId}", response_model=TrusteeContract)
@limiter.limit("10/minute")
async def updateContract(
request: Request,
contractId: str = Path(...),
data: TrusteeContract = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeContract:
"""Update a contract (organisationId is immutable)."""
interface = getInterface(context.user, mandateId=context.mandateId)
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 result
@router.delete("/contracts/{contractId}")
@limiter.limit("10/minute")
async def deleteContract(
request: Request,
contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete a contract."""
interface = getInterface(context.user, mandateId=context.mandateId)
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),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeDocument]:
"""Get all documents (metadata only) with optional pagination."""
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=context.mandateId)
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(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument:
"""Get document metadata by ID."""
interface = getInterface(context.user, mandateId=context.mandateId)
doc = interface.getDocument(documentId)
if not doc:
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
return doc
@router.get("/documents/{documentId}/data")
@limiter.limit("10/minute")
async def getDocumentData(
request: Request,
documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
):
"""Download document binary data."""
interface = getInterface(context.user, mandateId=context.mandateId)
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.documentMimeType or "application/octet-stream",
headers={"Content-Disposition": f"attachment; filename={doc.documentName or 'document'}"}
)
@router.get("/documents/contract/{contractId}", response_model=List[TrusteeDocument])
@limiter.limit("30/minute")
async def getDocumentsByContract(
request: Request,
contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeDocument]:
"""Get all documents for a contract."""
interface = getInterface(context.user, mandateId=context.mandateId)
return interface.getDocumentsByContract(contractId)
@router.post("/documents", response_model=TrusteeDocument, status_code=201)
@limiter.limit("10/minute")
async def createDocument(
request: Request,
data: TrusteeDocument = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument:
"""Create a new document."""
interface = getInterface(context.user, mandateId=context.mandateId)
result = interface.createDocument(data.model_dump())
if not result:
raise HTTPException(status_code=400, detail="Failed to create document")
return result
@router.put("/documents/{documentId}", response_model=TrusteeDocument)
@limiter.limit("10/minute")
async def updateDocument(
request: Request,
documentId: str = Path(...),
data: TrusteeDocument = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument:
"""Update document metadata."""
interface = getInterface(context.user, mandateId=context.mandateId)
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 result
@router.delete("/documents/{documentId}")
@limiter.limit("10/minute")
async def deleteDocument(
request: Request,
documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete a document."""
interface = getInterface(context.user, mandateId=context.mandateId)
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),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteePosition]:
"""Get all positions with optional pagination."""
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=context.mandateId)
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(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteePosition:
"""Get a single position by ID."""
interface = getInterface(context.user, mandateId=context.mandateId)
position = interface.getPosition(positionId)
if not position:
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
return position
@router.get("/positions/contract/{contractId}", response_model=List[TrusteePosition])
@limiter.limit("30/minute")
async def getPositionsByContract(
request: Request,
contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePosition]:
"""Get all positions for a contract."""
interface = getInterface(context.user, mandateId=context.mandateId)
return interface.getPositionsByContract(contractId)
@router.get("/positions/organisation/{orgId}", response_model=List[TrusteePosition])
@limiter.limit("30/minute")
async def getPositionsByOrganisation(
request: Request,
orgId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePosition]:
"""Get all positions for an organisation."""
interface = getInterface(context.user, mandateId=context.mandateId)
return interface.getPositionsByOrganisation(orgId)
@router.post("/positions", response_model=TrusteePosition, status_code=201)
@limiter.limit("10/minute")
async def createPosition(
request: Request,
data: TrusteePosition = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteePosition:
"""Create a new position."""
interface = getInterface(context.user, mandateId=context.mandateId)
result = interface.createPosition(data.model_dump())
if not result:
raise HTTPException(status_code=400, detail="Failed to create position")
return result
@router.put("/positions/{positionId}", response_model=TrusteePosition)
@limiter.limit("10/minute")
async def updatePosition(
request: Request,
positionId: str = Path(...),
data: TrusteePosition = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteePosition:
"""Update a position."""
interface = getInterface(context.user, mandateId=context.mandateId)
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 result
@router.delete("/positions/{positionId}")
@limiter.limit("10/minute")
async def deletePosition(
request: Request,
positionId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete a position."""
interface = getInterface(context.user, mandateId=context.mandateId)
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),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteePositionDocument]:
"""Get all position-document links with optional pagination."""
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=context.mandateId)
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(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteePositionDocument:
"""Get a single position-document link by ID."""
interface = getInterface(context.user, mandateId=context.mandateId)
link = interface.getPositionDocument(linkId)
if not link:
raise HTTPException(status_code=404, detail=f"Link {linkId} not found")
return link
@router.get("/position-documents/position/{positionId}", response_model=List[TrusteePositionDocument])
@limiter.limit("30/minute")
async def getDocumentsForPosition(
request: Request,
positionId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePositionDocument]:
"""Get all document links for a position."""
interface = getInterface(context.user, mandateId=context.mandateId)
return 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(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePositionDocument]:
"""Get all position links for a document."""
interface = getInterface(context.user, mandateId=context.mandateId)
return 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(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteePositionDocument:
"""Create a new position-document link."""
interface = getInterface(context.user, mandateId=context.mandateId)
result = interface.createPositionDocument(data.model_dump())
if not result:
raise HTTPException(status_code=400, detail="Failed to create link")
return result
@router.delete("/position-documents/{linkId}")
@limiter.limit("10/minute")
async def deletePositionDocument(
request: Request,
linkId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete a position-document link."""
interface = getInterface(context.user, mandateId=context.mandateId)
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"}