feat: implement governed cases workspace
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
import uuid
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.institutional import (
|
||||
InstitutionalContextError,
|
||||
ServiceDefinition,
|
||||
ServiceLaunchRequest,
|
||||
ServiceLaunchResult,
|
||||
)
|
||||
from govoplan_cases.backend.service import create_case_from_intake, get_case
|
||||
from govoplan_cases.backend.service_intake import CaseServiceIntake
|
||||
|
||||
|
||||
CAPABILITY_CASES_SERVICE_LAUNCHER = "cases.service_launcher"
|
||||
_CASE_LAUNCH_NAMESPACE = uuid.uuid5(
|
||||
uuid.NAMESPACE_URL,
|
||||
"https://govoplan.add-ideas.de/contracts/cases/service-launch/v1",
|
||||
)
|
||||
|
||||
|
||||
class CaseServiceLauncher:
|
||||
"""Start a replay-safe case from an exact institutional Service revision."""
|
||||
|
||||
def launch_service(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
definition: ServiceDefinition,
|
||||
request: ServiceLaunchRequest,
|
||||
) -> ServiceLaunchResult:
|
||||
if not isinstance(session, Session):
|
||||
raise InstitutionalContextError(
|
||||
"Case service launch requires a SQLAlchemy session."
|
||||
)
|
||||
if request.service_ref != definition.reference:
|
||||
raise InstitutionalContextError(
|
||||
"Case service launch must use the requested exact Service revision."
|
||||
)
|
||||
if request.binding.kind != "case" or request.binding not in definition.bindings:
|
||||
raise InstitutionalContextError(
|
||||
"Case service launch requires a case binding from the Service definition."
|
||||
)
|
||||
case_id = str(
|
||||
uuid.uuid5(
|
||||
_CASE_LAUNCH_NAMESPACE,
|
||||
":".join(
|
||||
(
|
||||
definition.reference.tenant_id,
|
||||
definition.reference.object_id,
|
||||
definition.reference.version or "",
|
||||
request.idempotency_key,
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
replayed = get_case(session, principal, case_id=case_id) is not None
|
||||
plan = CaseServiceIntake().plan(
|
||||
definition,
|
||||
case_id=case_id,
|
||||
effective_at=request.requested_at,
|
||||
)
|
||||
title = _parameter_text(
|
||||
request.parameters,
|
||||
"title",
|
||||
default=definition.title,
|
||||
maximum=500,
|
||||
)
|
||||
case_number = _parameter_text(
|
||||
request.parameters,
|
||||
"case_number",
|
||||
default=_default_case_number(definition, case_id),
|
||||
maximum=255,
|
||||
)
|
||||
status_key = _optional_parameter_text(
|
||||
request.parameters,
|
||||
"status_key",
|
||||
maximum=120,
|
||||
)
|
||||
item = create_case_from_intake(
|
||||
session,
|
||||
principal,
|
||||
plan=plan,
|
||||
case_number=case_number,
|
||||
title=title,
|
||||
status_key=status_key,
|
||||
opened_at=request.requested_at,
|
||||
recorded_at=request.requested_at,
|
||||
change_reason=(
|
||||
f"Started from service {definition.key}@"
|
||||
f"{definition.reference.version}."
|
||||
),
|
||||
idempotency_key=request.idempotency_key,
|
||||
metadata={
|
||||
"service_launch_binding": request.binding.reference,
|
||||
"service_launch_capability": CAPABILITY_CASES_SERVICE_LAUNCHER,
|
||||
},
|
||||
)
|
||||
return ServiceLaunchResult(
|
||||
service_ref=definition.reference,
|
||||
binding=request.binding,
|
||||
state="started",
|
||||
target_ref=item.reference,
|
||||
href=f"/cases/{quote(item.reference.object_id, safe='')}",
|
||||
replayed=replayed,
|
||||
metadata={
|
||||
"case_number": item.case_number,
|
||||
"case_revision": item.revision,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _default_case_number(definition: ServiceDefinition, case_id: str) -> str:
|
||||
prefix = re.sub(r"[^A-Za-z0-9]+", "-", definition.key).strip("-")
|
||||
return f"{(prefix or 'CASE')[:40].upper()}-{case_id[:8].upper()}"
|
||||
|
||||
|
||||
def _parameter_text(
|
||||
parameters: Mapping[str, object],
|
||||
key: str,
|
||||
*,
|
||||
default: str,
|
||||
maximum: int,
|
||||
) -> str:
|
||||
value = str(parameters.get(key) or default).strip()
|
||||
if not value or len(value) > maximum:
|
||||
raise InstitutionalContextError(
|
||||
f"Case launch parameter {key!r} must contain at most {maximum} characters."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _optional_parameter_text(
|
||||
parameters: Mapping[str, object],
|
||||
key: str,
|
||||
*,
|
||||
maximum: int,
|
||||
) -> str | None:
|
||||
raw = parameters.get(key)
|
||||
if raw is None:
|
||||
return None
|
||||
value = str(raw).strip()
|
||||
if not value or len(value) > maximum:
|
||||
raise InstitutionalContextError(
|
||||
f"Case launch parameter {key!r} must contain at most {maximum} characters."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
__all__ = ["CAPABILITY_CASES_SERVICE_LAUNCHER", "CaseServiceLauncher"]
|
||||
Reference in New Issue
Block a user