157 lines
4.8 KiB
Python
157 lines
4.8 KiB
Python
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"]
|