feat: add governed assisted form intake

This commit is contained in:
2026-08-19 02:45:53 +02:00
parent eea3db3d2e
commit d83bb92ec8
18 changed files with 1895 additions and 39 deletions
+207
View File
@@ -27,6 +27,7 @@ from govoplan_forms.backend.service import (
)
from govoplan_forms_runtime.backend.db.models import (
FormAcknowledgement,
FormAssistedConfirmation,
FormHandoffEffect,
FormInstanceEvent,
FormInstanceIdentity,
@@ -119,6 +120,7 @@ class FormsRuntimeTests(unittest.TestCase):
FormHandoffEffect.__table__,
FormIntakeProfile.__table__,
FormIntakeSession.__table__,
FormAssistedConfirmation.__table__,
FormAcknowledgement.__table__,
):
table.create(self.engine)
@@ -780,6 +782,211 @@ class FormsRuntimeTests(unittest.TestCase):
now=NOW + timedelta(minutes=1),
)
def test_assisted_intake_is_resumable_and_requires_exact_readback_evidence(
self,
) -> None:
intake = FormIntakeService(self.registry)
profile = intake.create_profile(
self.session,
self.principal,
definition_ref=self.definition.reference,
mode="assisted",
custodian_ref="function:parking-permits",
recorded_at=NOW,
)
started = intake.start_assisted(
self.session,
self.principal,
profile_id=profile.profile_id,
values={"name": "Ada", "delivery": "mail"},
channel="counter",
affected_party_ref="party:resident-1",
represented_party_ref=None,
authority_basis="self",
purpose="Apply for a resident parking permit.",
legal_basis_ref="law:resident-parking",
consent_basis="verbal-consent",
notice_given=True,
responsible_function_ref="function:parking-permits",
language="de",
accessibility_needs=("plain-language",),
field_sources={
"name": {
"source": "person_statement",
"confidence": "stated",
"declared_by_ref": "party:resident-1",
},
"delivery": {
"source": "person_statement",
"confidence": "stated",
"declared_by_ref": "party:resident-1",
},
},
idempotency_key="assisted-start-1",
recorded_at=NOW + timedelta(minutes=1),
)
self.assertIsNone(started.token)
self.assertEqual("assisted", started.mode)
self.assertEqual("counter", started.instance.metadata["intake"]["channel"])
replay = intake.start_assisted(
self.session,
self.principal,
profile_id=profile.profile_id,
values={"name": "Ada", "delivery": "mail"},
channel="counter",
affected_party_ref="party:resident-1",
represented_party_ref=None,
authority_basis="self",
purpose="Apply for a resident parking permit.",
legal_basis_ref="law:resident-parking",
consent_basis="verbal-consent",
notice_given=True,
responsible_function_ref="function:parking-permits",
language="de",
accessibility_needs=("plain-language",),
field_sources={
"name": {
"source": "person_statement",
"confidence": "stated",
"declared_by_ref": "party:resident-1",
},
"delivery": {
"source": "person_statement",
"confidence": "stated",
"declared_by_ref": "party:resident-1",
},
},
idempotency_key="assisted-start-1",
recorded_at=NOW + timedelta(minutes=1),
)
self.assertTrue(replay.replayed)
self.assertEqual(started.instance.instance_id, replay.instance.instance_id)
with self.assertRaisesRegex(FormRuntimeError, "read-back confirmation"):
self.runtime.submit_instance(
self.session,
self.principal,
instance_id=started.instance.instance_id,
expected_revision=1,
values=started.instance.values,
attachment_refs=(),
signature_refs=(),
idempotency_key="assisted-submit-unconfirmed",
recorded_at=NOW + timedelta(minutes=2),
)
with self.assertRaisesRegex(FormIntakeError, "Save assisted corrections"):
intake.record_assisted_confirmation(
self.session,
self.principal,
instance_id=started.instance.instance_id,
expected_revision=1,
values={"name": "Ada Lovelace", "delivery": "mail"},
attachment_refs=(),
signature_refs=(),
outcome="corrected",
method="spoken_readback",
confirmed_by_ref="party:resident-1",
confirmed_at=NOW + timedelta(minutes=2),
idempotency_key="readback-unsaved",
field_sources={
key: {"source": "person_statement", "confidence": "stated"}
for key in ("name", "delivery")
},
correction_note="Applicant corrected the full name.",
)
confirmed = intake.record_assisted_confirmation(
self.session,
self.principal,
instance_id=started.instance.instance_id,
expected_revision=1,
values=started.instance.values,
attachment_refs=(),
signature_refs=(),
outcome="confirmed",
method="spoken_readback",
confirmed_by_ref="party:resident-1",
confirmed_at=NOW + timedelta(minutes=3),
idempotency_key="readback-1",
field_sources={
key: {"source": "person_statement", "confidence": "stated"}
for key in ("name", "delivery")
},
)
self.assertEqual("confirmed", confirmed.outcome)
corrected_draft = self.runtime.update_draft(
self.session,
self.principal,
instance_id=started.instance.instance_id,
expected_revision=1,
values={"name": "Ada Lovelace", "delivery": "mail"},
attachment_refs=(),
signature_refs=(),
idempotency_key="assisted-correction-save",
recorded_at=NOW + timedelta(minutes=4),
change_reason="Correct the applicant name after read-back.",
)
with self.assertRaisesRegex(FormRuntimeError, "read-back confirmation"):
self.runtime.submit_instance(
self.session,
self.principal,
instance_id=corrected_draft.instance_id,
expected_revision=2,
values=corrected_draft.values,
attachment_refs=(),
signature_refs=(),
idempotency_key="assisted-submit-stale-readback",
recorded_at=NOW + timedelta(minutes=5),
)
corrected = intake.record_assisted_confirmation(
self.session,
self.principal,
instance_id=corrected_draft.instance_id,
expected_revision=2,
values=corrected_draft.values,
attachment_refs=(),
signature_refs=(),
outcome="corrected",
method="written_preview",
confirmed_by_ref="party:resident-1",
confirmed_at=NOW + timedelta(minutes=6),
idempotency_key="readback-2",
field_sources={
key: {"source": "person_statement", "confidence": "stated"}
for key in ("name", "delivery")
},
correction_note="Applicant corrected the full name.",
)
self.assertNotEqual(confirmed.payload_sha256, corrected.payload_sha256)
submitted = self.runtime.submit_instance(
self.session,
self.principal,
instance_id=corrected_draft.instance_id,
expected_revision=2,
values=corrected_draft.values,
attachment_refs=(),
signature_refs=(),
idempotency_key="assisted-submit-confirmed",
recorded_at=NOW + timedelta(minutes=7),
)
self.assertEqual("submitted", submitted.status)
stored_session = (
self.session.query(FormIntakeSession)
.filter(FormIntakeSession.instance_id == submitted.instance_id)
.one()
)
self.assertEqual("submitted", stored_session.status)
self.assertEqual(
2,
len(
intake.list_assisted_confirmations(
self.session,
self.principal,
instance_id=submitted.instance_id,
)
),
)
def test_native_acknowledgement_is_bound_to_actor_and_exact_payload(self) -> None:
draft = self.runtime.create_instance(
self.session,
+2 -1
View File
@@ -4,6 +4,7 @@ import unittest
from govoplan_forms_runtime.backend.manifest import (
ADMIN_SCOPE,
ASSIST_SCOPE,
PARTICIPATE_SCOPE,
READ_SCOPE,
WRITE_SCOPE,
@@ -19,7 +20,7 @@ class ManifestTests(unittest.TestCase):
self.assertEqual(manifest.dependencies, ("access", "forms"))
self.assertEqual(
{permission.scope for permission in manifest.permissions},
{PARTICIPATE_SCOPE, READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE},
{PARTICIPATE_SCOPE, ASSIST_SCOPE, READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE},
)
participant = next(
item
+2 -1
View File
@@ -34,12 +34,13 @@ class FormsRuntimeMigrationTests(unittest.TestCase):
"form_handoff_effects",
"form_intake_profiles",
"form_intake_sessions",
"form_assisted_confirmations",
"form_acknowledgements",
}.issubset(inspect(engine).get_table_names())
)
with engine.connect() as connection:
self.assertIn(
"b4e6f8a0c2d3",
"c5f7a9b1d3e4",
set(MigrationContext.configure(connection).get_current_heads()),
)
finally: