feat: implement institutional mandate resolver

This commit is contained in:
2026-08-01 17:48:37 +02:00
commit 8880818045
24 changed files with 1469 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
from govoplan_core.core.institutional import InstitutionalContextError
from govoplan_core.db.session import get_session
from govoplan_mandates.backend.manifest import READ_SCOPE, WRITE_SCOPE
from govoplan_mandates.backend.schemas import (
MandateListResponse,
MandateResolutionPayload,
MandateWriteRequest,
)
from govoplan_mandates.backend.service import (
MandateStoreError,
SqlMandateResolver,
definition_from_mapping,
get_mandate,
list_mandates,
record_mandate,
resolution_request_from_mapping,
)
router = APIRouter(prefix="/mandates", tags=["mandates"])
def _require_scope(principal: ApiPrincipal, scope: str) -> None:
if not has_scope(principal, scope):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing scope: {scope}",
)
def _error(exc: Exception) -> HTTPException:
message = str(exc)
code = status.HTTP_409_CONFLICT if "conflict" in message.lower() else status.HTTP_400_BAD_REQUEST
return HTTPException(status_code=code, detail=message)
@router.get("/definitions", response_model=MandateListResponse)
def api_list_mandates(
mandate_status: str | None = Query(default=None, alias="status"),
limit: int = Query(default=100, ge=1, le=200),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> MandateListResponse:
_require_scope(principal, READ_SCOPE)
try:
items = list_mandates(
session,
principal,
status=mandate_status,
limit=limit,
)
except (MandateStoreError, InstitutionalContextError) as exc:
raise _error(exc) from exc
return MandateListResponse(
mandates=[item.to_dict(include_inspection=True) for item in items]
)
@router.get("/definitions/{mandate_id}", response_model=dict[str, Any])
def api_get_mandate(
mandate_id: str,
revision: str | None = None,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, Any]:
_require_scope(principal, READ_SCOPE)
item = get_mandate(
session,
principal,
mandate_id=mandate_id,
revision=revision,
)
if item is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mandate not found")
return item.to_dict(include_inspection=True)
@router.post(
"/definitions",
response_model=dict[str, Any],
status_code=status.HTTP_201_CREATED,
)
def api_record_mandate(
payload: MandateWriteRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, Any]:
_require_scope(principal, WRITE_SCOPE)
try:
item = record_mandate(
session,
principal,
definition=definition_from_mapping(payload.definition),
expected_revision=payload.expected_revision,
)
session.commit()
except (MandateStoreError, InstitutionalContextError) as exc:
session.rollback()
raise _error(exc) from exc
return item.to_dict(include_inspection=True)
@router.post("/resolve", response_model=dict[str, Any])
def api_resolve_mandate(
payload: MandateResolutionPayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, Any]:
_require_scope(principal, READ_SCOPE)
try:
result = SqlMandateResolver().resolve_mandate(
session,
principal,
request=resolution_request_from_mapping(payload.request),
)
except (MandateStoreError, InstitutionalContextError) as exc:
raise _error(exc) from exc
return result.to_dict(include_inspection=True)
__all__ = ["router"]