feat: implement institutional governance and recovery architecture
This commit is contained in:
@@ -0,0 +1,591 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.institutional import (
|
||||
ActorRepresentationReference,
|
||||
DecisionEffectReference,
|
||||
EvidenceReference,
|
||||
InformationGovernanceReference,
|
||||
InstitutionalReference,
|
||||
LegalBasisReference,
|
||||
MandateDefinition,
|
||||
PartyRepresentation,
|
||||
PartySubjectReference,
|
||||
ProcedureParty,
|
||||
ServiceBinding,
|
||||
ServiceDefinition,
|
||||
TemporalRevision,
|
||||
service_launch_capability,
|
||||
)
|
||||
from govoplan_cases.backend.party_context import CasePartyContext
|
||||
from govoplan_cases.backend.db.models import (
|
||||
CaseAccessGrant,
|
||||
CaseIdentity,
|
||||
CaseRecordRevision,
|
||||
CaseStatusDefinition,
|
||||
CaseTimelineEntry,
|
||||
CaseTypeDefinition,
|
||||
)
|
||||
from govoplan_cases.backend.service import (
|
||||
create_case_from_intake,
|
||||
get_case,
|
||||
upsert_case_status,
|
||||
upsert_case_type,
|
||||
)
|
||||
from govoplan_cases.backend.service_intake import CaseServiceIntake
|
||||
from govoplan_committee.backend.db.models import (
|
||||
CommitteeDecisionProjection,
|
||||
CommitteeWorkspaceEvent,
|
||||
CommitteeWorkspaceRevision,
|
||||
)
|
||||
from govoplan_committee.backend.decision_path import (
|
||||
CommitteeDecisionPath,
|
||||
CommitteeDecisionProposal,
|
||||
)
|
||||
from govoplan_committee.backend.workspace import (
|
||||
CommitteeWorkspaceRecord,
|
||||
SqlCommitteeWorkspace,
|
||||
get_workspace_object,
|
||||
record_workspace_object,
|
||||
)
|
||||
from govoplan_portal.backend.service_directory import PortalServiceDirectory
|
||||
from govoplan_decisions.backend.db.models import FormalDecisionRevision
|
||||
from govoplan_decisions.backend.service import SqlDecisionRegistry
|
||||
from govoplan_mandates.backend.db.models import MandateRevision
|
||||
from govoplan_mandates.backend.service import SqlMandateResolver, record_mandate
|
||||
from govoplan_parties.backend.db.models import ProcedurePartyRevision
|
||||
from govoplan_parties.backend.service import SqlPartyResolver, record_procedure_party
|
||||
from govoplan_services.backend.db.models import ServiceDefinitionRevision
|
||||
from govoplan_services.backend.service import SqlServiceDefinitionProvider, record_service_definition
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def _reference(
|
||||
kind: str,
|
||||
object_id: str,
|
||||
*,
|
||||
owner: str,
|
||||
version: str = "1",
|
||||
) -> InstitutionalReference:
|
||||
return InstitutionalReference(
|
||||
kind=kind, # type: ignore[arg-type]
|
||||
owner_module=owner,
|
||||
object_id=object_id,
|
||||
tenant_id="tenant-1",
|
||||
version=version,
|
||||
valid_at=NOW,
|
||||
)
|
||||
|
||||
|
||||
def _evidence(kind: str, evidence_id: str, owner: str) -> EvidenceReference:
|
||||
return EvidenceReference(
|
||||
kind=kind, # type: ignore[arg-type]
|
||||
owner_module=owner,
|
||||
evidence_id=evidence_id,
|
||||
tenant_id="tenant-1",
|
||||
version="1",
|
||||
checksum=f"sha256:{evidence_id}",
|
||||
captured_at=NOW,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Principal:
|
||||
tenant_id: str = "tenant-1"
|
||||
account_id: str = "account-1"
|
||||
|
||||
|
||||
class _Registry:
|
||||
def __init__(self, capabilities: dict[str, object]) -> None:
|
||||
self.capabilities = capabilities
|
||||
|
||||
def has(self, module_id: str) -> bool:
|
||||
return module_id in {
|
||||
"portal",
|
||||
"cases",
|
||||
"committee",
|
||||
"postbox",
|
||||
"services",
|
||||
"parties",
|
||||
"mandates",
|
||||
"decisions",
|
||||
}
|
||||
|
||||
def has_capability(self, name: str) -> bool:
|
||||
return name in self.capabilities
|
||||
|
||||
def capability(self, name: str) -> object:
|
||||
return self.capabilities[name]
|
||||
|
||||
|
||||
class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
def test_service_to_formal_outcome_retains_governed_context(self) -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
for table in (
|
||||
ServiceDefinitionRevision.__table__,
|
||||
CaseStatusDefinition.__table__,
|
||||
CaseTypeDefinition.__table__,
|
||||
CaseIdentity.__table__,
|
||||
CaseRecordRevision.__table__,
|
||||
CaseAccessGrant.__table__,
|
||||
CaseTimelineEntry.__table__,
|
||||
ProcedurePartyRevision.__table__,
|
||||
MandateRevision.__table__,
|
||||
FormalDecisionRevision.__table__,
|
||||
CommitteeWorkspaceRevision.__table__,
|
||||
CommitteeWorkspaceEvent.__table__,
|
||||
CommitteeDecisionProjection.__table__,
|
||||
):
|
||||
table.create(engine)
|
||||
session = Session(engine)
|
||||
principal = _Principal()
|
||||
self.addCleanup(engine.dispose)
|
||||
self.addCleanup(session.close)
|
||||
|
||||
organization = _reference(
|
||||
"organization_unit",
|
||||
"permit-office",
|
||||
owner="organizations",
|
||||
)
|
||||
function = _reference("function", "permit-officer", owner="organizations")
|
||||
jurisdiction = _reference(
|
||||
"jurisdiction",
|
||||
"city-1",
|
||||
owner="organizations",
|
||||
)
|
||||
mandate_ref = _reference(
|
||||
"mandate",
|
||||
"permit-mandate",
|
||||
owner="mandates",
|
||||
version="4",
|
||||
)
|
||||
legal_basis = LegalBasisReference(
|
||||
kind="law",
|
||||
authority="Example legislature",
|
||||
reference="permit-law:3",
|
||||
version="2026-01",
|
||||
effective_from=NOW - timedelta(days=100),
|
||||
)
|
||||
service = ServiceDefinition(
|
||||
reference=_reference(
|
||||
"service",
|
||||
"permit-service",
|
||||
owner="services",
|
||||
version="5",
|
||||
),
|
||||
key="permit.apply",
|
||||
temporal=TemporalRevision(
|
||||
revision="5",
|
||||
valid_from=NOW - timedelta(days=1),
|
||||
valid_to=NOW + timedelta(days=30),
|
||||
recorded_at=NOW - timedelta(days=2),
|
||||
change_reason="Service published.",
|
||||
),
|
||||
title="Apply for a permit",
|
||||
audience=("resident",),
|
||||
legal_bases=(legal_basis,),
|
||||
required_evidence_types=("application", "identity"),
|
||||
channels=("portal", "postbox"),
|
||||
responsible_organization_ref=organization,
|
||||
responsible_function_ref=function,
|
||||
mandate_ref=mandate_ref,
|
||||
jurisdiction_refs=(jurisdiction,),
|
||||
bindings=(
|
||||
ServiceBinding("case", "permit-application"),
|
||||
ServiceBinding("workflow", "workflow:permit-review"),
|
||||
ServiceBinding("result", "decision:permit"),
|
||||
),
|
||||
remedy_refs=("review:administrative-court",),
|
||||
publication_state="published",
|
||||
)
|
||||
record_service_definition(session, principal, definition=service)
|
||||
service_registry = _Registry(
|
||||
{
|
||||
"services.definitions": SqlServiceDefinitionProvider(),
|
||||
service_launch_capability("case"): object(),
|
||||
}
|
||||
)
|
||||
entry = PortalServiceDirectory(service_registry).list_entries(
|
||||
session,
|
||||
principal,
|
||||
tenant_id="tenant-1",
|
||||
effective_at=NOW,
|
||||
audiences=("resident",),
|
||||
)[0]
|
||||
self.assertTrue(entry.available)
|
||||
intake = CaseServiceIntake().plan(
|
||||
entry.definition,
|
||||
case_id="case-1",
|
||||
effective_at=NOW,
|
||||
)
|
||||
application_evidence = _evidence("document", "application-1", "files")
|
||||
upsert_case_status(
|
||||
session,
|
||||
principal,
|
||||
status_key="intake",
|
||||
label="Intake",
|
||||
)
|
||||
upsert_case_status(
|
||||
session,
|
||||
principal,
|
||||
status_key="decided",
|
||||
label="Decided",
|
||||
category="decided",
|
||||
)
|
||||
upsert_case_type(
|
||||
session,
|
||||
principal,
|
||||
type_key="permit-application",
|
||||
label="Permit application",
|
||||
initial_status_key="intake",
|
||||
allowed_status_keys=("intake", "decided"),
|
||||
)
|
||||
case_record = create_case_from_intake(
|
||||
session,
|
||||
principal,
|
||||
plan=intake,
|
||||
case_number="PERMIT-2026-0001",
|
||||
title="Permit application",
|
||||
status_key=None,
|
||||
opened_at=NOW,
|
||||
recorded_at=NOW,
|
||||
change_reason="Portal application received.",
|
||||
idempotency_key="journey-case-create",
|
||||
evidence_refs=(application_evidence,),
|
||||
deadline_at=NOW + timedelta(days=30),
|
||||
)
|
||||
|
||||
applicant = _reference("party", "applicant", owner="parties")
|
||||
representative = _reference("party", "representative", owner="parties")
|
||||
address_evidence = _evidence(
|
||||
"snapshot",
|
||||
"address-snapshot-1",
|
||||
"addresses",
|
||||
)
|
||||
representative_party = ProcedureParty(
|
||||
reference=representative,
|
||||
procedure_ref=case_record.reference,
|
||||
role="representative",
|
||||
subject=PartySubjectReference(
|
||||
kind="identity",
|
||||
provider="identity",
|
||||
subject_id="identity-2",
|
||||
tenant_id="tenant-1",
|
||||
version="2",
|
||||
),
|
||||
temporal=TemporalRevision(
|
||||
revision="1",
|
||||
valid_from=NOW - timedelta(days=1),
|
||||
recorded_at=NOW - timedelta(days=1),
|
||||
change_reason="Representative added to the procedure.",
|
||||
),
|
||||
permitted_channels=("postbox",),
|
||||
preferred_channels=("postbox",),
|
||||
delivery_recipient=True,
|
||||
representations=(
|
||||
PartyRepresentation(
|
||||
representative_party_ref=representative,
|
||||
represented_party_ref=applicant,
|
||||
power_ref="power-1",
|
||||
permitted_actions=("submit", "receive"),
|
||||
temporal=TemporalRevision(
|
||||
revision="1",
|
||||
valid_from=NOW - timedelta(days=1),
|
||||
recorded_at=NOW - timedelta(days=1),
|
||||
change_reason="Representation power recorded.",
|
||||
),
|
||||
evidence=(address_evidence,),
|
||||
),
|
||||
),
|
||||
contact_snapshot_refs=("addresses:snapshot-1",),
|
||||
evidence=(address_evidence,),
|
||||
)
|
||||
record_procedure_party(session, principal, party=representative_party)
|
||||
party_context = CasePartyContext(
|
||||
_Registry({"parties.resolver": SqlPartyResolver()})
|
||||
)
|
||||
parties = party_context.resolve(
|
||||
session,
|
||||
principal,
|
||||
case_ref=case_record.reference,
|
||||
effective_at=NOW,
|
||||
)
|
||||
delivery_target = party_context.delivery_targets(
|
||||
parties,
|
||||
channel="postbox",
|
||||
)[0]
|
||||
|
||||
mandate = MandateDefinition(
|
||||
reference=mandate_ref,
|
||||
temporal=TemporalRevision(
|
||||
revision="4",
|
||||
valid_from=NOW - timedelta(days=30),
|
||||
valid_to=NOW + timedelta(days=30),
|
||||
recorded_at=NOW - timedelta(days=31),
|
||||
change_reason="Permit authority delegated.",
|
||||
),
|
||||
task_types=("committee.formal_decision",),
|
||||
authority_types=("permit",),
|
||||
organization_unit_refs=(organization,),
|
||||
function_refs=(function,),
|
||||
jurisdiction_refs=(jurisdiction,),
|
||||
legal_bases=(legal_basis,),
|
||||
evidence=(_evidence("record", "mandate-record-4", "mandates"),),
|
||||
authority_ceiling="permit:standard",
|
||||
)
|
||||
record_mandate(session, principal, definition=mandate)
|
||||
workspace = SqlCommitteeWorkspace()
|
||||
body = CommitteeWorkspaceRecord(
|
||||
tenant_id="tenant-1",
|
||||
object_kind="body",
|
||||
object_id="permit-board",
|
||||
revision=1,
|
||||
state="active",
|
||||
title="Permit board",
|
||||
recorded_at=NOW,
|
||||
change_reason="Permit board configured.",
|
||||
attributes={
|
||||
"organization_unit_ref": organization.to_dict(),
|
||||
"function_refs": [function.to_dict()],
|
||||
"quorum": {"minimum_count": 1},
|
||||
},
|
||||
)
|
||||
meeting = CommitteeWorkspaceRecord(
|
||||
tenant_id="tenant-1",
|
||||
object_kind="meeting",
|
||||
object_id="meeting-1",
|
||||
revision=1,
|
||||
state="open",
|
||||
title="Permit board meeting",
|
||||
parent_id=body.object_id,
|
||||
recorded_at=NOW,
|
||||
change_reason="Meeting opened.",
|
||||
attributes={
|
||||
"starts_at": NOW.isoformat(),
|
||||
"ends_at": (NOW + timedelta(hours=1)).isoformat(),
|
||||
},
|
||||
context=case_record.context,
|
||||
)
|
||||
agenda = CommitteeWorkspaceRecord(
|
||||
tenant_id="tenant-1",
|
||||
object_kind="agenda_item",
|
||||
object_id="item-1",
|
||||
revision=1,
|
||||
state="deliberating",
|
||||
title="Permit application",
|
||||
parent_id=meeting.object_id,
|
||||
recorded_at=NOW,
|
||||
change_reason="Agenda item entered deliberation.",
|
||||
attributes={
|
||||
"position": 1,
|
||||
"subject_refs": [case_record.reference.to_dict()],
|
||||
},
|
||||
context=case_record.context,
|
||||
evidence=(application_evidence,),
|
||||
)
|
||||
approval_ref = _reference("approval", "approval-1", owner="approvals")
|
||||
vote = CommitteeWorkspaceRecord(
|
||||
tenant_id="tenant-1",
|
||||
object_kind="vote",
|
||||
object_id="vote-1",
|
||||
revision=1,
|
||||
state="closed",
|
||||
title="Vote on permit application",
|
||||
parent_id=agenda.object_id,
|
||||
recorded_at=NOW,
|
||||
change_reason="Vote result accepted.",
|
||||
attributes={
|
||||
"method": "recorded",
|
||||
"choices": ["yes", "no"],
|
||||
"eligible_count": 3,
|
||||
"cast_count": 3,
|
||||
"counts": {"yes": 3, "no": 0},
|
||||
"quorum_met": True,
|
||||
"approval_ref": approval_ref.to_dict(),
|
||||
},
|
||||
context=case_record.context,
|
||||
evidence=(_evidence("record", "vote-result-1", "committee"),),
|
||||
)
|
||||
for record, key in (
|
||||
(body, "journey-body"),
|
||||
(meeting, "journey-meeting"),
|
||||
(agenda, "journey-agenda"),
|
||||
(vote, "journey-vote"),
|
||||
):
|
||||
record_workspace_object(
|
||||
session,
|
||||
principal,
|
||||
record=record,
|
||||
idempotency_key=key,
|
||||
)
|
||||
decision_registry = SqlDecisionRegistry()
|
||||
decision_result = CommitteeDecisionPath(
|
||||
_Registry(
|
||||
{
|
||||
"mandates.resolver": SqlMandateResolver(),
|
||||
"decisions.registry": decision_registry,
|
||||
"committee.workspace": workspace,
|
||||
}
|
||||
)
|
||||
).decide(
|
||||
session,
|
||||
principal,
|
||||
proposal=CommitteeDecisionProposal(
|
||||
tenant_id="tenant-1",
|
||||
decision_id="decision-1",
|
||||
revision="1",
|
||||
effective_at=NOW,
|
||||
meeting_ref="meeting-1",
|
||||
agenda_item_ref="item-1",
|
||||
decision_type="permit",
|
||||
subject_refs=(case_record.reference,),
|
||||
organization_unit_ref=organization,
|
||||
function_ref=function,
|
||||
actor=ActorRepresentationReference(
|
||||
tenant_id="tenant-1",
|
||||
account_id="account-1",
|
||||
identity_id="identity-1",
|
||||
represented_function_ref=function,
|
||||
mandate_ref=mandate_ref,
|
||||
),
|
||||
approval_refs=(
|
||||
approval_ref,
|
||||
),
|
||||
fact_evidence=(application_evidence, address_evidence),
|
||||
legal_bases=(legal_basis,),
|
||||
operative_result="Permit granted.",
|
||||
reasoning="The application satisfies the effective rule.",
|
||||
case_ref=case_record.reference,
|
||||
jurisdiction_refs=(jurisdiction,),
|
||||
party_refs=(applicant, representative),
|
||||
record_refs=(
|
||||
_reference("record", "record-1", owner="audit"),
|
||||
),
|
||||
remedy_refs=service.remedy_refs,
|
||||
review_refs=("review:administrative-court",),
|
||||
information_governance=InformationGovernanceReference(
|
||||
classification="restricted",
|
||||
purposes=("permit-decision", "party-delivery"),
|
||||
legal_basis_refs=("permit-law:3@2026-01",),
|
||||
retention_policy_ref="records:permit",
|
||||
disclosure_state="partly_disclosable",
|
||||
),
|
||||
),
|
||||
observed_effects=(
|
||||
DecisionEffectReference(
|
||||
effect_key="postbox.deliver_decision",
|
||||
state="confirmed",
|
||||
resource_refs=(
|
||||
f"postbox:{delivery_target.party_ref.object_id}",
|
||||
),
|
||||
audit_event_refs=("audit:delivery-1",),
|
||||
evidence_refs=(address_evidence.evidence_id,),
|
||||
),
|
||||
),
|
||||
)
|
||||
decided_agenda = replace(
|
||||
agenda,
|
||||
revision=2,
|
||||
state="decided",
|
||||
recorded_at=NOW + timedelta(minutes=1),
|
||||
change_reason="Formal Decision recorded.",
|
||||
attributes={
|
||||
**dict(agenda.attributes),
|
||||
"decision_ref": decision_result.decision.reference.to_dict(),
|
||||
},
|
||||
)
|
||||
record_workspace_object(
|
||||
session,
|
||||
principal,
|
||||
record=decided_agenda,
|
||||
expected_revision=1,
|
||||
idempotency_key="journey-agenda-decided",
|
||||
)
|
||||
closed_meeting = replace(
|
||||
meeting,
|
||||
revision=2,
|
||||
state="closed",
|
||||
recorded_at=NOW + timedelta(hours=1),
|
||||
change_reason="All agenda items completed.",
|
||||
)
|
||||
record_workspace_object(
|
||||
session,
|
||||
principal,
|
||||
record=closed_meeting,
|
||||
expected_revision=1,
|
||||
idempotency_key="journey-meeting-closed",
|
||||
)
|
||||
minute = CommitteeWorkspaceRecord(
|
||||
tenant_id="tenant-1",
|
||||
object_kind="minute",
|
||||
object_id="minute-1",
|
||||
revision=1,
|
||||
state="accepted",
|
||||
title="Accepted permit board minutes",
|
||||
parent_id=meeting.object_id,
|
||||
recorded_at=NOW + timedelta(hours=2),
|
||||
change_reason="Minutes approved.",
|
||||
attributes={
|
||||
"content_ref": _reference(
|
||||
"record",
|
||||
"meeting-minutes-1",
|
||||
owner="records",
|
||||
).to_dict(),
|
||||
"approval_ref": _reference(
|
||||
"approval",
|
||||
"minutes-approval-1",
|
||||
owner="approvals",
|
||||
).to_dict(),
|
||||
},
|
||||
context=case_record.context,
|
||||
evidence=(_evidence("record", "minutes-proof-1", "records"),),
|
||||
)
|
||||
record_workspace_object(
|
||||
session,
|
||||
principal,
|
||||
record=minute,
|
||||
idempotency_key="journey-minute",
|
||||
)
|
||||
reconstruction = decision_result.reconstruction_payload()["decision"]
|
||||
persisted = decision_registry.get_decision(
|
||||
session,
|
||||
principal,
|
||||
reference=decision_result.decision.reference,
|
||||
)
|
||||
|
||||
self.assertTrue(decision_result.persisted_by_decision_registry)
|
||||
self.assertEqual(decision_result.decision, persisted)
|
||||
self.assertEqual(
|
||||
case_record,
|
||||
get_case(session, principal, case_id="case-1"),
|
||||
)
|
||||
self.assertEqual(
|
||||
"decided",
|
||||
get_workspace_object(
|
||||
session,
|
||||
principal,
|
||||
object_kind="agenda_item",
|
||||
object_id="item-1",
|
||||
).state,
|
||||
)
|
||||
self.assertEqual(service.reference, intake.context.service_ref)
|
||||
self.assertEqual("applicant", delivery_target.represented_party_refs[0].object_id)
|
||||
self.assertEqual("permit-mandate", reconstruction["authority_context"]["mandate_ref"]["object_id"])
|
||||
self.assertEqual("city-1", reconstruction["authority_context"]["jurisdiction_refs"][0]["object_id"])
|
||||
self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"])
|
||||
self.assertEqual("audit:delivery-1", reconstruction["observed_effects"][0]["audit_event_refs"][0])
|
||||
self.assertEqual("application-1", reconstruction["fact_evidence"][0]["evidence_id"])
|
||||
self.assertEqual("The application satisfies the effective rule.", reconstruction["reasoning"])
|
||||
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user