Plan: Medicaid COA Phase E — ABD Non-FBR Waivers + AMN
On this page
- Status
- Context
- Scope
- Dependencies
- Design
- Steps
- Step 1: jurisdiction.toml + citations.toml
- Step 2: MedicaidParameterTable
- Step 3: ApplicationContext fields
- Step 4: NonMagiInput extension
- Step 5: NonMagiOutput extension
- Step 6: medicaid-non-magi.json expressions
- Step 7: eligible_fn + denial_reason_fn
- Step 8: Hierarchy ruleset
- Step 9: AMN expression upgrade
- Step 10: Unit tests
- Files Touched
- Verification
- Documentation Updates
Status
| Step | Description | Status |
|---|---|---|
1 |
Add jurisdiction.toml keys and citations.toml entries for ABD MNIL and QDWI thresholds |
Done (2026-04-13) |
2 |
Extend MedicaidParameterTable with ABD MNIL and QDWI fields |
Done (2026-04-13) |
3 |
Add ApplicationContext fields (waiver_type, hospice_election, length_of_stay_days, is_child_disabled_at_home) |
Done (2026-04-13) |
4 |
Extend NonMagiInput with waiver/institutional/QDWI fields |
Done (2026-04-13) |
5 |
Extend NonMagiOutput with 9 boolean fields |
Done (2026-04-13) |
6 |
Add 9 expressions to medicaid-non-magi.json |
Done (2026-04-13) |
7 |
Wire eligible_fn and denial_reason_fn match arms (9 COAs) |
Done (2026-04-13) |
8 |
Add 9 COAs to hierarchy ruleset |
Done (2026-04-13) |
9 |
Update AMN expression with ABD MNIL spenddown (replacing stub) |
Done (2026-04-13) |
10 |
Unit tests (4 cases) |
Done (2026-04-13) |
Branch: feature/medicaid-coa-phase-e
Context
The ABD (Aged, Blind, Disabled) non-FBR block contains the most diverse set of Medicaid COAs. These range from HCBS waivers (EDWP, NOW, COMP, ICWP) that provide community-based alternatives to institutional care, to institutional COAs (Hospice, Hospital, Nursing Home — Nursing Home is already implemented), to special populations (TEFRA/Katie Beckett for disabled children at home, QDWI for Medicare Part A premium assistance), to the ABD Medically Needy (AMN) spenddown safety net.
Current state:
-
MedicaidCategoryenum contains all 9 variants. -
CMD cascade evaluates them but all fall through to
_ ⇒ falseineligible_fn(except NursingHome which is already wired). -
NonMagiOutputhasamn_eligiblebut with a stub expression in the ruleset. -
The existing AMN denial reason is "amn_requires_spenddown_calculation" — confirming the spenddown logic is not yet implemented.
Key design considerations:
-
Waiver COAs (EDWP/NOW/COMP/ICWP) return "eligible_pending_slot" — slot availability is an external verification step outside the rules engine. The ruleset evaluates clinical/financial eligibility; slot verification is a canopy-verification concern.
-
TEFRA/Katie Beckett excludes parental income from the budget per federal law — the child’s own income (if any) is the only factor.
-
QDWI is a Medicare premium assistance program similar to Q-Track but limited to Part A premiums for a narrower population.
-
AMN is the ABD equivalent of FM-MN (Phase B) but uses a different MNIL schedule.
Scope
In scope:
-
4 jurisdiction.toml keys with PAMMS citations (ABD MNIL, QDWI thresholds)
-
ApplicationContext extensions (4 fields)
-
NonMagiInput / NonMagiOutput extensions
-
9 expressions in medicaid-non-magi.json
-
eligible_fn / denial_reason_fn wiring (9 match arms)
-
Hierarchy ruleset additions (9 COAs)
-
AMN spenddown replacement (upgrade stub to full expression)
-
4 unit tests
Out of scope:
-
Waiver slot availability verification (canopy-verification adapter — separate plan)
-
Level-of-care (LOC) clinical assessment workflow (assumed provided as input flag)
-
HCBS waiver enrollment and service plan management (post-UAT)
-
Nursing Home COA (already implemented)
Dependencies
-
Phase B must be complete (provides
countable_resources,medical_expenses_monthlyon ApplicationContext andmnilpattern on NonMagiInput) -
services/canopy-medicaid/src/params.rs— MedicaidParameterTable (will be extended) -
rulesets/georgia/jurisdiction.toml—[medicaid]section
Design
jurisdiction.toml keys
Add under [medicaid] in rulesets/georgia/jurisdiction.toml:
# ABD Medically Needy Income Level (different from Family MNIL)
abd_mnil_individual = 317
abd_mnil_couple = 367
# QDWI thresholds (PAMMS — 42 USC 1396d(p))
qdwi_income_limit_pct_fpl = 200
qdwi_resource_limit_individual = 4000
qdwi_resource_limit_couple = 6000
citations.toml entries
4 entries:
[medicaid.abd_mnil_individual]
value = 317
source = "PAMMS 2196"
description = "ABD Medically Needy Income Level — individual"
[medicaid.abd_mnil_couple]
value = 367
source = "PAMMS 2196"
description = "ABD Medically Needy Income Level — couple"
[medicaid.qdwi_income_limit_pct_fpl]
value = 200
source = "PAMMS (42 USC 1396d(p))"
description = "QDWI income limit as percentage of FPL"
[medicaid.qdwi_resource_limit_individual]
value = 4000
source = "PAMMS (42 USC 1396d(p))"
description = "QDWI resource limit for an individual"
MedicaidParameterTable additions
Add to services/canopy-medicaid/src/params.rs:
abd_mnil_individual: Decimal,
abd_mnil_couple: Decimal,
qdwi_income_limit_pct_fpl: Decimal,
qdwi_resource_limit_individual: Decimal,
qdwi_resource_limit_couple: Decimal,
With accessors following existing pattern.
ApplicationContext additions
Add to services/canopy-medicaid/src/determine.rs, struct ApplicationContext:
/// HCBS waiver type requested: "edwp", "now", "comp", "icwp", or null.
#[serde(default)]
pub waiver_type: Option<String>,
/// Hospice election filed (for Hospice COA).
#[serde(default)]
pub hospice_election: Option<bool>,
/// Length of institutional stay in days (for Hospital COA).
#[serde(default)]
pub length_of_stay_days: Option<i32>,
/// Disabled child living at home (TEFRA/Katie Beckett, PAMMS).
#[serde(default)]
pub is_child_disabled_at_home: Option<bool>,
NonMagiInput additions
Add to services/canopy-medicaid/src/rules_client.rs, struct NonMagiInput:
pub waiver_type: Option<String>,
pub hospice_election: bool,
pub length_of_stay_days: i32,
pub is_child_disabled_at_home: bool,
#[serde(serialize_with = "serialize_as_number")]
pub abd_mnil: Decimal,
#[serde(serialize_with = "serialize_as_number")]
pub qdwi_income_threshold: Decimal,
#[serde(serialize_with = "serialize_as_number")]
pub qdwi_resource_limit: Decimal,
countable_resources, medical_expenses_monthly, net_countable_income, and qmb_resource_limit are already on NonMagiInput from Phase B.
NonMagiOutput additions
Add to services/canopy-medicaid/src/rules_client.rs, struct NonMagiOutput:
pub edwp_eligible: bool,
pub now_waiver_eligible: bool,
pub comp_waiver_eligible: bool,
pub tefra_eligible: bool,
pub hospice_eligible: bool,
pub hospital_eligible: bool,
pub icwp_eligible: bool,
pub qdwi_eligible: bool,
// amn_eligible already exists — expression is being upgraded
medicaid-non-magi.json expressions
EDWP (new expression id ex-edwp):
waiver_type == "edwp" and level_of_care_met
NOW Waiver (new expression id ex-now):
waiver_type == "now" and level_of_care_met
COMP Waiver (new expression id ex-comp):
waiver_type == "comp" and level_of_care_met
ICWP (new expression id ex-icwp):
waiver_type == "icwp" and level_of_care_met
true for rules eligibility. Slot availability is verified externally. The Rust eligible_fn can optionally annotate the result as "eligible_pending_slot" in the determination record.
TEFRA/Katie Beckett (new expression id ex-tefra):
is_child_disabled_at_home and applicant_age < 19
Hospice (new expression id ex-hospice):
hospice_election and level_of_care_met
Hospital (new expression id ex-hospital):
is_institutionalized and length_of_stay_days > 30
QDWI (new expression id ex-qdwi):
(applicant_age >= 65 or disability_status != null) and has_medicare_part_a and net_countable_income <= qdwi_income_threshold and countable_resources <= qdwi_resource_limit
has_medicare_part_a (from Phase B’s NonMagiInput extension).
AMN (replace existing stub expression id ex-amn):
net_countable_income > abd_mnil and (net_countable_income - abd_mnil) <= medical_expenses_monthly
eligible_fn additions
MedicaidCategory::Edwp => non_magi_out.edwp_eligible,
MedicaidCategory::NowWaiver => non_magi_out.now_waiver_eligible,
MedicaidCategory::CompWaiver => non_magi_out.comp_waiver_eligible,
MedicaidCategory::TefraKatieBeckett => non_magi_out.tefra_eligible,
MedicaidCategory::Hospice => non_magi_out.hospice_eligible,
MedicaidCategory::Hospital => non_magi_out.hospital_eligible,
MedicaidCategory::Icwp => non_magi_out.icwp_eligible,
MedicaidCategory::Qdwi => non_magi_out.qdwi_eligible,
// MedicaidCategory::Amn already has a match arm — it now returns a real evaluation
denial_reason_fn additions
MedicaidCategory::Edwp => "edwp_waiver_not_requested_or_loc_not_met",
MedicaidCategory::NowWaiver => "now_waiver_not_requested_or_loc_not_met",
MedicaidCategory::CompWaiver => "comp_waiver_not_requested_or_loc_not_met",
MedicaidCategory::TefraKatieBeckett if age >= 19 => "age_19_or_older",
MedicaidCategory::TefraKatieBeckett => "not_disabled_child_at_home",
MedicaidCategory::Hospice => "no_hospice_election_or_loc_not_met",
MedicaidCategory::Hospital if !is_institutionalized => "not_institutionalized",
MedicaidCategory::Hospital => "length_of_stay_under_30_days",
MedicaidCategory::Icwp => "icwp_waiver_not_requested_or_loc_not_met",
MedicaidCategory::Qdwi => "qdwi_income_or_resources_over_limit_or_no_medicare_part_a",
MedicaidCategory::Amn => "abd_amn_spenddown_exceeds_medical_expenses",
Steps
Step 1: jurisdiction.toml + citations.toml
Files: rulesets/georgia/jurisdiction.toml, rulesets/georgia/citations.toml
Add 5 keys under [medicaid] (abd_mnil_individual, abd_mnil_couple, qdwi_income_limit_pct_fpl, qdwi_resource_limit_individual, qdwi_resource_limit_couple). Add 4 citation entries. Run cargo xtask policy audit.
Step 2: MedicaidParameterTable
Files: services/canopy-medicaid/src/params.rs
Add 5 struct fields and accessors. Load from jurisdiction.toml in the load() function following existing patterns.
Step 3: ApplicationContext fields
Files: services/canopy-medicaid/src/determine.rs
Add 4 fields to ApplicationContext as specified in Design. Unwrap in the determine function body.
Step 4: NonMagiInput extension
Files: services/canopy-medicaid/src/rules_client.rs
Add 7 fields to NonMagiInput (waiver_type, hospice_election, length_of_stay_days, is_child_disabled_at_home, abd_mnil, qdwi_income_threshold, qdwi_resource_limit). Wire in determine.rs where NonMagiInput is constructed, computing QDWI thresholds as:
let qdwi_income_threshold = fpl_100 * params.qdwi_income_limit_pct_fpl() / pct100;
let qdwi_resource_limit = if ctx.household_size <= 1 {
params.qdwi_resource_limit_individual()
} else {
params.qdwi_resource_limit_couple()
};
let abd_mnil = if ctx.household_size <= 1 {
params.abd_mnil_individual()
} else {
params.abd_mnil_couple()
};
Step 5: NonMagiOutput extension
Files: services/canopy-medicaid/src/rules_client.rs
Add 8 boolean fields to NonMagiOutput (edwp_eligible, now_waiver_eligible, comp_waiver_eligible, tefra_eligible, hospice_eligible, hospital_eligible, icwp_eligible, qdwi_eligible). The existing amn_eligible field will now return real evaluations.
Step 6: medicaid-non-magi.json expressions
Files: rulesets/georgia/medicaid-non-magi.json
Add 8 new expression nodes (ex-edwp, ex-now, ex-comp, ex-icwp, ex-tefra, ex-hospice, ex-hospital, ex-qdwi). Replace the existing AMN stub expression with the full spenddown formula. Add output keys for all 8 new booleans.
Step 7: eligible_fn + denial_reason_fn
Files: services/canopy-medicaid/src/determine.rs
Add 8 new match arms to eligible_fn (EDWP, NOW, COMP, TEFRA, Hospice, Hospital, ICWP, QDWI). The AMN arm already exists but now returns a real evaluation. Add/update denial reasons for all 9 COAs. Remove the _ ⇒ false catch-all if all COAs are now wired (verify against MedicaidCategory enum).
Step 8: Hierarchy ruleset
Files: rulesets/georgia/medicaid-eligibility-hierarchy.json
Add 9 expression nodes and decision table rows. Position in the hierarchy:
-
After the FBR block (SSI, Pickle, DAC, etc.)
-
Waivers: EDWP, NOW, COMP, ICWP (grouped)
-
TEFRA
-
Institutional: Hospice, Hospital (Nursing Home already present)
-
QDWI (after institutional, before Q-Track)
-
AMN (last in ABD block, safety net)
Step 9: AMN expression upgrade
Files: rulesets/georgia/medicaid-non-magi.json
This is part of Step 6 but called out separately for clarity. Replace the AMN stub expression (currently just a placeholder) with:
net_countable_income > abd_mnil and (net_countable_income - abd_mnil) <= medical_expenses_monthly
Update the AMN denial reason in denial_reason_fn from "amn_requires_spenddown_calculation" to "abd_amn_spenddown_exceeds_medical_expenses".
Step 10: Unit tests
Files: services/canopy-medicaid/src/determine.rs
Add 4 test cases:
-
EDWP eligible: waiver_type="edwp", level_of_care_met=true →
edwp_eligible = true -
TEFRA eligible: is_child_disabled_at_home=true, age=12 →
tefra_eligible = true; age=20 → denied -
QDWI eligible: age 67, disability, has_medicare_part_a=true, income ≤ 200% FPL, resources within limit →
qdwi_eligible = true -
AMN spenddown: income $600, ABD MNIL $317, medical expenses $350 → spenddown $283 ≤ $350 → eligible
Files Touched
| File | Change |
|---|---|
|
Add 5 keys: abd_mnil_individual, abd_mnil_couple, qdwi_income_limit_pct_fpl, qdwi_resource_limit_individual, qdwi_resource_limit_couple |
|
Add 4 citation entries |
|
Add 5 fields + accessors to MedicaidParameterTable |
|
Add 4 ApplicationContext fields, wire eligible_fn (9 arms), wire denial_reason_fn (9 arms) |
|
Extend NonMagiInput (7 fields), NonMagiOutput (8 fields) |
|
Add 8 new expressions, replace AMN stub expression |
|
Add 9 expression nodes + decision table rows |
Verification
-
cargo xtask policy audit— all new jurisdiction.toml keys have citations -
cargo nextest run -p canopy-medicaid --lib— unit tests pass (including 4 new tests) -
cargo xtask dev reload— service starts with new params -
cargo nextest run --workspace— integration tests pass -
cargo xtask e2e— E2E tests pass -
Verify the
_ ⇒ falsecatch-all in eligible_fn covers only stub COAs (or is removed entirely if all COAs are now wired) -
Verify AMN denial reason no longer says "requires_spenddown_calculation"
Documentation Updates
-
.claude/docs/services.md— update canopy-medicaid feature table (ABD non-FBR waivers, QDWI, AMN) -
CHANGELOG.adoc— entry under== Unreleased -
.claude/CLAUDE.md— update Phase 3 status