feat: implement governed cases workspace
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user