Files
govoplan-cases/src/govoplan_cases/backend/service_launcher.py
T

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"]