Plan: canopy-wic List Endpoints for Determinations and Nutritional-Risk Assessments
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Store: |
Done (2026-04-21) — |
2 |
Store: |
Done (2026-04-21) — |
3 |
API: |
Done (2026-04-21) — handler with |
4 |
API: |
Done (2026-04-21) — handler with |
5 |
API: |
Done (2026-04-21) — path-param handler. |
6 |
canopy-web wiring: |
Done (2026-04-21) — |
7 |
Integration tests (canopy-wic): seed a determination + 2 assessments, GET both list endpoints, assert shapes. |
Done (2026-04-21) — 4 new tests: |
8 |
E2E test (canopy-web, Playwright): navigate to a seeded WIC case, click Nutritional Risk tab, assert an assessment row renders. |
Done (2026-04-21) — Resolved by canopy-seed-caps-wic-fixtures: |
9 |
Plan sync: mark the WIC nutritional-risk deferral resolved in |
Done (2026-04-21) |
Branch: feature/canopy-wic-list-endpoints
Labels: type::feature, priority::medium, program::wic, service::wic, service::web, federal-partner::fns, workflow::ready
Context
The WIC nutritional-risk tab in canopy-web renders empty. render_wic_nutrition
at services/canopy-web/src/api/case_detail.rs:1480 returns
TabNutritionTemplate { assessments: Vec::new() } with the comment
"The endpoint is POST-only for creating; no GET list endpoint exists yet."
The underlying data exists. wic_nutritional_risk_assessments table is
populated by POST /v1/nutritional-risk-assessments
(services/canopy-wic/src/api/handlers.rs:113). The store layer already
has has_assessment and get_latest_assessment helpers — they just
aren’t exposed via HTTP.
Two gaps to close:
-
Determinations list — today only
GET /v1/determinations/{id}exists. Worker portal needs household-scoped lookup to show WIC case status. -
Assessments list — POST exists to create, no GET to read back. The worker portal can’t render nutritional-risk history or current status without it.
The WIC eligibility service owns both resources per ADR-001. Both endpoints are low-risk read-side additions.
Scope
In scope:
-
Three endpoints:
GET /determinations?household_id,GET /nutritional-risk-assessments?person_id,GET /nutritional-risk-assessments/{id}. -
Store helpers for the two list queries.
-
canopy-web wiring for the assessment list.
-
Integration + E2E tests.
Out of scope:
-
Creating assessments from the worker portal UI. The POST endpoint exists; wiring it to a UI is a follow-up (likely a worker-portal enhancement after this prereq lands).
-
Nutritional-risk code reference data (the CPA-sanctioned list). Today
risk_codesis aTEXT[]free-form field — acceptable for Phase A, may tighten to an enum later. -
Federal reporting integration (WIC PC reporting uses nutritional-risk codes; that’s a separate plan under Tier 4).
Dependencies
-
services/canopy-wic/migrations/20260413000000_create_wic_tables.sql—wic_nutritional_risk_assessmentsalready exists. -
services/canopy-wic/src/store/mod.rs— add list helpers next to the existinghas_assessment/get_latest_assessment. -
services/canopy-wic/src/api/handlers.rs— existing POST handler module. -
services/canopy-web/src/clients.rs— addWicClient::list_nutritional_risk_assessments_for_person. -
services/canopy-web/templates/cases/tab_nutrition.html— existing template with expected variables (already structured correctly; seeNutritionalRiskAssessmentview-model atcase_detail.rs:339).
Design
Store helpers
// services/canopy-wic/src/store/mod.rs
pub async fn list_determinations_for_household(
db: &PgPool,
household_id: HouseholdId,
) -> sqlx::Result<Vec<WicDetermination>> {
sqlx::query_as::<_, WicDetermination>(
r#"SELECT * FROM wic_determinations
WHERE household_id = $1
ORDER BY determined_at DESC"#,
)
.bind(household_id)
.fetch_all(db)
.await
}
pub async fn list_nutritional_risk_assessments_for_person(
db: &PgPool,
person_id: PersonId,
) -> sqlx::Result<Vec<WicNutritionalRiskAssessment>> {
sqlx::query_as::<_, WicNutritionalRiskAssessment>(
r#"SELECT * FROM wic_nutritional_risk_assessments
WHERE person_id = $1
ORDER BY assessment_date DESC"#,
)
.bind(person_id)
.fetch_all(db)
.await
}
Endpoint responses
GET /v1/determinations?household_id=X returns Vec<WicDetermination>
with the existing struct shape.
GET /v1/nutritional-risk-assessments?person_id=X returns
Vec<WicNutritionalRiskAssessment>. The struct already matches what
case_detail.rs:339’s view-model needs (`anthropometric_risk,
biochemical_risk, dietary_risk, medical_risk, risk_codes,
assessment_date) — a straightforward mapping.
canopy-web view
render_wic_nutrition replacement (pseudocode):
async fn render_wic_nutrition(
client: &WicClient,
household: &Household,
) -> Result<TabNutritionTemplate, CaseDetailError> {
let mut views = Vec::new();
for member in &household.members {
let raw = client.list_nutritional_risk_assessments_for_person(member.person_id).await?;
views.extend(raw.into_iter().map(|a| NutritionalRiskAssessment {
assessed_date: a.assessment_date.format("%Y-%m-%d").to_string(),
anthropometric: a.anthropometric_risk,
biochemical: a.biochemical_risk,
dietary: a.dietary_risk,
medical: a.medical_risk,
risk_codes: a.risk_codes.join(", "),
}));
}
Ok(TabNutritionTemplate { assessments: views })
}
Member iteration: the tab currently shows household-level nutritional risk. Individual WIC participants (pregnant women, infants, children ≤5) each have their own assessments. The tab renders one row per assessment across all members — that’s what the Askama template is shaped for.
Steps
Step 3, 4, 5: API endpoints
Files: services/canopy-wic/src/api/handlers.rs,
services/canopy-wic/src/api/mod.rs.
Three handlers with standard canopy-wic patterns (see existing
POST /v1/nutritional-risk-assessments for reference).
Step 6: canopy-web wiring
Files: services/canopy-web/src/clients.rs,
services/canopy-web/src/api/case_detail.rs.
Extend WicClient, rewrite render_wic_nutrition per Design.
Step 7: canopy-wic integration tests
Files: services/canopy-wic/tests/ (new file or extend existing).
Seed one determination, two assessments for one person + one assessment for another; GET each endpoint; assert filtering and ordering.
Files Touched
| File | Change |
|---|---|
|
+2 list helpers |
|
+3 handlers |
|
Route registration |
|
+WicClient::list_nutritional_risk_assessments_for_person |
|
Rewire render_wic_nutrition |
|
New integration tests |
|
New Playwright coverage |
|
Resolved deferral |
|
Tier 5.5 row → Done |
|
Unreleased entry |
Verification
-
cargo nextest run -p canopy-wic— new tests pass. -
cargo nextest run -p canopy-web— case-detail tests still green. -
Seed a WIC assessment,
curl $WIC_URL/v1/nutritional-risk-assessments?person_id=X— shape matches utoipa schema. -
cargo xtask e2e --grep "wic"— Playwright WIC spec passes. -
Manual: navigate to seeded WIC case in the worker portal, Nutritional Risk tab renders real rows.
-
cargo xtask validate— full battery green.
Documentation Updates
-
CHANGELOG.adoc—== Unreleasedentry -
canopy-wic API reference — new routes (deferred to follow-up doc pass)
-
.claude/docs/services.md— canopy-wic endpoint list (deferred to follow-up doc pass)
Errata
2026-04-21 — Step 8 Playwright E2E deferred (no WIC seed data)
Same pattern as the sibling canopy-caps-list-endpoints plan. tests/e2e/lib/seed.ts’s `findApproved() walks SNAP determinations only; tools/canopy-seed does not seed a WIC case, participant, or nutritional-risk assessment. Full WIC-case E2E coverage depends on extending canopy-seed with a pregnant-woman or infant fixture + assessment — cross-cutting and larger than this prereq plan.
The Step 7 integration tests cover the HTTP contract end-to-end with a real DB. The narrower "click Nutritional Risk tab, see a risk row" assertion is deferred to a follow-up plan that also extends canopy-seed.
Resolved 2026-04-21 by canopy-seed-caps-wic-fixtures — canopy-seed now emits WIC determinations + participants + nutritional-risk assessments, and tests/e2e/specs/wic.spec.ts asserts the nutritional-risk tab renders the seeded assessment_date row.
2026-04-21 — Step 6 signature change: render_wic_nutrition takes &ServiceClients
Plan pseudocode showed render_wic_nutrition(client: &WicClient, household: &Household). Implementation discovered the handler needs the household’s member list (WIC participants are individual persons, not the household) — which lives in canopy-persons, not canopy-wic. Changed the signature to &ServiceClients so the function can reach both clients.persons (members) and clients.wic (assessments). Dispatcher call site at case_detail.rs:1258 updated accordingly. No WicClient helper type added — the generic InternalClient.get pattern is the standing convention in canopy-web.