feat: prove assisted resident permit intake
Dependency Audit / dependency-audit (push) Successful in 1m40s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m37s

This commit is contained in:
2026-08-19 02:45:53 +02:00
parent e2f505eeab
commit 69519a92b4
6 changed files with 211 additions and 10 deletions
@@ -72,6 +72,16 @@ An assisted session is a resumable work item, not a privileged bypass. It:
8. creates follow-up tasks when original documents, signatures, translation,
or verification remain outstanding.
The first executable slice is implemented in Forms Runtime for authenticated
assisted sessions. Administrators enable an exact published Form revision;
operators then record channel, party and representation references, authority,
purpose, notice, responsible function, language, accessibility needs, and
field-level source/confidence provenance. Read-back outcomes are append-only and
payload-bound. A draft correction changes the Form revision and invalidates the
prior confirmation for submission. The resident-parking-permit fixture proves
resume and submission enforcement; browser accessibility and target archive
evidence remain acceptance work.
The helper's normal account and represented function remain in the audit
chain. Assistance never grants access to unrelated records about the person.
+6 -2
View File
@@ -57,8 +57,12 @@ all three contribute metadata-only native Search projections that can be
rebuilt from authoritative state. The executable fixtures prove those native
transitions without claiming archival custody. A persisted Workflow Engine
handoff is now reloaded through the Tasks aggregation surface and remains
visible until the authoritative Workflow transition completes. The journey
still needs pinned-composition reconstruction evidence and one target-tested
visible until the authoritative Workflow transition completes. Authenticated
assisted intake now uses the same exact Form revision and validation as digital
intake while retaining purpose, authority, party, channel, accessibility,
source, correction, and payload-bound read-back evidence across a session
restart. The journey still needs browser accessibility evidence for both
channels, pinned-composition reconstruction evidence, and one target-tested
archive profile.
## Why this sequence
+4 -3
View File
@@ -98,7 +98,7 @@ reconciliation, governed export/delivery, and browser-level handoff evidence.
| --- | --- | --- |
| No reference-ready product package | The platform cannot yet make a bounded supported-product claim | Complete one named target composition and evidence bundle |
| Human-work spine is only an MVP | Tasks aggregates explicit work plus Workflow, Approval, and unread Postbox projections, but broad domain coverage, deadline escalation, assignment lifecycle, and focused product UX remain | Extend source providers through the three reference journeys and prove overdue/reassignment behavior in browser tests |
| Records/eAkte target integration incomplete | Native lifecycle, retention, holds, approval, recovery, and transfer simulation are implemented, but real custody is not proved | Target-test one archive/xdomea profile and complete the assisted reference journey |
| Records/eAkte target integration incomplete | Native lifecycle, retention, holds, approval, recovery, and transfer simulation are implemented, but real custody is not proved | Target-test one archive/xdomea profile and browser-test the now server-enforced assisted reference journey |
| Cross-cutting governance adoption uneven | Historical and purpose-sensitive behavior varies by module | Enforced adoption declarations and route/query/effect migration |
| Explicit help/accessibility depth incomplete | German/reference and F1 association gates now pass, but generic fallback remains too common | High-risk German help content and browser/a11y matrix |
| Real federation absent | Cross-institution exchange remains connector-specific | Paired-instance signed exchange and reconciliation proof |
@@ -111,8 +111,9 @@ reconciliation, governed export/delivery, and browser-level handoff evidence.
2. Complete governed communication and Postbox against a named target.
3. Complete the monthly-data flow and use it as the data foundation for
sanctions screening.
4. Complete the browser and resumable-work proof for the digital and assisted
service-to-decision journey with its existing exact eAkte filing contracts.
4. Complete the browser proof for the digital and assisted service-to-decision
journey; server-side assisted resume, provenance, correction, and read-back
enforcement now complement its existing exact eAkte filing contracts.
5. Complete native PostgreSQL search coverage for remaining journey-owned
objects and prove reauthorization and reindex operations at target volume;
keep OpenSearch optional. Communication, Records, service-to-decision,
+12 -3
View File
@@ -25,6 +25,14 @@ Service discovery -> exact Form revision -> validated draft/submission
-> receipt and handoff evidence -> Case or Workflow owner
```
The assisted path now creates an authenticated, resumable session against that
same exact Form revision. It records channel, affected and represented parties,
authority, purpose, notice, responsible function, language, accessibility
support, and field provenance. Submission fails closed until an immutable
read-back outcome matches the current revision, values, attachments, and
signatures. Saving a correction therefore requires a fresh confirmation rather
than silently reusing old evidence.
Services, Cases, Parties, Mandates, Committee, and Decisions retain immutable
provider-owned revisions for the parts they own. Portal, Cases, and Committee
consume capabilities for cross-module semantics only. The package does not
@@ -59,9 +67,10 @@ The executable fixture in
`tests/test_institutional_governance_journey.py` proves SQL-backed Service,
Case, Party, Mandate, Committee meeting/agendum/vote/minute, and Decision state.
`tests/test_institutional_service_journey.py` separately proves exact Portal
Form launch, persisted submission provenance, idempotent replay, and a durable
Workflow handoff that remains visible through Tasks after the database session
is reopened and disappears only after the Workflow Engine records completion.
Form launch, persisted submission provenance, idempotent replay, resumable
assisted intake with enforced read-back evidence, and a durable Workflow handoff
that remains visible through Tasks after the database session is reopened and
disappears only after the Workflow Engine records completion.
Module-level Records source tests prove exact Form submission, Case revision,
and Decision revision filing. Target-environment browser accessibility,
production identity and delivery, a named archive profile, and recovery evidence
+17 -1
View File
@@ -25,6 +25,21 @@
"licence_plate": "B-AL 1843"
}
},
"assisted_intake": {
"channel": "counter",
"affected_party_ref": "party:resident-ada-lovelace",
"represented_party_ref": null,
"authority_basis": "self",
"purpose": "Apply for a resident parking permit.",
"legal_basis_ref": "law:resident-parking-permit",
"consent_basis": "in-person-confirmation",
"notice_given": true,
"responsible_function_ref": "function:parking-permits",
"language": "de",
"accessibility_needs": ["plain-language"],
"confirmation_method": "written_preview",
"confirmation_outcome": "confirmed"
},
"case": {
"type_key": "resident-parking-permit-application",
"number": "RPP-2026-0001",
@@ -51,6 +66,7 @@
"acceptance": {
"automated": [
"The published service and exact form revision drive digital intake.",
"An authenticated assisted session uses the same exact form and validation rules while retaining purpose, authority, channel, party, accessibility, source, correction, and read-back provenance.",
"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.",
@@ -58,7 +74,7 @@
],
"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.",
"Complete the assisted operator journey with keyboard and screen reader at desktop and mobile widths.",
"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."
+162 -1
View File
@@ -44,11 +44,16 @@ from govoplan_forms.backend.service import (
record_form_definition,
)
from govoplan_forms_runtime.backend.db.models import (
FormAssistedConfirmation,
FormInstanceEvent,
FormInstanceIdentity,
FormInstanceRevision,
FormIntakeProfile,
FormIntakeSession,
)
from govoplan_forms_runtime.backend.intake import FormIntakeService
from govoplan_forms_runtime.backend.service import (
FormRuntimeError,
FormRuntimeService,
FormsServiceLauncher,
)
@@ -243,7 +248,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
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(6, 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:
@@ -378,6 +383,162 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
session.close()
engine.dispose()
def test_assisted_intake_reuses_exact_form_and_persists_readback_provenance(
self,
) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
for table in (
FormDefinitionRevision.__table__,
FormInstanceIdentity.__table__,
FormInstanceRevision.__table__,
FormInstanceEvent.__table__,
FormIntakeProfile.__table__,
FormIntakeSession.__table__,
FormAssistedConfirmation.__table__,
):
table.create(engine)
sessions = sessionmaker(bind=engine)
principal = _Principal()
assisted = JOURNEY["assisted_intake"]
try:
with sessions() as session:
form = record_form_definition(
session,
principal,
definition=FormDefinition(
reference=InstitutionalReference(
kind="form",
owner_module="forms",
object_id=JOURNEY["form"]["object_id"],
tenant_id="tenant-1",
version=JOURNEY["form"]["version"],
),
key=JOURNEY["form"]["object_id"],
temporal=TemporalRevision(
revision=JOURNEY["form"]["version"],
recorded_at=NOW - timedelta(days=2),
change_reason="Publish the resident parking permit application.",
),
title=JOURNEY["title"],
fields=tuple(
FormFieldDefinition(
key=key,
label=key.replace("_", " ").title(),
required=True,
constraints={"min_length": 2},
)
for key in JOURNEY["form"]["fields"]
),
publication_state="published",
allow_drafts=True,
handoff_kinds=("case",),
),
)
registry = _FormRegistry(_service())
intake = FormIntakeService(registry)
profile = intake.create_profile(
session,
principal,
definition_ref=form.reference,
mode="assisted",
custodian_ref=assisted["responsible_function_ref"],
recorded_at=NOW,
)
started = intake.start_assisted(
session,
principal,
profile_id=profile.profile_id,
values=JOURNEY["form"]["fields"],
channel=assisted["channel"],
affected_party_ref=assisted["affected_party_ref"],
represented_party_ref=assisted["represented_party_ref"],
authority_basis=assisted["authority_basis"],
purpose=assisted["purpose"],
legal_basis_ref=assisted["legal_basis_ref"],
consent_basis=assisted["consent_basis"],
notice_given=assisted["notice_given"],
responsible_function_ref=assisted["responsible_function_ref"],
language=assisted["language"],
accessibility_needs=assisted["accessibility_needs"],
field_sources={
key: {
"source": "person_statement",
"confidence": "stated",
"declared_by_ref": assisted["affected_party_ref"],
}
for key in JOURNEY["form"]["fields"]
},
idempotency_key="resident-permit-assisted-start",
recorded_at=NOW + timedelta(minutes=1),
)
self.assertEqual(form.reference, started.instance.definition_ref)
self.assertEqual(JOURNEY["form"]["fields"], started.instance.values)
self.assertEqual(
assisted["purpose"], started.instance.metadata["intake"]["purpose"]
)
instance_id = started.instance.instance_id
session.commit()
with sessions() as resumed:
runtime = FormRuntimeService(registry)
current = runtime.get_instance(
resumed,
principal,
instance_id=instance_id,
)
self.assertIsNotNone(current)
with self.assertRaisesRegex(FormRuntimeError, "read-back confirmation"):
runtime.submit_instance(
resumed,
principal,
instance_id=instance_id,
expected_revision=current.revision,
values=current.values,
attachment_refs=(),
signature_refs=(),
idempotency_key="resident-permit-assisted-unconfirmed",
recorded_at=NOW + timedelta(minutes=2),
)
confirmation = FormIntakeService(registry).record_assisted_confirmation(
resumed,
principal,
instance_id=instance_id,
expected_revision=current.revision,
values=current.values,
attachment_refs=(),
signature_refs=(),
outcome=assisted["confirmation_outcome"],
method=assisted["confirmation_method"],
confirmed_by_ref=assisted["affected_party_ref"],
confirmed_at=NOW + timedelta(minutes=3),
idempotency_key="resident-permit-assisted-readback",
field_sources={
key: {
"source": "person_statement",
"confidence": "stated",
"declared_by_ref": assisted["affected_party_ref"],
}
for key in JOURNEY["form"]["fields"]
},
)
submitted = runtime.submit_instance(
resumed,
principal,
instance_id=instance_id,
expected_revision=current.revision,
values=current.values,
attachment_refs=(),
signature_refs=(),
idempotency_key="resident-permit-assisted-submit",
recorded_at=NOW + timedelta(minutes=4),
)
resumed.commit()
self.assertEqual("submitted", submitted.status)
self.assertEqual(current.revision, confirmation.instance_revision)
self.assertEqual(assisted["affected_party_ref"], confirmation.confirmed_by_ref)
finally:
engine.dispose()
def test_workflow_handoff_survives_session_reopen_and_projects_into_tasks(
self,
) -> None: