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,
|
8. creates follow-up tasks when original documents, signatures, translation,
|
||||||
or verification remain outstanding.
|
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
|
The helper's normal account and represented function remain in the audit
|
||||||
chain. Assistance never grants access to unrelated records about the person.
|
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
|
rebuilt from authoritative state. The executable fixtures prove those native
|
||||||
transitions without claiming archival custody. A persisted Workflow Engine
|
transitions without claiming archival custody. A persisted Workflow Engine
|
||||||
handoff is now reloaded through the Tasks aggregation surface and remains
|
handoff is now reloaded through the Tasks aggregation surface and remains
|
||||||
visible until the authoritative Workflow transition completes. The journey
|
visible until the authoritative Workflow transition completes. Authenticated
|
||||||
still needs pinned-composition reconstruction evidence and one target-tested
|
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.
|
archive profile.
|
||||||
|
|
||||||
## Why this sequence
|
## 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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.
|
2. Complete governed communication and Postbox against a named target.
|
||||||
3. Complete the monthly-data flow and use it as the data foundation for
|
3. Complete the monthly-data flow and use it as the data foundation for
|
||||||
sanctions screening.
|
sanctions screening.
|
||||||
4. Complete the browser and resumable-work proof for the digital and assisted
|
4. Complete the browser proof for the digital and assisted service-to-decision
|
||||||
service-to-decision journey with its existing exact eAkte filing contracts.
|
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
|
5. Complete native PostgreSQL search coverage for remaining journey-owned
|
||||||
objects and prove reauthorization and reindex operations at target volume;
|
objects and prove reauthorization and reindex operations at target volume;
|
||||||
keep OpenSearch optional. Communication, Records, service-to-decision,
|
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
|
-> 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
|
Services, Cases, Parties, Mandates, Committee, and Decisions retain immutable
|
||||||
provider-owned revisions for the parts they own. Portal, Cases, and Committee
|
provider-owned revisions for the parts they own. Portal, Cases, and Committee
|
||||||
consume capabilities for cross-module semantics only. The package does not
|
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,
|
`tests/test_institutional_governance_journey.py` proves SQL-backed Service,
|
||||||
Case, Party, Mandate, Committee meeting/agendum/vote/minute, and Decision state.
|
Case, Party, Mandate, Committee meeting/agendum/vote/minute, and Decision state.
|
||||||
`tests/test_institutional_service_journey.py` separately proves exact Portal
|
`tests/test_institutional_service_journey.py` separately proves exact Portal
|
||||||
Form launch, persisted submission provenance, idempotent replay, and a durable
|
Form launch, persisted submission provenance, idempotent replay, resumable
|
||||||
Workflow handoff that remains visible through Tasks after the database session
|
assisted intake with enforced read-back evidence, and a durable Workflow handoff
|
||||||
is reopened and disappears only after the Workflow Engine records completion.
|
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,
|
Module-level Records source tests prove exact Form submission, Case revision,
|
||||||
and Decision revision filing. Target-environment browser accessibility,
|
and Decision revision filing. Target-environment browser accessibility,
|
||||||
production identity and delivery, a named archive profile, and recovery evidence
|
production identity and delivery, a named archive profile, and recovery evidence
|
||||||
|
|||||||
+17
-1
@@ -25,6 +25,21 @@
|
|||||||
"licence_plate": "B-AL 1843"
|
"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": {
|
"case": {
|
||||||
"type_key": "resident-parking-permit-application",
|
"type_key": "resident-parking-permit-application",
|
||||||
"number": "RPP-2026-0001",
|
"number": "RPP-2026-0001",
|
||||||
@@ -51,6 +66,7 @@
|
|||||||
"acceptance": {
|
"acceptance": {
|
||||||
"automated": [
|
"automated": [
|
||||||
"The published service and exact form revision drive digital intake.",
|
"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.",
|
"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 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.",
|
"The formal decision retains party, mandate, legal-basis, evidence, delivery, review, and exact revision references.",
|
||||||
@@ -58,7 +74,7 @@
|
|||||||
],
|
],
|
||||||
"manual_or_target": [
|
"manual_or_target": [
|
||||||
"Complete the digital journey with keyboard and screen reader at desktop and mobile widths.",
|
"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.",
|
"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.",
|
"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."
|
"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,
|
record_form_definition,
|
||||||
)
|
)
|
||||||
from govoplan_forms_runtime.backend.db.models import (
|
from govoplan_forms_runtime.backend.db.models import (
|
||||||
|
FormAssistedConfirmation,
|
||||||
FormInstanceEvent,
|
FormInstanceEvent,
|
||||||
FormInstanceIdentity,
|
FormInstanceIdentity,
|
||||||
FormInstanceRevision,
|
FormInstanceRevision,
|
||||||
|
FormIntakeProfile,
|
||||||
|
FormIntakeSession,
|
||||||
)
|
)
|
||||||
|
from govoplan_forms_runtime.backend.intake import FormIntakeService
|
||||||
from govoplan_forms_runtime.backend.service import (
|
from govoplan_forms_runtime.backend.service import (
|
||||||
|
FormRuntimeError,
|
||||||
FormRuntimeService,
|
FormRuntimeService,
|
||||||
FormsServiceLauncher,
|
FormsServiceLauncher,
|
||||||
)
|
)
|
||||||
@@ -243,7 +248,7 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
def test_reference_fixture_names_remaining_manual_target_evidence(self) -> None:
|
def test_reference_fixture_names_remaining_manual_target_evidence(self) -> None:
|
||||||
self.assertEqual("Anwohnerparkausweis", JOURNEY["title_de"])
|
self.assertEqual("Anwohnerparkausweis", JOURNEY["title_de"])
|
||||||
self.assertEqual("de-DE", JOURNEY["locale"])
|
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"]))
|
self.assertEqual(5, len(JOURNEY["acceptance"]["manual_or_target"]))
|
||||||
|
|
||||||
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
|
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
|
||||||
@@ -378,6 +383,162 @@ class InstitutionalServiceJourneyTests(unittest.TestCase):
|
|||||||
session.close()
|
session.close()
|
||||||
engine.dispose()
|
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(
|
def test_workflow_handoff_survives_session_reopen_and_projects_into_tasks(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user