feat: add governed assisted form intake
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user