Plan: Medicaid COA Phase E — ABD Non-FBR Waivers + AMN

On this page

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:

  • MedicaidCategory enum contains all 9 variants.

  • CMD cascade evaluates them but all fall through to _ ⇒ false in eligible_fn (except NursingHome which is already wired).

  • NonMagiOutput has amn_eligible but 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_monthly on ApplicationContext and mnil pattern 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
NOTE
If Georgia’s ABD MNIL is the same as Family MNIL, these can share the same values — but they must be separate keys because federal regulations allow states to set different MNILs for ABD vs. family populations.

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,
NOTE
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
NOTE
All waiver COAs return 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
NOTE
Parental income is excluded per Katie Beckett. The child’s own income (typically zero) is the only factor — no income test is needed in practice.

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
NOTE
QDWI requires 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:

  1. After the FBR block (SSI, Pickle, DAC, etc.)

  2. Waivers: EDWP, NOW, COMP, ICWP (grouped)

  3. TEFRA

  4. Institutional: Hospice, Hospital (Nursing Home already present)

  5. QDWI (after institutional, before Q-Track)

  6. 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:

  1. EDWP eligible: waiver_type="edwp", level_of_care_met=true → edwp_eligible = true

  2. TEFRA eligible: is_child_disabled_at_home=true, age=12 → tefra_eligible = true; age=20 → denied

  3. QDWI eligible: age 67, disability, has_medicare_part_a=true, income ≤ 200% FPL, resources within limit → qdwi_eligible = true

  4. AMN spenddown: income $600, ABD MNIL $317, medical expenses $350 → spenddown $283 ≤ $350 → eligible

Files Touched

File Change

rulesets/georgia/jurisdiction.toml

Add 5 keys: abd_mnil_individual, abd_mnil_couple, qdwi_income_limit_pct_fpl, qdwi_resource_limit_individual, qdwi_resource_limit_couple

rulesets/georgia/citations.toml

Add 4 citation entries

services/canopy-medicaid/src/params.rs

Add 5 fields + accessors to MedicaidParameterTable

services/canopy-medicaid/src/determine.rs

Add 4 ApplicationContext fields, wire eligible_fn (9 arms), wire denial_reason_fn (9 arms)

services/canopy-medicaid/src/rules_client.rs

Extend NonMagiInput (7 fields), NonMagiOutput (8 fields)

rulesets/georgia/medicaid-non-magi.json

Add 8 new expressions, replace AMN stub expression

rulesets/georgia/medicaid-eligibility-hierarchy.json

Add 9 expression nodes + decision table rows

Verification

  1. cargo xtask policy audit — all new jurisdiction.toml keys have citations

  2. cargo nextest run -p canopy-medicaid --lib — unit tests pass (including 4 new tests)

  3. cargo xtask dev reload — service starts with new params

  4. cargo nextest run --workspace — integration tests pass

  5. cargo xtask e2e — E2E tests pass

  6. Verify the _ ⇒ false catch-all in eligible_fn covers only stub COAs (or is removed entirely if all COAs are now wired)

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

Edit this page · default