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