feat: implement governed cases workspace

This commit is contained in:
2026-08-01 17:48:24 +02:00
parent 122159de16
commit c7821a5cb0
32 changed files with 5699 additions and 27 deletions
+290
View File
@@ -0,0 +1,290 @@
from __future__ import annotations
from dataclasses import replace
from datetime import UTC, datetime, timedelta
import unittest
from govoplan_core.core.institutional import (
CAPABILITY_PARTY_RESOLVER,
EvidenceReference,
InstitutionalContextError,
InstitutionalReference,
LegalBasisReference,
PartyRepresentation,
PartySubjectReference,
ProcedureParty,
ServiceBinding,
ServiceDefinition,
TemporalRevision,
)
from govoplan_cases.backend.manifest import get_manifest
from govoplan_cases.backend.party_context import (
CasePartyCompatibilityRecord,
CasePartyContext,
)
from govoplan_cases.backend.service_intake import CaseServiceIntake
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
def reference(kind: str, object_id: str) -> InstitutionalReference:
owners = {
"organization_unit": "organizations",
"function": "organizations",
"mandate": "committee",
"jurisdiction": "organizations",
"service": "portal",
"case": "cases",
"party": "cases",
}
return InstitutionalReference(
kind=kind, # type: ignore[arg-type]
owner_module=owners[kind],
object_id=object_id,
tenant_id="tenant-1",
version="1",
valid_at=NOW,
)
def evidence() -> EvidenceReference:
return EvidenceReference(
kind="snapshot",
owner_module="addresses",
evidence_id="contact-snapshot-1",
tenant_id="tenant-1",
version="1",
captured_at=NOW,
)
def service() -> ServiceDefinition:
return ServiceDefinition(
reference=replace(reference("service", "permit"), version="4"),
key="permit.apply",
temporal=TemporalRevision(
revision="4",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
),
title="Apply for a permit",
audience=("resident",),
legal_bases=(
LegalBasisReference(
kind="law",
authority="Example legislature",
reference="permit-law:3",
version="2026-01",
),
),
required_evidence_types=("identity", "application"),
deadline_refs=("deadline:permit:90d",),
responsible_organization_ref=reference("organization_unit", "unit-1"),
responsible_function_ref=reference("function", "function-1"),
mandate_ref=reference("mandate", "mandate-1"),
jurisdiction_refs=(reference("jurisdiction", "city-1"),),
bindings=(
ServiceBinding("case", "permit-application"),
ServiceBinding("form", "forms:permit-application"),
ServiceBinding("workflow", "workflow:permit-review"),
ServiceBinding("result", "decision:permit"),
),
publication_state="published",
)
def procedure_party(*, revoked_at: datetime | None = None) -> ProcedureParty:
represented = reference("party", "applicant")
representative = reference("party", "representative")
return ProcedureParty(
reference=representative,
procedure_ref=reference("case", "case-1"),
role="representative",
subject=PartySubjectReference(
kind="identity",
provider="identity",
subject_id="identity-2",
tenant_id="tenant-1",
version="3",
),
temporal=TemporalRevision(
revision="2",
valid_from=NOW - timedelta(days=2),
recorded_at=NOW - timedelta(days=2),
),
preferred_channels=("postbox",),
permitted_channels=("postbox", "mail"),
delivery_recipient=True,
representations=(
PartyRepresentation(
representative_party_ref=representative,
represented_party_ref=represented,
power_ref="power-1",
permitted_actions=("submit", "receive"),
temporal=TemporalRevision(
revision="1",
valid_from=NOW - timedelta(days=1),
recorded_at=NOW - timedelta(days=1),
),
evidence=(evidence(),),
revoked_at=revoked_at,
),
),
contact_snapshot_refs=("addresses:snapshot-1",),
evidence=(evidence(),),
)
class PartyProvider:
def __init__(self, party: ProcedureParty) -> None:
self.party = party
def list_procedure_parties(self, session, principal, *, procedure_ref, effective_at=None):
return (self.party,)
class DenyingPartyProvider:
def list_procedure_parties(self, session, principal, *, procedure_ref, effective_at=None):
raise InstitutionalContextError("Party provider access denied.")
class Registry:
def __init__(self, provider: object | None = None) -> None:
self.provider = provider
def has_capability(self, name: str) -> bool:
return name == CAPABILITY_PARTY_RESOLVER and self.provider is not None
def capability(self, name: str) -> object:
return self.provider
class InstitutionalConsumerTests(unittest.TestCase):
def test_service_definition_drives_versioned_case_intake(self) -> None:
plan = CaseServiceIntake().plan(service(), case_id="case-1", effective_at=NOW)
self.assertEqual("permit-application", plan.case_type_ref)
self.assertEqual("4", plan.service_ref.version)
self.assertEqual("permit", plan.context.service_ref.object_id)
self.assertEqual("mandate-1", plan.context.mandate_ref.object_id)
self.assertEqual("city-1", plan.context.jurisdiction_refs[0].object_id)
self.assertEqual(("workflow:permit-review",), plan.workflow_refs)
def test_case_party_provider_drives_frozen_delivery_authority(self) -> None:
context = CasePartyContext(Registry(PartyProvider(procedure_party())))
party_set = context.resolve(
None,
None,
case_ref=reference("case", "case-1"),
effective_at=NOW,
)
targets = context.delivery_targets(party_set, channel="postbox")
self.assertEqual("provider", party_set.source)
self.assertEqual(("addresses:snapshot-1",), targets[0].contact_snapshot_refs)
self.assertEqual("applicant", targets[0].represented_party_refs[0].object_id)
def test_revoked_representation_is_not_delivery_authority(self) -> None:
context = CasePartyContext(
Registry(PartyProvider(procedure_party(revoked_at=NOW)))
)
party_set = context.resolve(
None,
None,
case_ref=reference("case", "case-1"),
effective_at=NOW,
)
self.assertEqual(
(),
context.delivery_targets(party_set, channel="postbox")[0].represented_party_refs,
)
def test_cases_only_compatibility_path_is_bounded(self) -> None:
subject = PartySubjectReference(
kind="organization",
provider="organizations",
subject_id="organization-1",
tenant_id="tenant-1",
)
party_set = CasePartyContext().resolve(
None,
None,
case_ref=reference("case", "case-1"),
effective_at=NOW,
compatibility=(
CasePartyCompatibilityRecord(
party_id="party-1",
role="applicant",
subject=subject,
valid_from=NOW - timedelta(days=1),
permitted_channels=("mail",),
delivery_recipient=True,
contact_snapshot_refs=("addresses:snapshot-2",),
),
),
)
self.assertEqual("compatibility", party_set.source)
self.assertEqual("organization-1", party_set.parties[0].subject.subject_id)
self.assertEqual((), party_set.parties[0].representations)
def test_cross_tenant_compatibility_subject_fails_closed(self) -> None:
with self.assertRaisesRegex(InstitutionalContextError, "another tenant"):
CasePartyContext().resolve(
None,
None,
case_ref=reference("case", "case-1"),
effective_at=NOW,
compatibility=(
CasePartyCompatibilityRecord(
party_id="party-1",
role="applicant",
subject=PartySubjectReference(
kind="identity",
provider="identity",
subject_id="identity-9",
tenant_id="tenant-2",
),
valid_from=NOW,
),
),
)
def test_provider_access_denial_never_falls_back_to_local_rows(self) -> None:
compatibility = CasePartyCompatibilityRecord(
party_id="party-1",
role="applicant",
subject=PartySubjectReference(
kind="identity",
provider="identity",
subject_id="identity-1",
tenant_id="tenant-1",
),
valid_from=NOW,
)
with self.assertRaisesRegex(InstitutionalContextError, "access denied"):
CasePartyContext(Registry(DenyingPartyProvider())).resolve(
None,
None,
case_ref=reference("case", "case-1"),
effective_at=NOW,
compatibility=(compatibility,),
)
def test_manifest_exposes_optional_party_provider(self) -> None:
manifest = get_manifest()
self.assertEqual("cases", manifest.id)
self.assertEqual((), manifest.dependencies)
self.assertIn(CAPABILITY_PARTY_RESOLVER, manifest.optional_capabilities)
self.assertIsNotNone(manifest.route_factory)
self.assertIsNotNone(manifest.migration_spec)
self.assertIn("cases.registry", manifest.capability_factories)
if __name__ == "__main__":
unittest.main()