817 lines
30 KiB
Python
817 lines
30 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
from datetime import UTC, datetime, timedelta
|
|
import unittest
|
|
|
|
from govoplan_core.core.automation import ActionExecutionRequest, AutomationInvocation
|
|
from govoplan_core.core.events import PlatformEvent
|
|
from govoplan_core.core.external_references import ExternalObjectReference
|
|
from govoplan_core.core.institutional import (
|
|
ActorRepresentationReference,
|
|
DecisionEffectReference,
|
|
EvidenceReference,
|
|
ExternalSourceReference,
|
|
FormDefinition,
|
|
FormFieldDefinition,
|
|
FormalDecision,
|
|
GovernedContextEnvelope,
|
|
InformationGovernanceReference,
|
|
InstitutionalContextError,
|
|
InstitutionalReference,
|
|
LegalBasisReference,
|
|
MandateDefinition,
|
|
MandateResolution,
|
|
MandateResolutionRequest,
|
|
PartyRepresentation,
|
|
PartySubjectReference,
|
|
ProcedureParty,
|
|
ServiceBinding,
|
|
ServiceAvailabilityRequirement,
|
|
ServiceDefinition,
|
|
ServiceLaunchRequest,
|
|
ServiceLaunchResult,
|
|
TemporalRevision,
|
|
derive_service_restriction,
|
|
resolve_mandate_candidates,
|
|
revise_procedure_party,
|
|
revise_formal_decision,
|
|
revise_mandate_definition,
|
|
revoke_party_representation,
|
|
)
|
|
|
|
|
|
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
|
|
|
|
|
def _reference(
|
|
kind: str,
|
|
object_id: str,
|
|
*,
|
|
tenant_id: str = "tenant-1",
|
|
) -> InstitutionalReference:
|
|
owners = {
|
|
"institution": "tenancy",
|
|
"organization_unit": "organizations",
|
|
"function": "organizations",
|
|
"function_assignment": "idm",
|
|
"mandate": "cases",
|
|
"jurisdiction": "cases",
|
|
"service": "portal",
|
|
"form": "forms",
|
|
"form_submission": "forms_runtime",
|
|
"case": "cases",
|
|
"party": "cases",
|
|
"decision": "committee",
|
|
"work_item": "workflow_engine",
|
|
"workflow": "workflow_engine",
|
|
"approval": "workflow_engine",
|
|
"record": "audit",
|
|
}
|
|
return InstitutionalReference(
|
|
kind=kind,
|
|
owner_module=owners[kind],
|
|
object_id=object_id,
|
|
tenant_id=tenant_id,
|
|
version="3",
|
|
valid_at=NOW,
|
|
label=f"Protected label for {object_id}",
|
|
)
|
|
|
|
|
|
def _context() -> GovernedContextEnvelope:
|
|
external = ExternalObjectReference(
|
|
system="gis.example",
|
|
object_type="administrative_area",
|
|
object_id="area-7",
|
|
maturity="read",
|
|
authority_mode="external_mirror",
|
|
canonical_url="https://gis.example/areas/7",
|
|
observed_at=NOW,
|
|
)
|
|
return GovernedContextEnvelope(
|
|
tenant_id="tenant-1",
|
|
temporal=TemporalRevision(
|
|
revision="7",
|
|
valid_from=NOW - timedelta(days=1),
|
|
valid_to=NOW + timedelta(days=1),
|
|
recorded_at=NOW,
|
|
change_reason="Case state changed.",
|
|
),
|
|
actor=ActorRepresentationReference(
|
|
tenant_id="tenant-1",
|
|
account_id="account-1",
|
|
identity_id="identity-1",
|
|
represented_function_ref=_reference("function", "function-1"),
|
|
function_assignment_ref=_reference(
|
|
"function_assignment", "assignment-1"
|
|
),
|
|
mandate_ref=_reference("mandate", "mandate-1"),
|
|
),
|
|
institution_ref=_reference("institution", "institution-1"),
|
|
organization_unit_ref=_reference("organization_unit", "unit-1"),
|
|
function_ref=_reference("function", "function-1"),
|
|
mandate_ref=_reference("mandate", "mandate-1"),
|
|
jurisdiction_refs=(_reference("jurisdiction", "jurisdiction-1"),),
|
|
service_ref=_reference("service", "service-1"),
|
|
case_ref=_reference("case", "case-1"),
|
|
party_refs=(_reference("party", "party-1"),),
|
|
work_item_ref=_reference("work_item", "work-item-1"),
|
|
workflow_ref=_reference("workflow", "workflow-1"),
|
|
approval_refs=(_reference("approval", "approval-1"),),
|
|
decision_ref=_reference("decision", "decision-1"),
|
|
record_refs=(_reference("record", "record-1"),),
|
|
legal_bases=(
|
|
LegalBasisReference(
|
|
kind="law",
|
|
authority="Example legislature",
|
|
reference="law-1/section-3",
|
|
version="2026-01",
|
|
effective_from=NOW - timedelta(days=30),
|
|
title="Protected legal title",
|
|
inspection_url="https://law.example/section-3",
|
|
),
|
|
),
|
|
evidence=(
|
|
EvidenceReference(
|
|
kind="document",
|
|
owner_module="files",
|
|
evidence_id="file-1",
|
|
tenant_id="tenant-1",
|
|
version="4",
|
|
checksum="sha256:abcd",
|
|
captured_at=NOW,
|
|
inspection_url="https://govoplan.example/files/file-1",
|
|
),
|
|
),
|
|
information_governance=InformationGovernanceReference(
|
|
classification="confidential",
|
|
purposes=("case decision",),
|
|
legal_basis_refs=("law-1/section-3",),
|
|
retention_policy_ref="records:case-default",
|
|
disclosure_state="restricted",
|
|
),
|
|
external_sources=(
|
|
ExternalSourceReference(
|
|
reference=external,
|
|
authority_mode="external_mirror",
|
|
maturity="read",
|
|
freshness_state="current",
|
|
health_state="ok",
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
class InstitutionalContractTests(unittest.TestCase):
|
|
def test_context_round_trip_preserves_versioned_references(self) -> None:
|
|
context = _context()
|
|
|
|
restored = GovernedContextEnvelope.from_mapping(
|
|
context.to_dict(include_inspection=True)
|
|
)
|
|
|
|
self.assertEqual(context.tenant_id, restored.tenant_id)
|
|
self.assertEqual("mandate-1", restored.mandate_ref.object_id)
|
|
self.assertEqual(
|
|
"external_mirror", restored.external_sources[0].authority_mode
|
|
)
|
|
self.assertEqual("workflow-1", restored.workflow_ref.object_id)
|
|
self.assertEqual("approval-1", restored.approval_refs[0].object_id)
|
|
self.assertEqual("record-1", restored.record_refs[0].object_id)
|
|
self.assertTrue(restored.temporal.effective_at(NOW))
|
|
|
|
def test_cross_tenant_reference_is_rejected(self) -> None:
|
|
with self.assertRaisesRegex(InstitutionalContextError, "different tenant"):
|
|
GovernedContextEnvelope(
|
|
tenant_id="tenant-1",
|
|
temporal=TemporalRevision(revision="1", recorded_at=NOW),
|
|
case_ref=_reference("case", "case-2", tenant_id="tenant-2"),
|
|
)
|
|
|
|
def test_default_serialization_redacts_labels_and_inspection_links(self) -> None:
|
|
payload = _context().to_dict()
|
|
|
|
self.assertIsNone(payload["case_ref"]["label"])
|
|
self.assertIsNone(payload["legal_bases"][0]["title"])
|
|
self.assertIsNone(payload["legal_bases"][0]["inspection_url"])
|
|
self.assertIsNone(payload["evidence"][0]["inspection_url"])
|
|
|
|
def test_event_and_action_envelopes_carry_reference_context(self) -> None:
|
|
context = _context()
|
|
event = PlatformEvent(
|
|
type="committee.decision.recorded",
|
|
module_id="committee",
|
|
institutional_context=context,
|
|
)
|
|
action = ActionExecutionRequest(
|
|
tenant_id="tenant-1",
|
|
action_key="committee.decision.publish",
|
|
input={"decision_ref": "decision-1"},
|
|
idempotency_key="decision-1:publish:1",
|
|
invocation=AutomationInvocation(kind="workflow"),
|
|
institutional_context=context,
|
|
)
|
|
|
|
self.assertEqual(
|
|
"decision-1",
|
|
event.to_dict()["institutional_context"]["decision_ref"]["object_id"],
|
|
)
|
|
self.assertIs(context, action.institutional_context)
|
|
|
|
def test_formal_decision_redacts_reasoning_but_keeps_evidence_refs(self) -> None:
|
|
context = _context()
|
|
decision = FormalDecision(
|
|
reference=_reference("decision", "decision-1"),
|
|
temporal=context.temporal,
|
|
decision_type="permit",
|
|
subject_refs=(_reference("case", "case-1"),),
|
|
state="decided",
|
|
authority_context=context,
|
|
fact_evidence=context.evidence,
|
|
legal_bases=context.legal_bases,
|
|
operative_result="Permit granted.",
|
|
reasoning="Protected reasoning.",
|
|
)
|
|
|
|
redacted = decision.to_dict()
|
|
protected = decision.to_dict(include_protected=True)
|
|
|
|
self.assertIsNone(redacted["reasoning"])
|
|
self.assertEqual("Protected reasoning.", protected["reasoning"])
|
|
self.assertEqual(
|
|
"file-1", redacted["fact_evidence"][0]["evidence_id"]
|
|
)
|
|
|
|
restored = FormalDecision.from_mapping(protected)
|
|
self.assertEqual("Protected reasoning.", restored.reasoning)
|
|
self.assertEqual("decision-1", restored.reference.object_id)
|
|
|
|
def test_semantic_provider_dtos_round_trip_without_domain_tables(self) -> None:
|
|
context = _context()
|
|
mandate = MandateDefinition(
|
|
reference=_reference("mandate", "mandate-1"),
|
|
temporal=context.temporal,
|
|
task_types=("committee.formal_decision",),
|
|
authority_types=("permit",),
|
|
organization_unit_refs=(
|
|
_reference("organization_unit", "unit-1"),
|
|
),
|
|
function_refs=(_reference("function", "function-1"),),
|
|
jurisdiction_refs=context.jurisdiction_refs,
|
|
legal_bases=context.legal_bases,
|
|
evidence=context.evidence,
|
|
)
|
|
resolution = MandateResolution(
|
|
competent=True,
|
|
mandates=(mandate,),
|
|
explanation="Authority matched.",
|
|
evidence=context.evidence,
|
|
)
|
|
service = ServiceDefinition(
|
|
reference=_reference("service", "service-1"),
|
|
key="permit.service",
|
|
temporal=context.temporal,
|
|
title="Permit service",
|
|
audience=("residents",),
|
|
legal_bases=context.legal_bases,
|
|
channels=("portal", "mail"),
|
|
responsible_organization_ref=_reference(
|
|
"organization_unit", "unit-1"
|
|
),
|
|
responsible_function_ref=_reference("function", "function-1"),
|
|
mandate_ref=_reference("mandate", "mandate-1"),
|
|
jurisdiction_refs=context.jurisdiction_refs,
|
|
bindings=(ServiceBinding("workflow", "workflow:permit"),),
|
|
publication_state="published",
|
|
)
|
|
represented = _reference("party", "party-applicant")
|
|
representative = _reference("party", "party-agent")
|
|
party = ProcedureParty(
|
|
reference=represented,
|
|
procedure_ref=_reference("case", "case-1"),
|
|
role="applicant",
|
|
subject=PartySubjectReference(
|
|
kind="identity",
|
|
provider="identity",
|
|
subject_id="identity-1",
|
|
tenant_id="tenant-1",
|
|
version="2",
|
|
),
|
|
temporal=context.temporal,
|
|
preferred_channels=("portal",),
|
|
permitted_channels=("portal", "mail"),
|
|
delivery_recipient=True,
|
|
representations=(
|
|
PartyRepresentation(
|
|
representative_party_ref=representative,
|
|
represented_party_ref=represented,
|
|
power_ref="power-1",
|
|
permitted_actions=("submit", "receive"),
|
|
temporal=context.temporal,
|
|
evidence=context.evidence,
|
|
),
|
|
),
|
|
contact_snapshot_refs=("addresses:snapshot-1",),
|
|
evidence=context.evidence,
|
|
)
|
|
|
|
restored_resolution = MandateResolution.from_mapping(
|
|
resolution.to_dict(include_inspection=True)
|
|
)
|
|
restored_service = ServiceDefinition.from_mapping(service.to_dict())
|
|
restored_party = ProcedureParty.from_mapping(
|
|
party.to_dict(include_inspection=True)
|
|
)
|
|
|
|
self.assertTrue(restored_resolution.competent)
|
|
self.assertEqual(
|
|
"committee.formal_decision",
|
|
restored_resolution.mandates[0].task_types[0],
|
|
)
|
|
self.assertEqual("workflow:permit", restored_service.bindings[0].reference)
|
|
self.assertEqual("identity-1", restored_party.subject.subject_id)
|
|
self.assertEqual(
|
|
"party-agent",
|
|
restored_party.representations[0].representative_party_ref.object_id,
|
|
)
|
|
|
|
def test_invalid_semantic_states_are_rejected(self) -> None:
|
|
with self.assertRaisesRegex(InstitutionalContextError, "disclosure state"):
|
|
InformationGovernanceReference(
|
|
classification="internal",
|
|
purposes=("test",),
|
|
disclosure_state="invalid", # type: ignore[arg-type]
|
|
)
|
|
|
|
def test_service_launch_contract_requires_exact_same_tenant_targets(self) -> None:
|
|
service_ref = _reference("service", "service-1")
|
|
binding = ServiceBinding("case", "permit")
|
|
request = ServiceLaunchRequest(
|
|
service_ref=service_ref,
|
|
binding=binding,
|
|
idempotency_key="launch-1",
|
|
requested_at=NOW,
|
|
parameters={"title": "Permit application"},
|
|
)
|
|
result = ServiceLaunchResult(
|
|
service_ref=service_ref,
|
|
binding=binding,
|
|
state="started",
|
|
target_ref=_reference("case", "case-1"),
|
|
href="/cases/case-1",
|
|
metadata={"case_number": "PERMIT-1"},
|
|
)
|
|
|
|
self.assertEqual("launch-1", request.idempotency_key)
|
|
self.assertEqual("case-1", result.to_dict()["target_ref"]["object_id"])
|
|
with self.assertRaisesRegex(InstitutionalContextError, "another tenant"):
|
|
ServiceLaunchResult(
|
|
service_ref=service_ref,
|
|
binding=binding,
|
|
state="started",
|
|
target_ref=_reference("case", "case-2", tenant_id="tenant-2"),
|
|
metadata={},
|
|
)
|
|
|
|
def test_form_definition_contract_round_trips_exact_schema(self) -> None:
|
|
definition = FormDefinition(
|
|
reference=_reference("form", "permit-application"),
|
|
key="permit-application",
|
|
temporal=TemporalRevision(
|
|
revision="3",
|
|
recorded_at=NOW,
|
|
change_reason="Add delivery preference.",
|
|
),
|
|
title="Permit application",
|
|
description="Apply for a permit.",
|
|
fields=(
|
|
FormFieldDefinition(
|
|
key="email",
|
|
label="Email address",
|
|
value_type="email",
|
|
required=True,
|
|
constraints={"max_length": 255},
|
|
),
|
|
FormFieldDefinition(
|
|
key="delivery",
|
|
label="Delivery",
|
|
value_type="choice",
|
|
options=("portal", "mail"),
|
|
),
|
|
),
|
|
max_attachments=3,
|
|
signature_requirement="optional",
|
|
policy_refs=("policy:permit-intake",),
|
|
handoff_kinds=("case",),
|
|
)
|
|
|
|
restored = FormDefinition.from_mapping(definition.to_dict())
|
|
|
|
self.assertEqual("3", restored.reference.version)
|
|
self.assertEqual("email", restored.fields[0].key)
|
|
self.assertEqual(("portal", "mail"), restored.fields[1].options)
|
|
with self.assertRaisesRegex(InstitutionalContextError, "unique"):
|
|
replace(restored, fields=(restored.fields[0], restored.fields[0]))
|
|
|
|
def test_service_launch_redirect_rejects_credentialed_urls(self) -> None:
|
|
with self.assertRaisesRegex(InstitutionalContextError, "credential-free"):
|
|
ServiceLaunchResult(
|
|
service_ref=_reference("service", "service-1"),
|
|
binding=ServiceBinding("external", "external-entry"),
|
|
state="redirect",
|
|
href="https://user:secret@example.test/start",
|
|
metadata={},
|
|
)
|
|
|
|
with self.assertRaisesRegex(InstitutionalContextError, "must be a boolean"):
|
|
ServiceBinding.from_mapping(
|
|
{
|
|
"kind": "workflow",
|
|
"reference": "workflow:permit",
|
|
"required": "false",
|
|
}
|
|
)
|
|
|
|
def test_service_derivation_can_only_tighten_parent_constraints(self) -> None:
|
|
parent = ServiceDefinition(
|
|
reference=_reference("service", "permit-template"),
|
|
key="permit.apply",
|
|
temporal=TemporalRevision(
|
|
revision="7",
|
|
valid_from=NOW - timedelta(days=1),
|
|
valid_to=NOW + timedelta(days=10),
|
|
recorded_at=NOW - timedelta(days=2),
|
|
),
|
|
title="Permit service",
|
|
audience=("resident", "business"),
|
|
prerequisites=("registered-address",),
|
|
required_evidence_types=("application",),
|
|
channels=("portal", "mail"),
|
|
bindings=(ServiceBinding("case", "permit-case"),),
|
|
availability_requirements=(
|
|
ServiceAvailabilityRequirement("policy", "permit-access"),
|
|
),
|
|
publication_state="published",
|
|
)
|
|
temporal = TemporalRevision(
|
|
revision="8",
|
|
valid_from=NOW,
|
|
valid_to=NOW + timedelta(days=5),
|
|
recorded_at=NOW,
|
|
change_reason="Tenant specialization.",
|
|
)
|
|
reference = replace(
|
|
_reference("service", "permit-tenant"),
|
|
version="8",
|
|
)
|
|
|
|
derived = derive_service_restriction(
|
|
parent,
|
|
reference=reference,
|
|
temporal=temporal,
|
|
audience=("resident",),
|
|
channels=("portal",),
|
|
add_prerequisites=("tenant-residency",),
|
|
add_required_evidence_types=("identity",),
|
|
add_bindings=(ServiceBinding("workflow", "permit-review"),),
|
|
)
|
|
|
|
self.assertEqual(parent.reference, derived.derived_from_ref)
|
|
self.assertEqual(("resident",), derived.audience)
|
|
self.assertEqual(("portal",), derived.channels)
|
|
self.assertEqual(
|
|
("registered-address", "tenant-residency"),
|
|
derived.prerequisites,
|
|
)
|
|
self.assertIn(ServiceBinding("case", "permit-case"), derived.bindings)
|
|
self.assertEqual(
|
|
parent.availability_requirements,
|
|
derived.availability_requirements,
|
|
)
|
|
self.assertEqual(
|
|
parent.availability_requirements,
|
|
ServiceDefinition.from_mapping(
|
|
derived.to_dict()
|
|
).availability_requirements,
|
|
)
|
|
with self.assertRaisesRegex(InstitutionalContextError, "widen.*audience"):
|
|
derive_service_restriction(
|
|
parent,
|
|
reference=reference,
|
|
temporal=temporal,
|
|
audience=("resident", "visitor"),
|
|
)
|
|
with self.assertRaisesRegex(InstitutionalContextError, "beyond"):
|
|
derive_service_restriction(
|
|
parent,
|
|
reference=reference,
|
|
temporal=replace(temporal, valid_to=None),
|
|
)
|
|
with self.assertRaisesRegex(
|
|
InstitutionalContextError,
|
|
"publication state",
|
|
):
|
|
derive_service_restriction(
|
|
replace(parent, publication_state="suspended"),
|
|
reference=reference,
|
|
temporal=temporal,
|
|
publication_state="published",
|
|
)
|
|
|
|
def test_mandate_resolution_is_effective_scoped_and_deterministic(self) -> None:
|
|
context = _context()
|
|
mandate = MandateDefinition(
|
|
reference=_reference("mandate", "mandate-1"),
|
|
temporal=TemporalRevision(
|
|
revision="1",
|
|
valid_from=NOW - timedelta(days=1),
|
|
valid_to=NOW + timedelta(days=1),
|
|
recorded_at=NOW - timedelta(days=2),
|
|
),
|
|
task_types=("committee.formal_decision",),
|
|
authority_types=("permit",),
|
|
organization_unit_refs=(
|
|
_reference("organization_unit", "unit-1"),
|
|
),
|
|
function_refs=(_reference("function", "function-1"),),
|
|
jurisdiction_refs=context.jurisdiction_refs,
|
|
subject_types=("permit",),
|
|
evidence=context.evidence,
|
|
)
|
|
request = MandateResolutionRequest(
|
|
tenant_id="tenant-1",
|
|
effective_at=NOW,
|
|
task_type="committee.formal_decision",
|
|
authority_type="permit",
|
|
organization_unit_ref=replace(
|
|
_reference("organization_unit", "unit-1"), version="9"
|
|
),
|
|
function_ref=replace(
|
|
_reference("function", "function-1"), version="9"
|
|
),
|
|
jurisdiction_refs=tuple(
|
|
replace(item, version="9") for item in context.jurisdiction_refs
|
|
),
|
|
subject_type="permit",
|
|
)
|
|
|
|
resolution = resolve_mandate_candidates(
|
|
request,
|
|
(
|
|
replace(
|
|
mandate,
|
|
reference=_reference("mandate", "retired"),
|
|
status="retired",
|
|
),
|
|
mandate,
|
|
),
|
|
)
|
|
|
|
self.assertTrue(resolution.competent)
|
|
self.assertEqual("mandate-1", resolution.mandates[0].reference.object_id)
|
|
self.assertEqual("file-1", resolution.evidence[0].evidence_id)
|
|
|
|
ambiguous = resolve_mandate_candidates(
|
|
request,
|
|
(
|
|
mandate,
|
|
replace(
|
|
mandate,
|
|
reference=_reference("mandate", "mandate-2"),
|
|
),
|
|
),
|
|
)
|
|
self.assertFalse(ambiguous.competent)
|
|
self.assertEqual(2, len(ambiguous.conflict_refs))
|
|
|
|
missing_jurisdiction = resolve_mandate_candidates(
|
|
replace(request, jurisdiction_refs=()),
|
|
(mandate,),
|
|
)
|
|
self.assertFalse(missing_jurisdiction.competent)
|
|
|
|
def test_mandate_resolution_rejects_cross_tenant_candidates(self) -> None:
|
|
request = MandateResolutionRequest(
|
|
tenant_id="tenant-1",
|
|
effective_at=NOW,
|
|
task_type="committee.formal_decision",
|
|
authority_type="permit",
|
|
)
|
|
candidate = MandateDefinition(
|
|
reference=_reference("mandate", "mandate-2", tenant_id="tenant-2"),
|
|
temporal=TemporalRevision(revision="1", recorded_at=NOW),
|
|
task_types=("committee.formal_decision",),
|
|
authority_types=("permit",),
|
|
)
|
|
|
|
with self.assertRaisesRegex(InstitutionalContextError, "cross tenants"):
|
|
resolve_mandate_candidates(request, (candidate,))
|
|
|
|
def test_mandate_lifecycle_revisions_are_linked_by_stable_identity(self) -> None:
|
|
current = MandateDefinition(
|
|
reference=_reference("mandate", "mandate-1"),
|
|
temporal=TemporalRevision(revision="1", recorded_at=NOW),
|
|
task_types=("committee.formal_decision",),
|
|
authority_types=("permit",),
|
|
status="active",
|
|
)
|
|
temporal = TemporalRevision(
|
|
revision="2",
|
|
recorded_at=NOW + timedelta(minutes=1),
|
|
change_reason="Authority temporarily suspended.",
|
|
)
|
|
|
|
suspended = revise_mandate_definition(
|
|
current,
|
|
expected_revision="1",
|
|
temporal=temporal,
|
|
status="suspended",
|
|
suspension_reason="Delegation is under review.",
|
|
)
|
|
|
|
self.assertEqual("active", current.status)
|
|
self.assertEqual("suspended", suspended.status)
|
|
self.assertEqual("2", suspended.reference.version)
|
|
self.assertEqual(current.reference.object_id, suspended.reference.object_id)
|
|
with self.assertRaisesRegex(InstitutionalContextError, "stale"):
|
|
revise_mandate_definition(
|
|
current,
|
|
expected_revision="0",
|
|
temporal=temporal,
|
|
status="suspended",
|
|
suspension_reason="Review.",
|
|
)
|
|
with self.assertRaisesRegex(InstitutionalContextError, "not allowed"):
|
|
revise_mandate_definition(
|
|
suspended,
|
|
expected_revision="2",
|
|
temporal=replace(
|
|
temporal,
|
|
revision="3",
|
|
change_reason="Invalid reset.",
|
|
),
|
|
status="draft",
|
|
)
|
|
|
|
def test_formal_decision_revisions_are_linked_and_occ_guarded(self) -> None:
|
|
context = _context()
|
|
reference = replace(
|
|
_reference("decision", "decision-1"),
|
|
version=context.temporal.revision,
|
|
)
|
|
context = replace(context, decision_ref=reference)
|
|
current = FormalDecision(
|
|
reference=reference,
|
|
temporal=context.temporal,
|
|
decision_type="permit",
|
|
subject_refs=(_reference("case", "case-1"),),
|
|
state="decided",
|
|
authority_context=context,
|
|
fact_evidence=context.evidence,
|
|
legal_bases=context.legal_bases,
|
|
operative_result="Permit granted.",
|
|
reasoning="Protected reasoning.",
|
|
requested_effects=(
|
|
DecisionEffectReference("notify", "requested"),
|
|
),
|
|
)
|
|
with self.assertRaisesRegex(
|
|
InstitutionalContextError,
|
|
"service account",
|
|
):
|
|
replace(current, assurance_level="automated_under_mandate")
|
|
revision = TemporalRevision(
|
|
revision="8",
|
|
valid_from=NOW + timedelta(minutes=1),
|
|
recorded_at=NOW + timedelta(minutes=1),
|
|
change_reason="Decision entered into force.",
|
|
)
|
|
|
|
effective = revise_formal_decision(
|
|
current,
|
|
expected_revision="7",
|
|
temporal=revision,
|
|
state="effective",
|
|
)
|
|
|
|
self.assertEqual("decided", current.state)
|
|
self.assertEqual("8", effective.reference.version)
|
|
self.assertEqual("7", effective.supersedes_ref.version)
|
|
self.assertEqual(
|
|
effective.reference,
|
|
effective.authority_context.decision_ref,
|
|
)
|
|
self.assertEqual(current.requested_effects, effective.requested_effects)
|
|
|
|
with self.assertRaisesRegex(InstitutionalContextError, "stale"):
|
|
revise_formal_decision(
|
|
current,
|
|
expected_revision="6",
|
|
temporal=revision,
|
|
state="effective",
|
|
)
|
|
with self.assertRaisesRegex(InstitutionalContextError, "not allowed"):
|
|
revise_formal_decision(
|
|
effective,
|
|
expected_revision="8",
|
|
temporal=replace(
|
|
revision,
|
|
revision="9",
|
|
change_reason="Invalid regression.",
|
|
),
|
|
state="proposed",
|
|
)
|
|
|
|
corrected = revise_formal_decision(
|
|
effective,
|
|
expected_revision="8",
|
|
temporal=replace(
|
|
revision,
|
|
revision="9",
|
|
recorded_at=NOW + timedelta(minutes=2),
|
|
change_reason="Corrected operative wording.",
|
|
),
|
|
state="corrected",
|
|
operative_result="Permit granted subject to condition A.",
|
|
)
|
|
self.assertEqual(effective.reference, corrected.correction_of_ref)
|
|
self.assertEqual(effective.reference, corrected.supersedes_ref)
|
|
|
|
def test_party_corrections_and_representation_revocation_are_versioned(self) -> None:
|
|
context = _context()
|
|
represented = _reference("party", "party-applicant")
|
|
representative = _reference("party", "party-agent")
|
|
representation = 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=context.evidence,
|
|
)
|
|
current = ProcedureParty(
|
|
reference=represented,
|
|
procedure_ref=_reference("case", "case-1"),
|
|
role="applicant",
|
|
subject=PartySubjectReference(
|
|
kind="identity",
|
|
provider="identity",
|
|
subject_id="identity-1",
|
|
tenant_id="tenant-1",
|
|
version="2",
|
|
),
|
|
temporal=TemporalRevision(revision="1", recorded_at=NOW),
|
|
permitted_channels=("mail", "postbox"),
|
|
preferred_channels=("mail",),
|
|
delivery_recipient=True,
|
|
representations=(representation,),
|
|
contact_snapshot_refs=("addresses:snapshot-1",),
|
|
evidence=context.evidence,
|
|
)
|
|
revision = TemporalRevision(
|
|
revision="2",
|
|
valid_from=NOW,
|
|
recorded_at=NOW + timedelta(minutes=1),
|
|
change_reason="Correct preferred delivery channel.",
|
|
)
|
|
|
|
corrected = revise_procedure_party(
|
|
current,
|
|
expected_revision="1",
|
|
temporal=revision,
|
|
preferred_channels=("postbox",),
|
|
)
|
|
revoked_power = revoke_party_representation(
|
|
representation,
|
|
expected_revision="1",
|
|
temporal=TemporalRevision(
|
|
revision="2",
|
|
valid_from=NOW - timedelta(days=1),
|
|
valid_to=NOW + timedelta(minutes=2),
|
|
recorded_at=NOW + timedelta(minutes=2),
|
|
change_reason="Power was revoked.",
|
|
),
|
|
revoked_at=NOW + timedelta(minutes=2),
|
|
)
|
|
|
|
self.assertEqual(("mail",), current.preferred_channels)
|
|
self.assertEqual(("postbox",), corrected.preferred_channels)
|
|
self.assertEqual("2", corrected.reference.version)
|
|
self.assertIsNone(representation.revoked_at)
|
|
self.assertEqual(NOW + timedelta(minutes=2), revoked_power.revoked_at)
|
|
with self.assertRaisesRegex(InstitutionalContextError, "stale"):
|
|
revise_procedure_party(
|
|
current,
|
|
expected_revision="0",
|
|
temporal=revision,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|