Skip to content

Add event subscriptions and missing published events to intake API contract #236

@mryhmln

Description

@mryhmln

Summary

Intake depends on coordination across several systems: claiming a task should open the application for review, submitting an application should generate the required document checklist and kick off verification, and results from verification services, appointment scheduling, and eligibility determinations should flow back to the application automatically. Without this wiring, caseworkers have to update application records by hand after each external action — re-entering data the system already has.

This issue implements the event subscriptions that drive that automation. Rule sets react to events from workflow, data exchange, scheduling, and document management and update application state without caseworker intervention. It also adds POST /platform/events to the mock server so forward subscriptions (events from domains not yet built) can be injected and exercised end-to-end.

It also adds API paths for ApplicationDocument and Interview — entities created by the rules engine but previously unqueryable — and updates the path structure to use sub-resource paths where appropriate, with explicit conventions added to api-patterns.yaml.

This is the implementation issue for the event contracts decisions in #232.

Use cases

  • As a caseworker, I want the system to automatically open my review when I claim a task, so I don't have to open the application manually in a separate step
  • As a caseworker, I want document checklists to be automatically created when an application is submitted, so I don't have to manually request income, identity, and residency documents for each applicant
  • As a caseworker, I want the application to reflect the results of identity, income, and citizenship verification automatically, so I don't have to re-enter information that was already checked electronically
  • As a caseworker, I want eligibility determination results to be recorded on the application automatically, so I can see the outcome of each program check without switching systems
  • As a supervisor, I want interview completion and document verification to be recorded on the application automatically, so caseworkers don't have to update the record manually after each step

Scope

Event subscriptions and rule sets:

  • packages/contracts/intake-rules.yaml — add rule sets for each subscription (declared via on: in the rules file)
  • packages/contracts/schemas/rules-schema.yaml — add all-match to evaluation enum; allow from: to accept JSON Logic {var: "..."} form
  • packages/mock-server/scripts/server.js — implement POST /platform/events wired to the event bus so externally injected events fire subscriptions

Application review trigger:

  • packages/contracts/intake-openapi.yaml — add application.review_completed to x-events with payload schema
  • packages/contracts/intake-state-machine.yaml — add complete-review trigger (from: under_review, no to: — stays in current state, emits application.review_completed per Decision 6)

Sub-resource paths for ApplicationDocument and Interview:

  • packages/contracts/intake-openapi.yaml — add /applications/{applicationId}/documents (collection + item) and /applications/{applicationId}/interview (singleton); transitions come from state machine contracts via overlay
  • packages/mock-server/src/route-generator.js — update deriveCollectionName and endpoint type detection to handle nested sub-resource paths; add singleton sub-resource handling for Interview
  • docs/architecture/domains/intake.md — update Decision 20 to reflect sub-resource paths and singleton Interview pattern

API pattern conventions:

  • packages/contracts/patterns/api-patterns.yaml — add explicit patterns for sub-resource paths (noun segments, owned child entities with lifecycle) and RPC transition endpoints (verb segments); clarify the noun/verb distinction

Subscriptions (declared in intake-rules.yaml via on:):

Event Source Effect Status
workflow.task.claimed Workflow Transition application submittedunder_review; create Interview record Active
intake.application.submitted Intake Create document checklist per applicant programs (Decision 16) Active
data-exchange.service-call.completed Data Exchange Write back FDSH/IEVS/SAVE/SSA results; create citizenship doc if FDSH inconclusive Forward
scheduling.appointment.scheduled Scheduling Append appointment ID to Interview.appointments (Decision 15) Forward
document-management.document.verified Document Management Transition ApplicationDocument to verified Forward
eligibility.determination.completed Eligibility Write determination outcome to ApplicationMember (informational write-back only) Forward

How to validate

Schema validation:

npm run validate

Expected: ✓ All schema validations passed!

workflow.task.claimed — start the mock server (npm run mock:start), then:

APP_ID=$(curl -s -X POST http://localhost:1080/intake/applications \
  -H "Content-Type: application/json" \
  -H "X-Caller-Id: user-1" -H "X-Caller-Roles: applicant" \
  -d '{"programs": ["snap"], "channel": "online"}' | jq -r .id)

curl -s -X POST "http://localhost:1080/intake/applications/$APP_ID/submit" \
  -H "X-Caller-Id: user-1" -H "X-Caller-Roles: applicant"

TASK_ID=$(curl -s "http://localhost:1080/workflow/tasks?subjectId=$APP_ID" \
  -H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker" | jq -r '.items[0].id')

curl -s -X POST "http://localhost:1080/workflow/tasks/$TASK_ID/claim" \
  -H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker"

curl -s "http://localhost:1080/intake/applications/$APP_ID" \
  -H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker" | jq .status
# → "under_review"

Document checklist on submit — using the APP_ID from above:

curl -s "http://localhost:1080/intake/applications/$APP_ID/documents" \
  -H "X-Caller-Roles: caseworker" | jq '[.items[].category]'
# → ["income", "identity", "residency"] (SNAP applicant — three documents requested on submit)

complete-review trigger:

curl -s -X POST "http://localhost:1080/intake/applications/$APP_ID/complete-review" \
  -H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker" | jq .status
# → "under_review"

Forward subscriptions via POST /platform/events — use the IDs from the steps above:

DOC_ID=$(curl -s "http://localhost:1080/intake/applications/$APP_ID/documents" \
  -H "X-Caller-Roles: caseworker" | jq -r '.items[0].id')
INTERVIEW_ID=$(curl -s "http://localhost:1080/intake/applications/$APP_ID/interview" \
  -H "X-Caller-Roles: caseworker" | jq -r .id)

# document-management.document.verified → transitions ApplicationDocument to verified, populates evidence
curl -s "http://localhost:1080/intake/applications/$APP_ID/documents/$DOC_ID" \
  -H "X-Caller-Roles: caseworker" | jq .status
# → "requested"

curl -s -X POST http://localhost:1080/platform/events \
  -H "Content-Type: application/json" \
  -d "{\"specversion\":\"1.0\",\"type\":\"org.codeforamerica.safety-net-blueprint.document-management.document.verified\",\"source\":\"/document-management\",\"id\":\"evt-doc\",\"subject\":\"dm-doc-abc123\",\"data\":{\"subjectType\":\"application-document\",\"subjectId\":\"$DOC_ID\"}}" | jq .id

curl -s "http://localhost:1080/intake/applications/$APP_ID/documents/$DOC_ID" \
  -H "X-Caller-Roles: caseworker" | jq '{status, evidence}'
# → {"status": "verified", "evidence": ["dm-doc-abc123"]}
# subject in the CloudEvents envelope is stored in evidence; subjectId in data is used for lookup

# data-exchange.service-call.completed with fdsh+inconclusive → creates citizenship ApplicationDocument
curl -s -X POST http://localhost:1080/platform/events \
  -H "Content-Type: application/json" \
  -d "{\"specversion\":\"1.0\",\"type\":\"org.codeforamerica.safety-net-blueprint.data-exchange.service-call.completed\",\"source\":\"/data-exchange\",\"id\":\"evt-fdsh\",\"subject\":\"svc-1\",\"data\":{\"applicationId\":\"$APP_ID\",\"serviceType\":\"fdsh\",\"result\":\"inconclusive\",\"verificationType\":\"citizenship\"}}" | jq .id

curl -s "http://localhost:1080/intake/applications/$APP_ID/documents" \
  -H "X-Caller-Roles: caseworker" | jq '[.items[].category]'
# → includes "citizenship"

# scheduling.appointment.scheduled → appends appointment ID to Interview.appointments
curl -s -X POST http://localhost:1080/platform/events \
  -H "Content-Type: application/json" \
  -d "{\"specversion\":\"1.0\",\"type\":\"org.codeforamerica.safety-net-blueprint.scheduling.appointment.scheduled\",\"source\":\"/scheduling\",\"id\":\"evt-appt\",\"subject\":\"appt-abc123\",\"data\":{\"subjectType\":\"interview\",\"subjectId\":\"$INTERVIEW_ID\"}}" | jq .id

curl -s "http://localhost:1080/intake/applications/$APP_ID/interview" \
  -H "X-Caller-Roles: caseworker" | jq .appointments
# → ["appt-abc123"]

# eligibility.determination.completed → informational write-back only (app stays under_review)
curl -s -X POST http://localhost:1080/platform/events \
  -H "Content-Type: application/json" \
  -d "{\"specversion\":\"1.0\",\"type\":\"org.codeforamerica.safety-net-blueprint.eligibility.determination.completed\",\"source\":\"/eligibility\",\"id\":\"evt-elig\",\"subject\":\"$APP_ID\",\"data\":{\"applicationId\":\"$APP_ID\",\"program\":\"snap\",\"outcome\":\"approved\",\"determinedAt\":\"2026-04-16T00:00:00Z\"}}" | jq .id

curl -s "http://localhost:1080/intake/applications/$APP_ID" \
  -H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker" | jq .status
# → "under_review" (application does not close automatically; close is a manual caseworker action)

Dependencies

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestintakeIntake domain — application submission, eligibility screening, and household data

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions