Plan: canopy-wic List Endpoints for Determinations and Nutritional-Risk Assessments

On this page

Status

Step Description Status

1

Store: list_determinations_for_household(db, household_id) — returns Vec<WicDetermination> ordered by determined_at DESC

Done (2026-04-21) — list_determinations_by_household added to services/canopy-wic/src/store/determinations.rs, ORDER BY created_at DESC LIMIT 100.

2

Store: list_nutritional_risk_assessments_for_person(db, person_id) — returns Vec<WicNutritionalRiskAssessment> ordered by assessment_date DESC

Done (2026-04-21) — list_assessments_by_person added to services/canopy-wic/src/store/assessments.rs, ORDER BY assessment_date DESC LIMIT 100. Pre-existing get_assessment also unwrapped from #[allow(dead_code)] to support Step 5.

3

API: GET /v1/determinations?household_id=X — lists WIC determinations. RBAC: require_caseworker_or_above. utoipa-annotated.

Done (2026-04-21) — handler with HouseholdScopedQuery extractor.

4

API: GET /v1/nutritional-risk-assessments?person_id=X — lists assessments. RBAC + utoipa.

Done (2026-04-21) — handler with PersonScopedQuery extractor. Routed via .get(…​) on the same path as the pre-existing POST using axum’s MethodRouter chaining.

5

API: GET /v1/nutritional-risk-assessments/{id} — fetch one by ID (completes CRUD surface). Same RBAC + utoipa.

Done (2026-04-21) — path-param handler.

6

canopy-web wiring: render_wic_nutrition at services/canopy-web/src/api/case_detail.rs:1480 stops returning an empty Vec; calls the new list endpoint through WicClient::list_nutritional_risk_assessments_for_person and renders rows.

Done (2026-04-21) — render_wic_nutrition signature changed from &InternalClient to &ServiceClients so it can hit canopy-persons for the member list. Fetches /v1/households/{id} → iterates members[].person_id → for each calls /v1/nutritional-risk-assessments?person_id=X on canopy-wic → maps rows into NutritionalRiskAssessment view-model. render_wic_determination also switched from ?limit=50 client-side filtering to the new household-scoped endpoint (parity with CAPS). No new WicClient helper needed — the generic InternalClient.get pattern is used throughout canopy-web.

7

Integration tests (canopy-wic): seed a determination + 2 assessments, GET both list endpoints, assert shapes.

Done (2026-04-21) — 4 new tests: wic_list_determinations_for_household (2 determinations in same household, asserts filter + count), wic_list_assessments_for_person (2 assessments with different dates, asserts DESC ordering), wic_get_assessment_by_id (POST → GET round-trip), wic_list_endpoints_empty (random IDs return []). 18/18 canopy-wic tests pass.

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: tests/e2e/specs/wic.spec.ts asserts the seeded assessment_date row against a WIC-seeded household with participants + nutritional-risk assessments.

9

Plan sync: mark the WIC nutritional-risk deferral resolved in worker-portal-expansion.adoc and the Tier 5.5 row in roadmap.adoc.

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_codes is a TEXT[] 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.sqlwic_nutritional_risk_assessments already exists.

  • services/canopy-wic/src/store/mod.rs — add list helpers next to the existing has_assessment / get_latest_assessment.

  • services/canopy-wic/src/api/handlers.rs — existing POST handler module.

  • services/canopy-web/src/clients.rs — add WicClient::list_nutritional_risk_assessments_for_person.

  • services/canopy-web/templates/cases/tab_nutrition.html — existing template with expected variables (already structured correctly; see NutritionalRiskAssessment view-model at case_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 1 & 2: Store helpers

Files: services/canopy-wic/src/store/mod.rs.

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.

Step 8: Playwright E2E

Files: tests/e2e/specs/wic.spec.ts (new).

Caseworker logs in, opens a WIC case, clicks Nutritional Risk tab, sees a row with the seeded risk flags.

Step 9: Plan sync

Files: worker-portal-expansion.adoc, roadmap.adoc, CHANGELOG.adoc.

Files Touched

File Change

services/canopy-wic/src/store/mod.rs

+2 list helpers

services/canopy-wic/src/api/handlers.rs

+3 handlers

services/canopy-wic/src/api/mod.rs

Route registration

services/canopy-web/src/clients.rs

+WicClient::list_nutritional_risk_assessments_for_person

services/canopy-web/src/api/case_detail.rs

Rewire render_wic_nutrition

services/canopy-wic/tests/*

New integration tests

tests/e2e/specs/wic.spec.ts

New Playwright coverage

docs/modules/ROOT/pages/plans/worker-portal-expansion.adoc

Resolved deferral

docs/modules/ROOT/pages/roadmap.adoc

Tier 5.5 row → Done

CHANGELOG.adoc

Unreleased entry

Verification

  1. cargo nextest run -p canopy-wic — new tests pass.

  2. cargo nextest run -p canopy-web — case-detail tests still green.

  3. Seed a WIC assessment, curl $WIC_URL/v1/nutritional-risk-assessments?person_id=X — shape matches utoipa schema.

  4. cargo xtask e2e --grep "wic" — Playwright WIC spec passes.

  5. Manual: navigate to seeded WIC case in the worker portal, Nutritional Risk tab renders real rows.

  6. cargo xtask validate — full battery green.

Documentation Updates

  • CHANGELOG.adoc== Unreleased entry

  • 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.

Edit this page · default