feat: enforce shared UI foundations and pin reference journey
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "resident-parking-permit-berlin-style-reference",
|
||||
"title": "Resident parking permit",
|
||||
"title_de": "Anwohnerparkausweis",
|
||||
"locale": "de-DE",
|
||||
"service": {
|
||||
"object_id": "resident-parking-permit",
|
||||
"key": "resident_parking_permit.apply",
|
||||
"version": "6",
|
||||
"audience": "resident",
|
||||
"required_evidence_types": [
|
||||
"application",
|
||||
"identity",
|
||||
"primary_residence",
|
||||
"vehicle_registration"
|
||||
],
|
||||
"channels": ["portal", "assisted"]
|
||||
},
|
||||
"form": {
|
||||
"object_id": "resident-parking-permit-application",
|
||||
"version": "3",
|
||||
"fields": {
|
||||
"applicant_name": "Ada Lovelace",
|
||||
"residence_address": "Musterstrasse 17, 10115 Berlin",
|
||||
"licence_plate": "B-AL 1843"
|
||||
}
|
||||
},
|
||||
"case": {
|
||||
"type_key": "resident-parking-permit-application",
|
||||
"number": "RPP-2026-0001",
|
||||
"initial_status": "intake",
|
||||
"decided_status": "decided",
|
||||
"deadline_days": 30
|
||||
},
|
||||
"workflow": {
|
||||
"definition_name": "Resident parking permit decision",
|
||||
"work_item_title": "Decide the resident parking permit application",
|
||||
"instructions": "Review identity, primary residence, vehicle evidence, and the effective local rule before recording the decision."
|
||||
},
|
||||
"decision": {
|
||||
"type": "resident-parking-permit",
|
||||
"operative_result": "Resident parking permit granted.",
|
||||
"reasoning": "Identity, primary residence, vehicle registration, and the effective local rule were verified.",
|
||||
"delivery_channel": "postbox",
|
||||
"remedy": "review:administrative-court"
|
||||
},
|
||||
"records": {
|
||||
"file_plan_key": "traffic.resident-parking-permits",
|
||||
"retention_policy_ref": "records:resident-parking-permit"
|
||||
},
|
||||
"acceptance": {
|
||||
"automated": [
|
||||
"The published service and exact form revision drive digital intake.",
|
||||
"An idempotent replay returns the same persisted submission.",
|
||||
"The human review handoff survives a database-session restart and remains visible in Tasks until completion.",
|
||||
"The formal decision retains party, mandate, legal-basis, evidence, delivery, review, and exact revision references.",
|
||||
"Forms Runtime, Cases, and Decisions can expose exact snapshots for explicit eAkte filing."
|
||||
],
|
||||
"manual_or_target": [
|
||||
"Complete the digital journey with keyboard and screen reader at desktop and mobile widths.",
|
||||
"Repeat intake through an assisted-channel operator without weakening identity, purpose, or provenance.",
|
||||
"Verify the configured Postbox or external delivery provider, including unknown outcome and reconciliation.",
|
||||
"Restore the pinned composition and reconstruct the exact form, case, decision, delivery evidence, and eAkte chronology.",
|
||||
"Transfer through a named archive profile and retain independently signed target evidence."
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
@@ -66,6 +68,11 @@ from govoplan_services.backend.service import SqlServiceDefinitionProvider, reco
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
||||
JOURNEY = json.loads(
|
||||
(Path(__file__).parent / "fixtures/resident_parking_permit_journey.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _reference(
|
||||
@@ -177,31 +184,31 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
service = ServiceDefinition(
|
||||
reference=_reference(
|
||||
"service",
|
||||
"permit-service",
|
||||
JOURNEY["service"]["object_id"],
|
||||
owner="services",
|
||||
version="5",
|
||||
version=JOURNEY["service"]["version"],
|
||||
),
|
||||
key="permit.apply",
|
||||
key=JOURNEY["service"]["key"],
|
||||
temporal=TemporalRevision(
|
||||
revision="5",
|
||||
revision=JOURNEY["service"]["version"],
|
||||
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",
|
||||
title=JOURNEY["title"],
|
||||
audience=("resident",),
|
||||
legal_bases=(legal_basis,),
|
||||
required_evidence_types=("application", "identity"),
|
||||
channels=("portal", "postbox"),
|
||||
required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
|
||||
channels=tuple(JOURNEY["service"]["channels"]) + ("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"),
|
||||
ServiceBinding("case", JOURNEY["case"]["type_key"]),
|
||||
ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
|
||||
ServiceBinding("result", f"decision:{JOURNEY['decision']['type']}"),
|
||||
),
|
||||
remedy_refs=("review:administrative-court",),
|
||||
publication_state="published",
|
||||
@@ -230,37 +237,40 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
upsert_case_status(
|
||||
session,
|
||||
principal,
|
||||
status_key="intake",
|
||||
status_key=JOURNEY["case"]["initial_status"],
|
||||
label="Intake",
|
||||
)
|
||||
upsert_case_status(
|
||||
session,
|
||||
principal,
|
||||
status_key="decided",
|
||||
status_key=JOURNEY["case"]["decided_status"],
|
||||
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"),
|
||||
type_key=JOURNEY["case"]["type_key"],
|
||||
label=JOURNEY["title"],
|
||||
initial_status_key=JOURNEY["case"]["initial_status"],
|
||||
allowed_status_keys=(
|
||||
JOURNEY["case"]["initial_status"],
|
||||
JOURNEY["case"]["decided_status"],
|
||||
),
|
||||
)
|
||||
case_record = create_case_from_intake(
|
||||
session,
|
||||
principal,
|
||||
plan=intake,
|
||||
case_number="PERMIT-2026-0001",
|
||||
title="Permit application",
|
||||
status_key=None,
|
||||
case_number=JOURNEY["case"]["number"],
|
||||
title=JOURNEY["title"],
|
||||
status_key=JOURNEY["case"]["initial_status"],
|
||||
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),
|
||||
deadline_at=NOW + timedelta(days=JOURNEY["case"]["deadline_days"]),
|
||||
)
|
||||
|
||||
applicant = _reference("party", "applicant", owner="parties")
|
||||
@@ -333,7 +343,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
change_reason="Permit authority delegated.",
|
||||
),
|
||||
task_types=("committee.formal_decision",),
|
||||
authority_types=("permit",),
|
||||
authority_types=(JOURNEY["decision"]["type"],),
|
||||
organization_unit_refs=(organization,),
|
||||
function_refs=(function,),
|
||||
jurisdiction_refs=(jurisdiction,),
|
||||
@@ -380,7 +390,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
object_id="item-1",
|
||||
revision=1,
|
||||
state="deliberating",
|
||||
title="Permit application",
|
||||
title=JOURNEY["title"],
|
||||
parent_id=meeting.object_id,
|
||||
recorded_at=NOW,
|
||||
change_reason="Agenda item entered deliberation.",
|
||||
@@ -398,7 +408,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
object_id="vote-1",
|
||||
revision=1,
|
||||
state="closed",
|
||||
title="Vote on permit application",
|
||||
title=f"Vote on {JOURNEY['title'].lower()}",
|
||||
parent_id=agenda.object_id,
|
||||
recorded_at=NOW,
|
||||
change_reason="Vote result accepted.",
|
||||
@@ -445,7 +455,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
effective_at=NOW,
|
||||
meeting_ref="meeting-1",
|
||||
agenda_item_ref="item-1",
|
||||
decision_type="permit",
|
||||
decision_type=JOURNEY["decision"]["type"],
|
||||
subject_refs=(case_record.reference,),
|
||||
organization_unit_ref=organization,
|
||||
function_ref=function,
|
||||
@@ -461,8 +471,8 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
),
|
||||
fact_evidence=(application_evidence, address_evidence),
|
||||
legal_bases=(legal_basis,),
|
||||
operative_result="Permit granted.",
|
||||
reasoning="The application satisfies the effective rule.",
|
||||
operative_result=JOURNEY["decision"]["operative_result"],
|
||||
reasoning=JOURNEY["decision"]["reasoning"],
|
||||
case_ref=case_record.reference,
|
||||
jurisdiction_refs=(jurisdiction,),
|
||||
party_refs=(applicant, representative),
|
||||
@@ -475,7 +485,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
classification="restricted",
|
||||
purposes=("permit-decision", "party-delivery"),
|
||||
legal_basis_refs=("permit-law:3@2026-01",),
|
||||
retention_policy_ref="records:permit",
|
||||
retention_policy_ref=JOURNEY["records"]["retention_policy_ref"],
|
||||
disclosure_state="partly_disclosable",
|
||||
),
|
||||
),
|
||||
@@ -568,7 +578,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
get_case(session, principal, case_id="case-1"),
|
||||
)
|
||||
self.assertEqual(
|
||||
"decided",
|
||||
JOURNEY["case"]["decided_status"],
|
||||
get_workspace_object(
|
||||
session,
|
||||
principal,
|
||||
@@ -583,7 +593,7 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
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(JOURNEY["decision"]["reasoning"], reconstruction["reasoning"])
|
||||
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
|
||||
@@ -82,6 +84,11 @@ from govoplan_workflow_engine.backend.work_items import WorkflowWorkItemProvider
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
||||
JOURNEY = json.loads(
|
||||
(Path(__file__).parent / "fixtures/resident_parking_permit_journey.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _service() -> ServiceDefinition:
|
||||
@@ -89,24 +96,24 @@ def _service() -> ServiceDefinition:
|
||||
reference=InstitutionalReference(
|
||||
kind="service",
|
||||
owner_module="portal",
|
||||
object_id="permit",
|
||||
object_id=JOURNEY["service"]["object_id"],
|
||||
tenant_id="tenant-1",
|
||||
version="5",
|
||||
version=JOURNEY["service"]["version"],
|
||||
),
|
||||
key="permit.apply",
|
||||
key=JOURNEY["service"]["key"],
|
||||
temporal=TemporalRevision(
|
||||
revision="5",
|
||||
revision=JOURNEY["service"]["version"],
|
||||
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",),
|
||||
required_evidence_types=("application",),
|
||||
title=JOURNEY["title"],
|
||||
audience=(JOURNEY["service"]["audience"],),
|
||||
required_evidence_types=tuple(JOURNEY["service"]["required_evidence_types"]),
|
||||
bindings=(
|
||||
ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE),
|
||||
ServiceBinding("case", "permit-application"),
|
||||
ServiceBinding("workflow", "workflow:permit-review"),
|
||||
ServiceBinding("case", JOURNEY["case"]["type_key"]),
|
||||
ServiceBinding("workflow", "workflow:resident-parking-permit-review"),
|
||||
),
|
||||
publication_state="published",
|
||||
)
|
||||
@@ -230,8 +237,14 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
self.assertTrue(entries[0].available)
|
||||
self.assertIs(definition, entries[0].definition)
|
||||
self.assertEqual(definition.reference, plan.service_ref)
|
||||
self.assertEqual("5", plan.context.service_ref.version)
|
||||
self.assertEqual("workflow:permit-review", plan.workflow_refs[0])
|
||||
self.assertEqual(JOURNEY["service"]["version"], plan.context.service_ref.version)
|
||||
self.assertEqual("workflow:resident-parking-permit-review", plan.workflow_refs[0])
|
||||
|
||||
def test_reference_fixture_names_remaining_manual_target_evidence(self) -> None:
|
||||
self.assertEqual("Anwohnerparkausweis", JOURNEY["title_de"])
|
||||
self.assertEqual("de-DE", JOURNEY["locale"])
|
||||
self.assertEqual(5, len(JOURNEY["acceptance"]["automated"]))
|
||||
self.assertEqual(5, len(JOURNEY["acceptance"]["manual_or_target"]))
|
||||
|
||||
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
@@ -252,19 +265,19 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
reference=InstitutionalReference(
|
||||
kind="form",
|
||||
owner_module="forms",
|
||||
object_id="permit-application",
|
||||
object_id=JOURNEY["form"]["object_id"],
|
||||
tenant_id="tenant-1",
|
||||
version="3",
|
||||
),
|
||||
key="permit-application",
|
||||
key=JOURNEY["form"]["object_id"],
|
||||
temporal=TemporalRevision(
|
||||
revision="3",
|
||||
valid_from=NOW - timedelta(days=1),
|
||||
valid_to=NOW + timedelta(days=1),
|
||||
recorded_at=NOW - timedelta(days=2),
|
||||
change_reason="Publish the permit application.",
|
||||
change_reason="Publish the resident parking permit application.",
|
||||
),
|
||||
title="Permit application",
|
||||
title=JOURNEY["title"],
|
||||
fields=(
|
||||
FormFieldDefinition(
|
||||
key="applicant_name",
|
||||
@@ -272,6 +285,18 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
required=True,
|
||||
constraints={"min_length": 2},
|
||||
),
|
||||
FormFieldDefinition(
|
||||
key="residence_address",
|
||||
label="Primary residence address",
|
||||
required=True,
|
||||
constraints={"min_length": 5},
|
||||
),
|
||||
FormFieldDefinition(
|
||||
key="licence_plate",
|
||||
label="Vehicle licence plate",
|
||||
required=True,
|
||||
constraints={"min_length": 3},
|
||||
),
|
||||
),
|
||||
publication_state="published",
|
||||
allow_drafts=True,
|
||||
@@ -286,18 +311,18 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
reference=InstitutionalReference(
|
||||
kind="service",
|
||||
owner_module="services",
|
||||
object_id="permit",
|
||||
object_id=JOURNEY["service"]["object_id"],
|
||||
tenant_id="tenant-1",
|
||||
version="6",
|
||||
version=JOURNEY["service"]["version"],
|
||||
),
|
||||
key="permit.apply",
|
||||
key=JOURNEY["service"]["key"],
|
||||
temporal=TemporalRevision(
|
||||
revision="6",
|
||||
revision=JOURNEY["service"]["version"],
|
||||
valid_from=NOW - timedelta(days=1),
|
||||
valid_to=NOW + timedelta(days=1),
|
||||
recorded_at=NOW - timedelta(days=2),
|
||||
),
|
||||
title="Apply for a permit",
|
||||
title=JOURNEY["title"],
|
||||
audience=("public",),
|
||||
bindings=(binding,),
|
||||
publication_state="published",
|
||||
@@ -311,7 +336,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
reference=service.reference,
|
||||
requested_at=NOW,
|
||||
idempotency_key="portal-form-launch-1",
|
||||
parameters={"applicant_name": "Ada Lovelace"},
|
||||
parameters=JOURNEY["form"]["fields"],
|
||||
)
|
||||
replay = directory.launch_service(
|
||||
session,
|
||||
@@ -319,7 +344,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
reference=service.reference,
|
||||
requested_at=NOW,
|
||||
idempotency_key="portal-form-launch-1",
|
||||
parameters={"applicant_name": "Ada Lovelace"},
|
||||
parameters=JOURNEY["form"]["fields"],
|
||||
)
|
||||
instance = FormRuntimeService(registry).get_instance(
|
||||
session,
|
||||
@@ -348,7 +373,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(NOW, instance.definition_ref.valid_at)
|
||||
self.assertEqual(service.reference, instance.service_ref)
|
||||
self.assertEqual("Ada Lovelace", instance.values["applicant_name"])
|
||||
self.assertEqual(JOURNEY["form"]["fields"], instance.values)
|
||||
finally:
|
||||
session.close()
|
||||
engine.dispose()
|
||||
@@ -392,7 +417,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
payload=WorkflowDefinitionCreateRequest(
|
||||
name="Permit decision",
|
||||
name=JOURNEY["workflow"]["definition_name"],
|
||||
graph=WorkflowGraph(
|
||||
nodes=[
|
||||
WorkflowNode(
|
||||
@@ -403,8 +428,8 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
id="review",
|
||||
type="workflow.activity",
|
||||
config={
|
||||
"title": "Decide the permit application",
|
||||
"instructions": "Review the filed evidence and record the decision.",
|
||||
"title": JOURNEY["workflow"]["work_item_title"],
|
||||
"instructions": JOURNEY["workflow"]["instructions"],
|
||||
"assignee": "account:account-1",
|
||||
"due_after": "2d",
|
||||
},
|
||||
@@ -464,7 +489,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||
self.assertEqual(1, work.total)
|
||||
self.assertEqual(step_id, work.items[0].id)
|
||||
self.assertEqual(
|
||||
"Decide the permit application",
|
||||
JOURNEY["workflow"]["work_item_title"],
|
||||
work.items[0].title,
|
||||
)
|
||||
self.assertTrue(work.items[0].action_url.startswith("/workflow?"))
|
||||
|
||||
Reference in New Issue
Block a user