feat: prove assisted resident permit intake
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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."
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user