Add governed form handoffs
This commit is contained in:
@@ -19,19 +19,24 @@ from govoplan_forms_runtime.backend.manifest import (
|
||||
from govoplan_forms_runtime.backend.schemas import (
|
||||
FormDraftUpdateRequest,
|
||||
FormHandoffRequest,
|
||||
FormHandoffActionRequest,
|
||||
FormHandoffCompensateRequest,
|
||||
FormInstanceCreateRequest,
|
||||
FormInstanceEventsResponse,
|
||||
FormInstanceHistoryResponse,
|
||||
FormInstanceListResponse,
|
||||
FormSubmitRequest,
|
||||
FormTransitionRequest,
|
||||
FormNativeHandoffRequest,
|
||||
)
|
||||
from govoplan_forms_runtime.backend.handoffs import FormHandoffService
|
||||
from govoplan_forms_runtime.backend.service import FormRuntimeError, FormRuntimeService
|
||||
|
||||
|
||||
def create_router(registry: object | None) -> APIRouter:
|
||||
router = APIRouter(prefix="/forms-runtime", tags=["forms-runtime"])
|
||||
runtime = FormRuntimeService(registry)
|
||||
handoffs = FormHandoffService(registry)
|
||||
|
||||
@router.get("/instances", response_model=FormInstanceListResponse)
|
||||
def api_list_instances(
|
||||
@@ -162,7 +167,12 @@ def create_router(registry: object | None) -> APIRouter:
|
||||
allow_all=has_scope(principal, WRITE_SCOPE),
|
||||
)
|
||||
session.commit()
|
||||
except (FormRuntimeError, InstitutionalContextError, LookupError, PermissionError) as exc:
|
||||
except (
|
||||
FormRuntimeError,
|
||||
InstitutionalContextError,
|
||||
LookupError,
|
||||
PermissionError,
|
||||
) as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
return item.to_dict()
|
||||
@@ -192,7 +202,12 @@ def create_router(registry: object | None) -> APIRouter:
|
||||
allow_all=has_scope(principal, WRITE_SCOPE),
|
||||
)
|
||||
session.commit()
|
||||
except (FormRuntimeError, InstitutionalContextError, LookupError, PermissionError) as exc:
|
||||
except (
|
||||
FormRuntimeError,
|
||||
InstitutionalContextError,
|
||||
LookupError,
|
||||
PermissionError,
|
||||
) as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
return item.to_dict()
|
||||
@@ -221,7 +236,12 @@ def create_router(registry: object | None) -> APIRouter:
|
||||
allow_all=True,
|
||||
)
|
||||
session.commit()
|
||||
except (FormRuntimeError, InstitutionalContextError, LookupError, PermissionError) as exc:
|
||||
except (
|
||||
FormRuntimeError,
|
||||
InstitutionalContextError,
|
||||
LookupError,
|
||||
PermissionError,
|
||||
) as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
return item.to_dict()
|
||||
@@ -250,11 +270,199 @@ def create_router(registry: object | None) -> APIRouter:
|
||||
allow_all=True,
|
||||
)
|
||||
session.commit()
|
||||
except (FormRuntimeError, InstitutionalContextError, LookupError, PermissionError) as exc:
|
||||
except (
|
||||
FormRuntimeError,
|
||||
InstitutionalContextError,
|
||||
LookupError,
|
||||
PermissionError,
|
||||
) as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
return item.to_dict()
|
||||
|
||||
@router.get(
|
||||
"/instances/{instance_id}/handoffs",
|
||||
response_model=dict[str, object],
|
||||
)
|
||||
def api_list_handoffs(
|
||||
instance_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, object]:
|
||||
_require_any(principal, PARTICIPATE_SCOPE, READ_SCOPE)
|
||||
try:
|
||||
items = handoffs.list(
|
||||
session,
|
||||
principal,
|
||||
instance_id=instance_id,
|
||||
allow_all=has_scope(principal, READ_SCOPE),
|
||||
)
|
||||
except (FormRuntimeError, LookupError, PermissionError) as exc:
|
||||
raise _error(exc) from exc
|
||||
return {"handoffs": [item.to_dict() for item in items]}
|
||||
|
||||
@router.post(
|
||||
"/instances/{instance_id}/handoffs/native",
|
||||
response_model=dict[str, object],
|
||||
)
|
||||
def api_native_handoff(
|
||||
instance_id: str,
|
||||
payload: FormNativeHandoffRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, object]:
|
||||
_require_any(principal, WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
prepared = handoffs.prepare(
|
||||
session,
|
||||
principal,
|
||||
instance_id=instance_id,
|
||||
expected_revision=payload.expected_revision,
|
||||
binding_kind=payload.binding_kind,
|
||||
binding_reference=payload.binding_reference,
|
||||
idempotency_key=payload.idempotency_key,
|
||||
requested_at=payload.requested_at,
|
||||
allow_all=True,
|
||||
)
|
||||
# The intent is durable before owner capability execution starts.
|
||||
session.commit()
|
||||
item, instance = handoffs.execute(
|
||||
session,
|
||||
principal,
|
||||
effect_id=prepared.effect_id,
|
||||
executed_at=payload.requested_at,
|
||||
allow_all=True,
|
||||
)
|
||||
session.commit()
|
||||
except (
|
||||
FormRuntimeError,
|
||||
InstitutionalContextError,
|
||||
LookupError,
|
||||
PermissionError,
|
||||
) as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
return {
|
||||
"handoff": item.to_dict(),
|
||||
"instance": instance.to_dict() if instance is not None else None,
|
||||
}
|
||||
|
||||
@router.post(
|
||||
"/instances/{instance_id}/handoffs/{effect_id}/retry",
|
||||
response_model=dict[str, object],
|
||||
)
|
||||
def api_retry_handoff(
|
||||
instance_id: str,
|
||||
effect_id: str,
|
||||
payload: FormHandoffActionRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, object]:
|
||||
_require_any(principal, WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
prepared = handoffs.retry_rejected(
|
||||
session,
|
||||
principal,
|
||||
effect_id=effect_id,
|
||||
requested_at=payload.recorded_at,
|
||||
allow_all=True,
|
||||
)
|
||||
if prepared.instance_id != instance_id:
|
||||
raise LookupError("Form handoff does not belong to this instance.")
|
||||
session.commit()
|
||||
item, instance = handoffs.execute(
|
||||
session,
|
||||
principal,
|
||||
effect_id=effect_id,
|
||||
executed_at=payload.recorded_at,
|
||||
allow_all=True,
|
||||
)
|
||||
session.commit()
|
||||
except (
|
||||
FormRuntimeError,
|
||||
InstitutionalContextError,
|
||||
LookupError,
|
||||
PermissionError,
|
||||
) as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
return {
|
||||
"handoff": item.to_dict(),
|
||||
"instance": instance.to_dict() if instance is not None else None,
|
||||
}
|
||||
|
||||
@router.post(
|
||||
"/instances/{instance_id}/handoffs/{effect_id}/reconcile",
|
||||
response_model=dict[str, object],
|
||||
)
|
||||
def api_reconcile_handoff(
|
||||
instance_id: str,
|
||||
effect_id: str,
|
||||
payload: FormHandoffActionRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, object]:
|
||||
_require_any(principal, WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
item, instance = handoffs.execute(
|
||||
session,
|
||||
principal,
|
||||
effect_id=effect_id,
|
||||
executed_at=payload.recorded_at,
|
||||
allow_all=True,
|
||||
reconcile=True,
|
||||
)
|
||||
if item.instance_id != instance_id:
|
||||
raise LookupError("Form handoff does not belong to this instance.")
|
||||
session.commit()
|
||||
except (
|
||||
FormRuntimeError,
|
||||
InstitutionalContextError,
|
||||
LookupError,
|
||||
PermissionError,
|
||||
) as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
return {
|
||||
"handoff": item.to_dict(),
|
||||
"instance": instance.to_dict() if instance is not None else None,
|
||||
}
|
||||
|
||||
@router.post(
|
||||
"/instances/{instance_id}/handoffs/{effect_id}/compensate",
|
||||
response_model=dict[str, object],
|
||||
)
|
||||
def api_compensate_handoff(
|
||||
instance_id: str,
|
||||
effect_id: str,
|
||||
payload: FormHandoffCompensateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> dict[str, object]:
|
||||
_require_any(principal, ADMIN_SCOPE)
|
||||
try:
|
||||
item = handoffs.compensate(
|
||||
session,
|
||||
principal,
|
||||
effect_id=effect_id,
|
||||
compensated_at=payload.recorded_at,
|
||||
confirmed_absent=payload.confirmed_absent,
|
||||
change_reason=payload.change_reason,
|
||||
allow_all=True,
|
||||
)
|
||||
if item.instance_id != instance_id:
|
||||
raise LookupError("Form handoff does not belong to this instance.")
|
||||
session.commit()
|
||||
except (
|
||||
FormRuntimeError,
|
||||
InstitutionalContextError,
|
||||
LookupError,
|
||||
PermissionError,
|
||||
) as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
return {"handoff": item.to_dict()}
|
||||
|
||||
@router.get(
|
||||
"/instances/{instance_id}/history",
|
||||
response_model=FormInstanceHistoryResponse,
|
||||
@@ -278,9 +486,7 @@ def create_router(registry: object | None) -> APIRouter:
|
||||
raise _error(exc) from exc
|
||||
if not items:
|
||||
raise HTTPException(status_code=404, detail="Form instance not found")
|
||||
return FormInstanceHistoryResponse(
|
||||
revisions=[item.to_dict() for item in items]
|
||||
)
|
||||
return FormInstanceHistoryResponse(revisions=[item.to_dict() for item in items])
|
||||
|
||||
@router.get(
|
||||
"/instances/{instance_id}/events",
|
||||
|
||||
Reference in New Issue
Block a user