Plan: Medicaid COA Phase D — ABD FBR SSA-Linked COAs
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Add 5 ApplicationContext boolean flags for SSA-linked COAs |
Done (2026-04-13) |
2 |
Extend NonMagiInput with 5 boolean fields |
Done (2026-04-13) |
3 |
Extend NonMagiOutput with 5 boolean fields |
Done (2026-04-13) |
4 |
Add 5 expressions to medicaid-non-magi.json |
Done (2026-04-13) |
5 |
Wire eligible_fn and denial_reason_fn match arms |
Done (2026-04-13) |
6 |
Add 5 COAs to hierarchy ruleset |
Done (2026-04-13) |
7 |
Update canopy-eligibility orchestrator to query SSA data and populate flags |
Done (2026-04-13) |
8 |
Unit tests (3 cases) |
Done (2026-04-13) |
Branch: feature/medicaid-coa-phase-d
Context
The ABD FBR (Federal Benefit Rate) SSA-linked COAs cover individuals who lost SSI eligibility due to specific Social Security Administration actions but retain Medicaid eligibility through federal safety-net provisions. These five COAs are:
-
Pickle (PAMMS 2120): Lost SSI due to Social Security COLA increases. Named after Congressman Jake Pickle who sponsored the amendment (42 USC 1396a(a)(10)(A)(i)(II)).
-
DAC (PAMMS 2122): Disabled Adult Child who lost SSI upon receipt of DAC benefits.
-
Disabled Widow (PAMMS 2124): Disabled widow(er) aged 50-64 who lost SSI upon receipt of widow’s benefits.
-
Widow 60-64 (PAMMS 2126): Non-disabled widow(er) aged 60-64 who lost SSI upon receipt of widow’s benefits.
-
Former SSI Disabled Child (PAMMS 2128): Child who lost SSI due to the Zebley redetermination or age-18 redetermination.
All five COAs are simple boolean gates — the core eligibility question is whether SSA data confirms the specific loss-of-SSI scenario. The actual verification comes from SSA SOLQ/BINDEX data available through canopy-verification’s SsaSdxRecord and SsaBendexRecord structs.
The MedicaidCategory enum already contains all five variants (Pickle, Dac, DisabledWidow, Widow6064, FormerSsiDisabledChild) in the ABD FBR block. The CMD cascade evaluates them but they fall through to _ ⇒ false in eligible_fn.
Scope
In scope:
-
5 boolean flags on ApplicationContext
-
NonMagiInput / NonMagiOutput extensions (5 fields each)
-
medicaid-non-magi.json boolean gate expressions (5 expressions)
-
eligible_fn and denial_reason_fn wiring (5 match arms each)
-
Hierarchy ruleset additions (5 COAs between ssi_medicaid and institutional block)
-
Orchestrator data flow: canopy-eligibility queries canopy-verification SSA endpoints
-
3 unit tests
Out of scope:
-
SSA SOLQ/BINDEX adapter implementation (canopy-verification has NoopSsaAdapter with deterministic test data — real SSA integration is a separate compliance plan)
-
SSI benefit amount recalculation (these COAs only require the boolean flag, not the dollar amounts)
-
Pickle/DAC/etc. benefit package differences (all receive full Medicaid — benefit package is identical)
Dependencies
-
services/canopy-verification/src/ssa.rs— existingSsaSdxRecord,SsaBendexRecordstructs -
services/canopy-eligibility/src/orchestrator.rs— must be updated to query SSA data -
services/canopy-medicaid/src/store/models.rs— MedicaidCategory enum (already has all 5 variants)
Design
ApplicationContext additions
Add to services/canopy-medicaid/src/determine.rs, struct ApplicationContext:
/// Lost SSI due to Social Security COLA increase (Pickle Amendment, PAMMS 2120).
#[serde(default)]
pub lost_ssi_due_to_cola: Option<bool>,
/// Disabled Adult Child receiving DAC benefits (PAMMS 2122).
#[serde(default)]
pub is_disabled_adult_child: Option<bool>,
/// Disabled widow(er) 50-64 receiving widow's benefits (PAMMS 2124).
#[serde(default)]
pub is_disabled_widow: Option<bool>,
/// Non-disabled widow(er) 60-64 receiving widow's benefits (PAMMS 2126).
#[serde(default)]
pub is_widow_60_64: Option<bool>,
/// Former SSI disabled child (Zebley/age-18 redetermination, PAMMS 2128).
#[serde(default)]
pub lost_ssi_as_disabled_child: Option<bool>,
NonMagiInput additions
Add to services/canopy-medicaid/src/rules_client.rs, struct NonMagiInput:
pub lost_ssi_due_to_cola: bool,
pub is_disabled_adult_child: bool,
pub is_disabled_widow: bool,
pub is_widow_60_64: bool,
pub lost_ssi_as_disabled_child: bool,
NonMagiOutput additions
Add to services/canopy-medicaid/src/rules_client.rs, struct NonMagiOutput:
pub pickle_eligible: bool,
pub dac_eligible: bool,
pub disabled_widow_eligible: bool,
pub widow_60_64_eligible: bool,
pub former_ssi_disabled_child_eligible: bool,
medicaid-non-magi.json expressions
5 simple boolean gate expressions:
Pickle (new expression id ex-pickle):
lost_ssi_due_to_cola
DAC (new expression id ex-dac):
is_disabled_adult_child
Disabled Widow (new expression id ex-disabled-widow):
is_disabled_widow
Widow 60-64 (new expression id ex-widow-60-64):
is_widow_60_64
Former SSI Disabled Child (new expression id ex-former-ssi-disabled-child):
lost_ssi_as_disabled_child
eligible_fn additions
MedicaidCategory::Pickle => non_magi_out.pickle_eligible,
MedicaidCategory::Dac => non_magi_out.dac_eligible,
MedicaidCategory::DisabledWidow => non_magi_out.disabled_widow_eligible,
MedicaidCategory::Widow6064 => non_magi_out.widow_60_64_eligible,
MedicaidCategory::FormerSsiDisabledChild => non_magi_out.former_ssi_disabled_child_eligible,
denial_reason_fn additions
MedicaidCategory::Pickle => "no_ssi_loss_due_to_cola",
MedicaidCategory::Dac => "not_disabled_adult_child",
MedicaidCategory::DisabledWidow => "not_disabled_widow_50_64",
MedicaidCategory::Widow6064 => "not_widow_60_64",
MedicaidCategory::FormerSsiDisabledChild => "no_ssi_loss_as_disabled_child",
Hierarchy ruleset additions
Add to rulesets/georgia/medicaid-eligibility-hierarchy.json:
{"id": "ex-has-pickle", "key": "has_pickle", "value": "some(eligible_coas, # == \"pickle\")"},
{"id": "ex-has-dac", "key": "has_dac", "value": "some(eligible_coas, # == \"dac\")"},
{"id": "ex-has-disabled-widow", "key": "has_disabled_widow", "value": "some(eligible_coas, # == \"disabled_widow\")"},
{"id": "ex-has-widow-60-64", "key": "has_widow_60_64", "value": "some(eligible_coas, # == \"widow_60_64\")"},
{"id": "ex-has-former-ssi-dc", "key": "has_former_ssi_dc", "value": "some(eligible_coas, # == \"former_ssi_disabled_child\")"}
These should be placed in the hierarchy after has_ssi_medicaid and before the institutional/waiver block, matching the MedicaidCategory enum order.
Orchestrator data flow
services/canopy-eligibility/src/orchestrator.rs must be updated to:
-
Query
canopy-verificationSSA endpoints for the applicant’s SSA data. -
Map
SsaSdxRecord/SsaBendexRecordfields to the 5 boolean flags. -
Inject the flags into the
MedicaidApplicationContextbefore dispatching to canopy-medicaid.
The mapping logic:
// Pseudo-code — exact field names depend on SsaSdxRecord/SsaBendexRecord schemas
let lost_ssi_due_to_cola = ssa_sdx.map_or(false, |r| r.ssi_terminated && r.termination_reason == "cola_increase");
let is_disabled_adult_child = ssa_bendex.map_or(false, |r| r.benefit_type == "dac" && r.disability_onset.is_some());
let is_disabled_widow = ssa_bendex.map_or(false, |r| {
r.benefit_type == "disabled_widow" && r.age >= 50 && r.age <= 64
});
let is_widow_60_64 = ssa_bendex.map_or(false, |r| {
r.benefit_type == "widow" && r.age >= 60 && r.age <= 64 && r.disability_onset.is_none()
});
let lost_ssi_as_disabled_child = ssa_sdx.map_or(false, |r| {
r.ssi_terminated && (r.termination_reason == "zebley" || r.termination_reason == "age_18_redetermination")
});
SsaSdxRecord/SsaBendexRecord must be verified at implementation time. The NoopSsaAdapter returns deterministic test data that should cover these fields.
Steps
Step 1: ApplicationContext fields
Files: services/canopy-medicaid/src/determine.rs
Add 5 boolean Option fields with #[serde(default)] to ApplicationContext as specified in the Design section. Unwrap with .unwrap_or(false) in the determine function body.
Step 2: NonMagiInput extension
Files: services/canopy-medicaid/src/rules_client.rs
Add 5 boolean fields to NonMagiInput. Wire in determine.rs where NonMagiInput is constructed, pulling from the unwrapped ApplicationContext values.
Step 3: NonMagiOutput extension
Files: services/canopy-medicaid/src/rules_client.rs
Add 5 boolean fields to NonMagiOutput.
Step 4: medicaid-non-magi.json expressions
Files: rulesets/georgia/medicaid-non-magi.json
Add 5 expression nodes with simple boolean gate expressions. Add 5 output keys to the output mapping node.
Step 5: eligible_fn + denial_reason_fn
Files: services/canopy-medicaid/src/determine.rs
Add 5 match arms to eligible_fn mapping each MedicaidCategory variant to its NonMagiOutput boolean. These replace the _ ⇒ false catch-all for these COAs. Add 5 match arms to denial_reason_fn with specific denial reasons.
Step 6: Hierarchy ruleset
Files: rulesets/georgia/medicaid-eligibility-hierarchy.json
Add 5 expression nodes and 5 decision table rows in the correct priority position (after SSI Medicaid, before the institutional block).
Step 7: Orchestrator SSA data flow
Files: services/canopy-eligibility/src/orchestrator.rs
Update the Medicaid dispatch path to:
-
Call canopy-verification SSA endpoint(s) for the applicant.
-
Map SSA record fields to the 5 boolean flags.
-
Include the flags in the context sent to canopy-medicaid.
This step requires inspecting the current SsaSdxRecord / SsaBendexRecord structs and the verification service’s internal API. The NoopSsaAdapter provides deterministic data for testing.
Step 8: Unit tests
Files: services/canopy-medicaid/src/determine.rs
Add 3 test cases:
-
Pickle eligible:
lost_ssi_due_to_cola = true→pickle_eligible = true, assigned_coa includes "pickle" -
DAC eligible:
is_disabled_adult_child = true→dac_eligible = true -
All SSA flags false: all 5 flags false → all 5 COAs denied with appropriate reasons
Files Touched
| File | Change |
|---|---|
|
Add 5 ApplicationContext fields, wire eligible_fn (5 arms), wire denial_reason_fn (5 arms) |
|
Extend NonMagiInput (5 boolean fields), NonMagiOutput (5 boolean fields) |
|
Add 5 boolean gate expressions + output keys |
|
Add 5 expression nodes + decision table rows |
|
Query canopy-verification SSA endpoints, map to boolean flags, inject into Medicaid context |
Verification
-
cargo nextest run -p canopy-medicaid --lib— unit tests pass (including 3 new tests) -
cargo nextest run -p canopy-eligibility --lib— orchestrator tests pass with SSA data flow -
cargo xtask dev reload— services start cleanly -
cargo nextest run --workspace— integration tests pass -
cargo xtask e2e— E2E tests pass -
Verify all 5 COAs no longer fall through to the
_ ⇒ falsecatch-all in eligible_fn
Documentation Updates
-
.claude/docs/services.md— update canopy-medicaid feature table (ABD FBR COAs), update canopy-eligibility data flow description -
CHANGELOG.adoc— entry under== Unreleased -
.claude/CLAUDE.md— update Phase 3 status
Errata
Step 7 deferred — orchestrator SSA data flow
The canopy-eligibility orchestrator was NOT updated in this implementation. The 5 boolean flags (lost_ssi_due_to_cola, is_disabled_adult_child, is_disabled_widow, is_widow_60_64, lost_ssi_as_disabled_child) are on ApplicationContext and wired through to the ruleset, but the orchestrator does not yet query canopy-verification for SSA SOLQ/BINDEX data to populate them.
Until Step 7 is implemented, these COAs can only be evaluated when the calling system (or test fixture) explicitly sets the boolean flags on the request payload. In production, the orchestrator must:
-
Call
canopy-verificationSSA endpoints for the applicant -
Map
SsaSdxRecord/SsaBendexRecordfields to the 5 flags -
Inject the flags into the Medicaid context before dispatching
This is the same pattern needed for Phase E (clinical data) and Phase F (foster care data) — the orchestrator is the integration point for all external data sources.