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 |
Done (2026-04-20) |
2 |
Extend |
Done (2026-04-20) |
3 |
Rewrite |
Done (2026-04-20) |
4 |
Program-specific display: extend the income view-model with a |
Done (2026-04-20) |
5 |
Replace UUID-truncation fallbacks with real name resolution. Any template variable that currently renders |
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 |
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 |
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 atservices/canopy-persons/src/api/mod.rsand returns aPersonstruct that includesfirst_name,last_name,date_of_birth, anddisability_status(services/canopy-persons/src/store/models.rs:44-103). -
GET /v1/persons/{id}/incomeexists atservices/canopy-persons/src/api/mod.rs:444-463and 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
PersonsClientextension. -
Rewrite of
render_income_taband 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-463—GET /incomeendpoint. -
services/canopy-persons/src/api/mod.rs:336or nearby —GET /persons/{id}endpoint. -
services/canopy-persons/src/store/models.rs:44-103—Personstruct. -
services/canopy-persons/src/store/models.rs:201-217—Incomestruct. -
services/canopy-web/src/clients.rs—PersonsClient(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.
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.
Files Touched
| File | Change |
|---|---|
|
+2 PersonsClient methods |
|
Rewire income + name resolution |
|
Program-specific sections |
|
New integration tests |
|
Name + income assertions |
|
Resolved deferrals |
|
2x Tier 5.5 rows → Done |
|
Unreleased entry |
Verification
-
cargo nextest run -p canopy-web— integration tests pass. -
Manual: navigate to SNAP case detail, Income tab renders per-member income + names.
-
Manual: navigate to Medicaid case detail, Members section renders real names (not UUIDs).
-
cargo xtask e2e— Playwright green including new name/income assertions. -
cargo xtask validate— full battery green. -
grep -nE "Person \{.*\[\.\.8" services/canopy-web/src— zero matches (no remaining UUID-truncation fallbacks except the explicit 404 path inresolve_name).
Documentation Updates
-
canopy-persons API reference — note that worker portal now consumes
GET /income(no new endpoint, but document the consumer) -
CHANGELOG.adoc—== Unreleasedentry
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
Incomestruct haseffective_dateandend_datebut nosuperseded_bypointer. 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):
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.