Plan: canopy-web — Wire Existing canopy-persons Endpoints for Income and Names

On this page

Status

Step Description Status

1

Correct the stale comment at case_detail.rs:800-802 claiming "canopy-persons only has POST /persons/{id}/income — no GET/list endpoint". GET list exists; see services/canopy-persons/src/api/mod.rs:444-463.

Done (2026-04-20)

2

Extend PersonsClient (in services/canopy-web/src/clients.rs) with list_income_for_person(person_id) — thin wrapper over GET /v1/persons/{person_id}/income

Done (2026-04-20)

3

Rewrite render_income_tab at services/canopy-web/src/api/case_detail.rs:794 to fetch self-reported income per household member via Step 2, keep IEVS discrepancies from canopy-snap, and collapse them into a per-person view grouped by income source.

Done (2026-04-20)

4

Program-specific display: extend the income view-model with a program: Program field and apply program-specific presentation rules per Design (e.g., SNAP shows gross+deductions breakdown; TANF shows MAGI-style breakdown; Medicaid shows MAGI only).

Done (2026-04-20)

5

Replace UUID-truncation fallbacks with real name resolution. Any template variable that currently renders Person {uuid_prefix} should call PersonsClient::get_person(person_id) and render first_name + last_name. Audit sites: case_detail.rs:1375 (WIC), any Medicaid COA rendering, any member-display in the household header.

Done (2026-04-20)

6

Integration tests (canopy-web): seed persons with known names + income records; render case detail; assert name + income display. Use the existing canopy-web/tests/session_test.rs harness as a template.

Done (2026-04-20)

7

E2E tests (Playwright): extend applicable specs to assert names render (not UUIDs) and income values are program-appropriate. Cover SNAP, TANF, Medicaid, CAPS, WIC case detail views.

Done (2026-04-20)

8

Plan sync: resolve the Tier 5.5 rows for "Medicaid person names" and "program-specific income display" in roadmap.adoc. Update worker-portal-expansion.adoc status.

Done (2026-04-20)

Branch: feature/canopy-web-persons-wiring
Labels: type::feature, priority::medium, program::cross-program, service::web, service::persons, workflow::ready

Context

The worker-portal-expansion plan and Tier 5.5 tracker both list "program-specific income display" and "Medicaid person names show truncated UUIDs" as blocked-on-upstream-endpoints. Per the 2026-04-19 audit of the Tier 2A prerequisites (see sibling prereq plans), that claim is stale:

  • GET /v1/persons/{id} exists at services/canopy-persons/src/api/mod.rs and returns a Person struct that includes first_name, last_name, date_of_birth, and disability_status (services/canopy-persons/src/store/models.rs:44-103).

  • GET /v1/persons/{id}/income exists at services/canopy-persons/src/api/mod.rs:444-463 and returns the person’s income rows.

The blocker is canopy-web not calling them. The comment at case_detail.rs:800-802 reflects a prior state and was never updated. This plan fixes the comment, adds the client wiring, and removes the UUID-fallback display.

Because both endpoints already exist and are require_caseworker_or_above protected, this plan doesn’t touch canopy-persons or any other upstream service. It’s purely a canopy-web consumer-side fix.

Scope

In scope:

  • canopy-web PersonsClient extension.

  • Rewrite of render_income_tab and any UUID-truncation call sites.

  • Program-specific income presentation rules (see Design).

  • Integration + E2E tests.

  • Stale-comment / TODO cleanup.

Out of scope:

  • New endpoints on canopy-persons (not needed).

  • Household-level income aggregation (would require a new endpoint; not needed for the Tier 5.5 display work — per-person is sufficient).

  • Income-edit UI (read-only for now; editing is tracked under applicant-portal work post-UAT).

  • canopy-portal (applicant) income display — post-UAT per ADR-008.

Dependencies

  • services/canopy-persons/src/api/mod.rs:444-463GET /income endpoint.

  • services/canopy-persons/src/api/mod.rs:336 or nearby — GET /persons/{id} endpoint.

  • services/canopy-persons/src/store/models.rs:44-103Person struct.

  • services/canopy-persons/src/store/models.rs:201-217Income struct.

  • services/canopy-web/src/clients.rsPersonsClient (extend; do not replace).

  • services/canopy-web/src/api/case_detail.rs — rendering functions to rewire.

  • services/canopy-web/templates/cases/tab_income.html — existing template (may need minor adjustments for program-specific layout).

Design

PersonsClient additions

// services/canopy-web/src/clients.rs

impl PersonsClient {
    pub async fn get_person(&self, person_id: Uuid) -> Result<Person, ClientError> {
        self.get(&format!("/v1/persons/{person_id}")).await
    }

    pub async fn list_income_for_person(&self, person_id: Uuid)
        -> Result<Vec<Income>, ClientError>
    {
        self.get(&format!("/v1/persons/{person_id}/income")).await
    }
}

Program-specific income presentation

The income tab should adapt its columns + totals to the viewing program’s eligibility-rules mental model:

Program Display rules

SNAP

Group by person, show gross earned / gross unearned / total, deductions (dependent care, medical, shelter), net income. Matches PAMMS 3205.

TANF

Group by person, show countable earned (after 90% disregard) / countable unearned / total countable. Matches PAMMS 1605/1611.

Medicaid (MAGI)

Show MAGI components: wages, SE income, SS, pension, interest/dividends, capital gains. Medicaid uses Modified Adjusted Gross Income per 42 CFR 435.603.

CAPS

Show gross earned / unearned / total. CAPS eligibility uses simpler total-income test against 85% SMI.

WIC

Show gross income test (185% FPL). Adjunctive eligibility supersedes if SNAP/Medicaid/TANF active — display that status alongside.

A single Askama template branching on program is acceptable; if complexity grows, split into tab_income_snap.html, tab_income_tanf.html, etc. Start with one template and the match/case; refactor if it gets unwieldy.

Name resolution pattern

Current fallback (e.g., case_detail.rs:1375):

format!("Person {}", &person_id[..8.min(person_id.len())])

Replacement:

async fn resolve_name(client: &PersonsClient, person_id: Uuid) -> String {
    match client.get_person(person_id).await {
        Ok(p) => format!("{} {}", p.first_name, p.last_name),
        Err(ClientError::NotFound) => format!("Person {person_id:.8}"),
        Err(_) => "(name unavailable)".into(),
    }
}
  • 404 → keep the UUID-prefix fallback (the person was deleted but we still need something to render).

  • Other errors → generic placeholder. Don’t panic; don’t block rendering.

For cases with N members rendered on a single page, batch the lookups via futures::future::join_all to avoid N sequential round trips.

Caching / N+1 mitigation

Each case-detail page may reference 2–6 household members. Individual GET /persons/{id} calls are tolerable — canopy-web isn’t a hot path. If profiling shows otherwise, introduce a short-TTL in-memory cache (seconds-scoped to the request). Deferred until measured.

Steps

Step 1: Stale-comment cleanup

Files: services/canopy-web/src/api/case_detail.rs:800-802.

Delete the 3-line comment. The new implementation documents what it does.

Step 2: PersonsClient extension

Files: services/canopy-web/src/clients.rs.

Two methods per Design. Unit test with mockito or the existing client-test harness.

Step 3: Income tab rewrite

Files: services/canopy-web/src/api/case_detail.rs:794.

Fetch per-person income, merge with existing IEVS discrepancy data, render by person.

Step 4: Program-specific presentation

Files: services/canopy-web/templates/cases/tab_income.html, possibly split template files.

Step 5: Name resolution

Files: any case_detail.rs site that truncates a UUID for display, associated templates.

Audit via grep:

grep -nE "Person \{.*\[\.\.8" services/canopy-web/src
grep -nE "uuid_prefix|person_id_prefix|person_id\[..8" services/canopy-web/src

Step 6: Integration tests

Files: services/canopy-web/tests/session_test.rs extension or new tests/case_detail_income_test.rs.

Seed 2 persons + income records; render case detail; assert names
income values present.

Step 7: Playwright E2E

Files: extend tests/e2e/specs/applications.spec.ts and add coverage in per-program spec files (likely caps.spec.ts, wic.spec.ts created by sibling Tier 2A plans).

Step 8: Plan sync

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

Files Touched

File Change

services/canopy-web/src/clients.rs

+2 PersonsClient methods

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

Rewire income + name resolution

services/canopy-web/templates/cases/tab_income.html

Program-specific sections

services/canopy-web/tests/*

New integration tests

tests/e2e/specs/*.spec.ts

Name + income assertions

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

Resolved deferrals

docs/modules/ROOT/pages/roadmap.adoc

2x Tier 5.5 rows → Done

CHANGELOG.adoc

Unreleased entry

Verification

  1. cargo nextest run -p canopy-web — integration tests pass.

  2. Manual: navigate to SNAP case detail, Income tab renders per-member income + names.

  3. Manual: navigate to Medicaid case detail, Members section renders real names (not UUIDs).

  4. cargo xtask e2e — Playwright green including new name/income assertions.

  5. cargo xtask validate — full battery green.

  6. grep -nE "Person \{.*\[\.\.8" services/canopy-web/src — zero matches (no remaining UUID-truncation fallbacks except the explicit 404 path in resolve_name).

Documentation Updates

  • canopy-persons API reference — note that worker portal now consumes GET /income (no new endpoint, but document the consumer)

  • CHANGELOG.adoc== Unreleased entry

Potential Improvements

  • Household-scoped income endpoint on canopy-persons — would reduce the N+1 round-trips when a household has many members. Defer until profiling shows it matters.

  • Income editing from the worker portal — read-only today; edit UI is worker-portal polish beyond Tier 2A.

  • Historical income versioning — the current Income struct has effective_date and end_date but no superseded_by pointer. A separate audit-trail plan if required for Pub 1075 or QC.


Tracked follow-ups (filed 2026-04-24 after audit of plan Errata + Potential Improvements sections across the repo):

  • #318 — Household-scoped income endpoint — N+1 reduction (from Potential Improvements)

Tracked follow-ups (filed 2026-05-04 during PI sweep):

  • #409 — Per-member income editing UI for caseworkers

  • #410 — Historical income versioning with superseded_by pointer

Errata

2026-04-20 — hardcoded federal percentages in display copy (caught pre-commit)

Initial draft of Program::income_rule_note() included hardcoded jurisdiction-specific percentages in the header text ("90% earned-income disregard" for TANF, "50% / 85% SMI" for CAPS, "185% FPL" for WIC). Per ADR-011 even display copy should avoid baking numeric thresholds into Rust source — values belong in jurisdiction.toml with citations, and display strings should either omit the number or fetch it live from a /v1/params endpoint.

Resolved in-branch: stripped the percentages; kept only regulatory citations (PAMMS 3205, PAMMS 1605 / 1611, 42 CFR 435.603, 45 CFR 98.20, 7 CFR 246.7). Added a guard unit test (income_rule_notes_contain_no_hardcoded_percentages) that scans every program’s rule note for % patterns to prevent future drift.

2026-04-20 — broader hardcoded-policy-values audit triggered

This incident prompted a codebase-wide audit (hardcoded-policy-values-audit-2026-04-20) covering SNAP / TANF / Medicaid / CAPS / WIC and shared crates. ~90 distinct findings across the codebase; follow-up plans will address them individually. The Tier 7 backlog gained 10+ items as a result.

Edit this page · default