diff --git a/changelog.d/codex-msp-ssi-deeming-assets.fixed.md b/changelog.d/codex-msp-ssi-deeming-assets.fixed.md new file mode 100644 index 00000000000..6d3f9afcb5a --- /dev/null +++ b/changelog.d/codex-msp-ssi-deeming-assets.fixed.md @@ -0,0 +1 @@ +Fix MSP rules to use SSI spouse deeming, applicant-plus-spouse resources, strict SLMB/QI boundaries, category-consistent MSP eligibility, and complete 2021-2026 Medicare/MSP premium and resource thresholds. diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml index 065cd4d3d63..a681c0151e4 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml @@ -4,10 +4,22 @@ metadata: period: month label: Medicare Part A full premium reference: + - title: 2021 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/press-releases/2021-medicare-part-b-premiums-remain-steady + - title: 2022 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2022-medicare-parts-b-premiums-deductibles-2022-medicare-part-d-income-related-monthly-adjustment + - title: 2023 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2023-medicare-parts-b-premiums-and-deductibles-2023-medicare-part-d-income-related-monthly - title: 2024 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2024-medicare-parts-b-premiums-and-deductibles - title: 2025 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2025-medicare-parts-b-premiums-and-deductibles + - title: 2026 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2026-medicare-parts-b-premiums-deductibles values: + 2021-01-01: 471 + 2022-01-01: 499 + 2023-01-01: 506 2024-01-01: 505 2025-01-01: 518 + 2026-01-01: 565 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml index 38a74c2ea22..c124a354e27 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml @@ -4,10 +4,20 @@ metadata: period: month label: Medicare Part A reduced premium reference: + - title: 2022 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2022-medicare-parts-b-premiums-deductibles-2022-medicare-part-d-income-related-monthly-adjustment + - title: 2023 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2023-medicare-parts-b-premiums-and-deductibles-2023-medicare-part-d-income-related-monthly - title: 2024 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2024-medicare-parts-b-premiums-and-deductibles - title: 2025 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2025-medicare-parts-b-premiums-and-deductibles + - title: 2026 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2026-medicare-parts-b-premiums-deductibles values: + 2021-01-01: 259 + 2022-01-01: 274 + 2023-01-01: 278 2024-01-01: 278 2025-01-01: 285 + 2026-01-01: 311 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml index 1db473e354e..3f456752c45 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml @@ -5,12 +5,21 @@ metadata: label: Medicare Part B base premium reference: - title: 2021 Medicare Parts A & B Premiums and Deductibles - href: https://www.cms.gov/newsroom/fact-sheets/2021-medicare-parts-b-premiums-and-deductibles + href: https://www.cms.gov/newsroom/press-releases/2021-medicare-part-b-premiums-remain-steady + - title: 2022 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2022-medicare-parts-b-premiums-deductibles-2022-medicare-part-d-income-related-monthly-adjustment + - title: 2023 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2023-medicare-parts-b-premiums-and-deductibles-2023-medicare-part-d-income-related-monthly - title: 2024 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2024-medicare-parts-b-premiums-and-deductibles - title: 2025 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2025-medicare-parts-b-premiums-and-deductibles + - title: 2026 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2026-medicare-parts-b-premiums-deductibles values: 2021-01-01: 148.50 + 2022-01-01: 170.1 + 2023-01-01: 164.9 2024-01-01: 174.70 2025-01-01: 185 + 2026-01-01: 202.9 diff --git a/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/couple.yaml b/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/couple.yaml index 8e72a24f4b9..8d5e5ce2a0e 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/couple.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/couple.yaml @@ -2,6 +2,7 @@ description: The Department of Health and Human Services limits resources to thi values: 2024-01-01: 14_130 2025-01-01: 14_470 + 2026-01-01: 14_910 metadata: unit: currency-USD diff --git a/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/individual.yaml b/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/individual.yaml index e482a5ecf78..5787d7e2255 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/individual.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/individual.yaml @@ -2,6 +2,7 @@ description: The Department of Health and Human Services limits resources to thi values: 2024-01-01: 9_430 2025-01-01: 9_660 + 2026-01-01: 9_950 metadata: unit: currency-USD diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml index c1bec359e6c..27e9423d9c3 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml @@ -33,3 +33,39 @@ medicare_quarters_of_coverage: 40 output: base_part_a_premium: 0 + +- name: unit test 5 - reduced premium in 2022 + period: 2022 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 35 + output: + base_part_a_premium: 3_288 # $274 * 12 months + +- name: unit test 6 - full premium in 2023 + period: 2023 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 20 + output: + base_part_a_premium: 6_072 # $506 * 12 months + +- name: unit test 7 - reduced premium in 2026 + period: 2026 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 35 + output: + base_part_a_premium: 3_732 # $311 * 12 months + +- name: 2026 Part A full premium for fewer than 30 quarters. + period: 2026 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 20 + output: + base_part_a_premium: 6_780 # $565 * 12 months diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml index 7c873218320..0711fd86c9f 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml @@ -13,3 +13,27 @@ is_medicare_eligible: false output: base_part_b_premium: 0 + +- name: unit test 3 - eligible in 2022 + period: 2022 + input: + age: 65 + is_medicare_eligible: true + output: + base_part_b_premium: 2_041.20 # $170.10 * 12 months + +- name: unit test 4 - eligible in 2023 + period: 2023 + input: + age: 65 + is_medicare_eligible: true + output: + base_part_b_premium: 1_978.80 # $164.90 * 12 months + +- name: unit test 5 - eligible in 2026 + period: 2026 + input: + age: 65 + is_medicare_eligible: true + output: + base_part_b_premium: 2_434.80 # $202.90 * 12 months diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/category/is_qi_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/category/is_qi_eligible.yaml index 23514233058..9afd6bad9ed 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/category/is_qi_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/category/is_qi_eligible.yaml @@ -9,7 +9,20 @@ # $19,440 unearned -> ($1,620 - $20) = $1,600/month countable # $1,506 < $1,600 < $1,694.25 (120-135% FPL) -> QI income range ssi_unearned_income: 19_440 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 + spm_unit_is_married: false + is_medicaid_eligible: false + output: + is_qi_eligible: true + +- name: QI eligible - income exactly at 120% FPL, no Medicaid + period: 2024-01 + input: + age: 67 + is_medicare_eligible: true + # $18,312 unearned -> ($1,526 - $20) = $1,506/month countable + ssi_unearned_income: 18_312 + bank_account_assets: 5_000 spm_unit_is_married: false is_medicaid_eligible: false output: @@ -22,9 +35,22 @@ is_medicare_eligible: true # Income in QI range ssi_unearned_income: 19_440 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false # But eligible for Medicaid is_medicaid_eligible: true output: is_qi_eligible: false + +- name: QI not eligible - income exactly at 135% FPL + period: 2024-01 + input: + age: 67 + is_medicare_eligible: true + # $20,571 unearned -> ($1,714.25 - $20) = $1,694.25/month countable + ssi_unearned_income: 20_571 + bank_account_assets: 5_000 + spm_unit_is_married: false + is_medicaid_eligible: false + output: + is_qi_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/category/msp_category.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/category/msp_category.yaml index 590bff5aea8..c788af80531 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/category/msp_category.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/category/msp_category.yaml @@ -2,8 +2,8 @@ # Countable = unearned - $20 (or earned - $85 - 50% remaining) # For 2024 individual FPL = $15,060: # QMB: <= 100% = $1,255/month countable -# SLMB: 100-120% = $1,255.01-$1,506/month countable -# QI: 120-135% = $1,506.01-$1,694.25/month countable +# SLMB: > 100% and < 120% FPL +# QI: >= 120% and < 135% FPL - name: MSP category - QMB (income at 100% FPL) period: 2024-01 @@ -13,7 +13,7 @@ # $12,000 unearned -> ($1,000 - $20) = $980/month countable # $980 < $1,255 (100% FPL) -> QMB ssi_unearned_income: 12_000 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false output: msp_category: QMB @@ -26,7 +26,7 @@ # $16,500 unearned -> ($1,375 - $20) = $1,355/month countable # $1,255 < $1,355 < $1,506 (100-120% FPL) -> SLMB ssi_unearned_income: 16_500 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false output: msp_category: SLMB @@ -39,13 +39,27 @@ # $19,440 unearned -> ($1,620 - $20) = $1,600/month countable # $1,506 < $1,600 < $1,694.25 (120-135% FPL) -> QI ssi_unearned_income: 19_440 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false # QI requires not being eligible for other Medicaid is_medicaid_eligible: false output: msp_category: QI +- name: MSP category - QI (income exactly at 120% FPL) + period: 2024-01 + input: + age: 67 + is_medicare_eligible: true + # $18,312 unearned -> ($1,526 - $20) = $1,506/month countable + # Exact 120% FPL belongs to QI, not SLMB + ssi_unearned_income: 18_312 + bank_account_assets: 5_000 + spm_unit_is_married: false + is_medicaid_eligible: false + output: + msp_category: QI + - name: MSP category - NONE (income above 135% FPL) period: 2024-01 input: @@ -54,18 +68,32 @@ # $21_840 unearned -> ($1,820 - $20) = $1,800/month countable # $1,800 > $1,694.25 (135% FPL) -> NONE ssi_unearned_income: 21_840 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false output: msp_category: NONE +- name: MSP category - NONE (income exactly at 135% FPL) + period: 2024-01 + input: + age: 67 + is_medicare_eligible: true + # $20,571 unearned -> ($1,714.25 - $20) = $1,694.25/month countable + # Exact 135% FPL is not QI-eligible + ssi_unearned_income: 20_571 + bank_account_assets: 5_000 + spm_unit_is_married: false + is_medicaid_eligible: false + output: + msp_category: NONE + - name: MSP category - NONE (not Medicare eligible) period: 2024-01 input: age: 60 is_medicare_eligible: false ssi_unearned_income: 10_000 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false output: msp_category: NONE diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.yaml new file mode 100644 index 00000000000..78404ca8f06 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.yaml @@ -0,0 +1,85 @@ +- name: Case 1, single applicant under the MSP asset limit. + period: 2024-01 + input: + age: 67 + bank_account_assets: 5_000 + spm_unit_is_married: false + state_code: TX + output: + msp_asset_eligible: true + +- name: Case 2, couple resources use the spouses' combined assets. + period: 2024-01 + input: + people: + person1: + age: 68 + bank_account_assets: 8_000 + person2: + age: 66 + bank_account_assets: 8_000 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_is_married: true + households: + household: + members: [person1, person2] + state_code: TX + output: + msp_asset_eligible: [false, false] + +- name: Case 3, non-spouse household members' assets are excluded. + period: 2024-01 + input: + people: + person1: + age: 67 + bank_account_assets: 5_000 + person2: + age: 30 + bank_account_assets: 20_000 + tax_units: + tax_unit: + members: [person1] + marital_units: + marital_unit1: + members: [person1] + marital_unit2: + members: [person2] + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_is_married: false + households: + household: + members: [person1, person2] + state_code: TX + output: + msp_asset_eligible: [true, false] + +- name: Case 4, 2026 individual asset limit uses the current Medicare.gov value. + period: 2026-01 + input: + age: 67 + bank_account_assets: 9_950 + spm_unit_is_married: false + state_code: TX + output: + msp_asset_eligible: true + +- name: Case 5, 2026 individual asset limit rejects amounts above the current threshold. + period: 2026-01 + input: + age: 67 + bank_account_assets: 9_951 + spm_unit_is_married: false + state_code: TX + output: + msp_asset_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_eligible.yaml index 55ddce8fe31..52a1c364f2d 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_eligible.yaml @@ -7,7 +7,7 @@ is_medicare_eligible: true # $10,000 unearned -> ($833.33 - $20) = $813.33/month countable ssi_unearned_income: 10_000 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false output: msp_eligible: true @@ -20,8 +20,9 @@ # $19_200 unearned -> ($1,600 - $20) = $1,580/month countable # $1,580 is between 120% ($1,506) and 135% ($1,694.25) FPL -> QI eligible ssi_unearned_income: 19_200 - spm_unit_cash_assets: 8_000 + bank_account_assets: 8_000 spm_unit_is_married: false + is_medicaid_eligible: false output: msp_eligible: true @@ -33,7 +34,7 @@ # $24_000 unearned -> ($2,000 - $20) = $1,980/month countable # $1,980 > $1,694.25 (135% FPL) -> not eligible ssi_unearned_income: 24_000 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false output: msp_eligible: false @@ -44,7 +45,7 @@ age: 67 is_medicare_eligible: true ssi_unearned_income: 10_000 - spm_unit_cash_assets: 15_000 # Over $9,430 limit + bank_account_assets: 15_000 # Over $9,430 limit spm_unit_is_married: false state_code: TX # Use a state with asset test output: @@ -56,7 +57,19 @@ age: 60 # Under 65 is_medicare_eligible: false ssi_unearned_income: 10_000 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false output: msp_eligible: false + +- name: MSP not eligible - QI income band but already Medicaid eligible + period: 2024-01 + input: + age: 67 + is_medicare_eligible: true + ssi_unearned_income: 19_440 + bank_account_assets: 5_000 + spm_unit_is_married: false + is_medicaid_eligible: true + output: + msp_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_income_eligible.yaml new file mode 100644 index 00000000000..f2a0690fc2f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/eligibility/msp_income_eligible.yaml @@ -0,0 +1,19 @@ +- name: MSP income eligible - below 135% FPL + period: 2024-01 + input: + age: 67 + # $19,440 unearned -> ($1,620 - $20) = $1,600/month countable + ssi_unearned_income: 19_440 + spm_unit_is_married: false + output: + msp_income_eligible: true + +- name: MSP income eligible - exact 135% FPL is excluded + period: 2024-01 + input: + age: 67 + # $20,571 unearned -> ($1,714.25 - $20) = $1,694.25/month countable + ssi_unearned_income: 20_571 + spm_unit_is_married: false + output: + msp_income_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml new file mode 100644 index 00000000000..6172d39ff87 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml @@ -0,0 +1,76 @@ +- name: Case 1, couple income is combined for married MSP applicants. + period: 2024-01 + input: + people: + person1: + age: 68 + ssi_unearned_income: 12_000 + person2: + age: 66 + ssi_unearned_income: 12_000 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_is_married: true + output: + msp_countable_income: [1_980, 1_980] + absolute_error_margin: 0.01 + +- name: Case 2, spouse-to-spouse deeming counts an ineligible spouse's earnings. + period: 2025-01 + input: + people: + person1: + age: 65 + is_tax_unit_spouse: true + person2: + age: 60 + is_tax_unit_head: true + ssi_earned_income: 31_080 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_is_married: true + output: + msp_countable_income: [1_252.50, 1_252.50] + absolute_error_margin: 0.01 + +- name: Spouse deeming with moderate income still allows QMB eligibility. + period: 2024-01 + absolute_error_margin: 1 + input: + people: + person1: + age: 67 + is_medicare_eligible: true + is_ssi_aged_blind_disabled: true + ssi_unearned_income: 0 + ssi_earned_income: 0 + person2: + age: 60 + is_medicare_eligible: false + is_ssi_aged_blind_disabled: false + ssi_earned_income: 12_000 + ssi_unearned_income: 0 + marital_units: + marital_unit: + members: [person1, person2] + output: + # Spouse earned $12,000/yr deemed to person1. Monthly: $1,000. + # SSI exclusions: $20 general shifted to earned (unearned=0), + # $65 earned excl => flat $85, remainder $915, 50% excluded => $457.50/mo. + # Person2 own income also $12,000 earned, same exclusions => $457.50/mo. + # Both under 100% FPL (~$1,255/mo) so person1 still qualifies for QMB. + msp_countable_income: [457.50, 457.50] diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/integration.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/integration.yaml index 4ed6182d095..c177b3f7b3a 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/integration.yaml @@ -11,21 +11,22 @@ # Combined $18,000 unearned -> ($1,500 - $20) = $1,480/month combined countable # $1,480 < $1,703.33 (100% couple FPL) -> QMB ssi_unearned_income: 9_000 + bank_account_assets: 5_000 spouse2: age: 66 is_medicare_eligible: true ssi_unearned_income: 9_000 - marital_units: - marital_unit: - members: [spouse1, spouse2] + bank_account_assets: 5_000 spm_units: spm_unit: members: [spouse1, spouse2] spm_unit_is_married: true - spm_unit_cash_assets: 10_000 # Under $14,130 couple limit tax_units: tax_unit: members: [spouse1, spouse2] + marital_units: + marital_unit: + members: [spouse1, spouse2] households: household: members: [spouse1, spouse2] @@ -46,10 +47,12 @@ age: 68 is_medicare_eligible: true ssi_unearned_income: 14_000 + bank_account_assets: 5_000 spouse2: age: 66 is_medicare_eligible: true ssi_unearned_income: 14_000 + bank_account_assets: 5_000 marital_units: marital_unit: members: [spouse1, spouse2] @@ -57,7 +60,6 @@ spm_unit: members: [spouse1, spouse2] spm_unit_is_married: true - spm_unit_cash_assets: 10_000 # Under $14,130 couple limit tax_units: tax_unit: members: [spouse1, spouse2] @@ -81,10 +83,12 @@ age: 68 is_medicare_eligible: true ssi_unearned_income: 21_000 + bank_account_assets: 5_000 spouse2: age: 66 is_medicare_eligible: true ssi_unearned_income: 0 + bank_account_assets: 5_000 marital_units: marital_unit: members: [spouse1, spouse2] @@ -92,7 +96,6 @@ spm_unit: members: [spouse1, spouse2] spm_unit_is_married: true - spm_unit_cash_assets: 10_000 tax_units: tax_unit: members: [spouse1, spouse2] @@ -103,11 +106,12 @@ msp_eligible: [true, true] msp_category: [SLMB, SLMB] -- name: MSP integration - married couple, only one Medicare-eligible, no income aggregation +- name: MSP integration - married couple, only one Medicare-eligible, no full couple aggregation period: 2024-01 # Edge case: only one spouse is Medicare-eligible. - # person.marital_unit.sum(is_medicare_eligible) == 1, not 2, so no aggregation. - # Spouse1 is tested on their own income ($10,000) against the couple FPL. + # person.marital_unit.sum(is_medicare_eligible) == 1, not 2, so full couple + # aggregation does not apply. With no spouse income to deem, spouse1 is + # tested on their own income ($10,000) against the married MSP threshold. # ($10,000 - $240) / 12 = $813.33/month < $1,703.33 -> QMB. # Spouse2 is not Medicare-eligible and cannot qualify for MSP. input: @@ -116,10 +120,12 @@ age: 67 is_medicare_eligible: true ssi_unearned_income: 10_000 + bank_account_assets: 5_000 spouse2: age: 55 is_medicare_eligible: false - ssi_unearned_income: 20_000 + ssi_unearned_income: 0 + bank_account_assets: 5_000 marital_units: marital_unit: members: [spouse1, spouse2] @@ -127,7 +133,6 @@ spm_unit: members: [spouse1, spouse2] spm_unit_is_married: true - spm_unit_cash_assets: 10_000 tax_units: tax_unit: members: [spouse1, spouse2] @@ -151,11 +156,13 @@ is_medicare_eligible: true is_medicaid_eligible: false ssi_unearned_income: 13_200 + bank_account_assets: 5_000 spouse2: age: 66 is_medicare_eligible: true is_medicaid_eligible: false ssi_unearned_income: 13_200 + bank_account_assets: 5_000 marital_units: marital_unit: members: [spouse1, spouse2] @@ -163,7 +170,6 @@ spm_unit: members: [spouse1, spouse2] spm_unit_is_married: true - spm_unit_cash_assets: 10_000 tax_units: tax_unit: members: [spouse1, spouse2] @@ -186,11 +192,11 @@ # $16_500 unearned -> ($1,375 - $20) = $1,355/month countable # $1,255 < $1,355 < $1,506 (100-120% FPL) -> SLMB ssi_unearned_income: 16_500 + bank_account_assets: 8_000 spm_units: spm_unit: members: [person1] spm_unit_is_married: false - spm_unit_cash_assets: 8_000 tax_units: tax_unit: members: [person1] @@ -211,11 +217,11 @@ age: 67 is_medicare_eligible: true ssi_unearned_income: 10_000 + bank_account_assets: 12_000 spm_units: spm_unit: members: [person1] spm_unit_is_married: false - spm_unit_cash_assets: 12_000 # Over $9,430 individual limit tax_units: tax_unit: members: [person1] @@ -227,3 +233,66 @@ msp_eligible: [false] msp_category: [NONE] msp: [0] + +- name: MSP integration - married couple income is combined for category determination + period: 2024-01 + input: + people: + spouse1: + age: 68 + is_medicare_eligible: true + ssi_unearned_income: 12_000 + bank_account_assets: 5_000 + spouse2: + age: 66 + is_medicare_eligible: true + ssi_unearned_income: 12_000 + bank_account_assets: 5_000 + spm_units: + spm_unit: + members: [spouse1, spouse2] + spm_unit_is_married: true + tax_units: + tax_unit: + members: [spouse1, spouse2] + marital_units: + marital_unit: + members: [spouse1, spouse2] + households: + household: + members: [spouse1, spouse2] + output: + msp_eligible: [true, true] + msp_category: [SLMB, SLMB] + +- name: MSP integration - ineligible spouse income is deemed to the Medicare applicant + period: 2024-01 + input: + people: + person1: + age: 67 + is_medicare_eligible: true + is_tax_unit_spouse: true + bank_account_assets: 5_000 + person2: + age: 60 + is_tax_unit_head: true + ssi_earned_income: 60_000 + bank_account_assets: 5_000 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_is_married: true + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + msp_eligible: [false, false] + msp_category: [NONE, NONE] diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_benefit_value.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_benefit_value.yaml index bd5413a9593..d4f28fa02d3 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_benefit_value.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_benefit_value.yaml @@ -8,7 +8,7 @@ # $12,000 unearned -> ($1,000 - $20) = $980/month countable # $980 < $1,255 (100% FPL) -> QMB ssi_unearned_income: 12_000 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false base_part_a_premium: 6_060 # Annual Part A premium ($505/month * 12) base_part_b_premium: 2_096.40 # Annual Part B premium ($174.70/month * 12) @@ -24,7 +24,7 @@ # $16,500 unearned -> ($1,375 - $20) = $1,355/month countable # $1,255 < $1,355 < $1,506 (100-120% FPL) -> SLMB ssi_unearned_income: 16_500 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false base_part_a_premium: 0 # Assume premium-free Part A base_part_b_premium: 2_096.40 @@ -39,7 +39,7 @@ # $19,440 unearned -> ($1,620 - $20) = $1,600/month countable # $1,506 < $1,600 < $1,694.25 (120-135% FPL) -> QI ssi_unearned_income: 19_440 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false base_part_a_premium: 0 base_part_b_premium: 2_096.40 @@ -56,7 +56,7 @@ # $24_000 unearned -> ($2,000 - $20) = $1,980/month countable # $1,980 > $1,694.25 (135% FPL) -> not eligible ssi_unearned_income: 24_000 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false base_part_a_premium: 0 base_part_b_premium: 2_096.40 @@ -70,7 +70,7 @@ is_medicare_eligible: true # Low income to qualify for QMB ssi_unearned_income: 12_000 - spm_unit_cash_assets: 5_000 + bank_account_assets: 5_000 spm_unit_is_married: false # Explicitly provide quarters (no default in model) medicare_quarters_of_coverage: 40 @@ -80,3 +80,17 @@ # With 40 quarters, Part A is premium-free ($0) # Part B base premium for 2025 is $185/month msp_benefit_value: 185 + +- name: MSP benefit - integration test uses 2026 Medicare premiums + period: 2026-01 + input: + age: 67 + is_medicare_eligible: true + ssi_unearned_income: 12_000 + bank_account_assets: 5_000 + spm_unit_is_married: false + medicare_quarters_of_coverage: 40 + output: + # With 40 quarters, Part A is premium-free ($0) + # Part B base premium for 2026 is $202.90/month + msp_benefit_value: 202.90 diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_qi_eligible.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_qi_eligible.py index 36eeec8ce3a..febc8e8d5a8 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_qi_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_qi_eligible.py @@ -7,12 +7,12 @@ class is_qi_eligible(Variable): label = "Qualifying Individual (QI) eligible" definition_period = MONTH reference = ( - "https://www.law.cornell.edu/cfr/text/42/435.123", + "https://www.law.cornell.edu/cfr/text/42/435.125", "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", ) def formula(person, period, parameters): - # QI requires income above 120% FPL but at or below 135% FPL + # QI requires income at least 120% FPL but below 135% FPL # QI is only available for people who don't qualify for other Medicaid p = parameters(period).gov.hhs.medicare.savings_programs.eligibility.income @@ -25,9 +25,9 @@ def formula(person, period, parameters): slmb_income_limit = fpg * p.slmb.fpl_limit qi_income_limit = fpg * p.qi.fpl_limit - income_above_slmb = countable_income > slmb_income_limit - income_at_or_below_qi = countable_income <= qi_income_limit - income_eligible = income_above_slmb & income_at_or_below_qi + income_at_or_above_slmb = countable_income >= slmb_income_limit + income_below_qi = countable_income < qi_income_limit + income_eligible = income_at_or_above_slmb & income_below_qi # QI excludes those eligible for other Medicaid coverage medicaid_eligible = person("is_medicaid_eligible", period.this_year) diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_qmb_eligible.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_qmb_eligible.py index a5a65ea03c2..97628c9ce47 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_qmb_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_qmb_eligible.py @@ -7,7 +7,7 @@ class is_qmb_eligible(Variable): label = "Qualified Medicare Beneficiary (QMB) eligible" definition_period = MONTH reference = ( - "https://www.law.cornell.edu/cfr/text/42/435.121", + "https://www.law.cornell.edu/cfr/text/42/435.123", "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", ) diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_slmb_eligible.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_slmb_eligible.py index 44f66dc7097..9bfe6d615c6 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_slmb_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/is_slmb_eligible.py @@ -7,12 +7,12 @@ class is_slmb_eligible(Variable): label = "Specified Low-Income Medicare Beneficiary (SLMB) eligible" definition_period = MONTH reference = ( - "https://www.law.cornell.edu/cfr/text/42/435.122", + "https://www.law.cornell.edu/cfr/text/42/435.124", "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", ) def formula(person, period, parameters): - # SLMB requires income above 100% FPL but at or below 120% FPL + # SLMB requires income above 100% FPL but below 120% FPL p = parameters(period).gov.hhs.medicare.savings_programs.eligibility.income medicare_eligible = person("is_medicare_eligible", period.this_year) @@ -25,7 +25,7 @@ def formula(person, period, parameters): slmb_income_limit = fpg * p.slmb.fpl_limit income_above_qmb = countable_income > qmb_income_limit - income_at_or_below_slmb = countable_income <= slmb_income_limit - income_eligible = income_above_qmb & income_at_or_below_slmb + income_below_slmb = countable_income < slmb_income_limit + income_eligible = income_above_qmb & income_below_slmb return medicare_eligible & income_eligible & asset_eligible diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/msp_category.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/msp_category.py index 3ebd2c1deac..5befee315ff 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/msp_category.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/category/msp_category.py @@ -17,7 +17,7 @@ class msp_category(Variable): definition_period = MONTH reference = ( "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", - "https://www.law.cornell.edu/cfr/text/42/435.121", + "https://www.law.cornell.edu/uscode/text/42/1396d#p", ) def formula(person, period, parameters): diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.py index dcdcbd71da9..b662caba728 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_asset_eligible.py @@ -7,6 +7,8 @@ class msp_asset_eligible(Variable): label = "Medicare Savings Program asset eligible" definition_period = MONTH reference = ( + "https://www.law.cornell.edu/uscode/text/42/1396d#p", + "https://www.law.cornell.edu/uscode/text/42/1382b", "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", "https://www.medicareinteractive.org/understanding-medicare/" "cost-saving-programs/medicare-savings-programs-qmb-slmb-qi/" @@ -19,8 +21,13 @@ def formula(person, period, parameters): # Check if asset test applies (some states have eliminated it) asset_test_applies = p.asset.applies[state_code] # If asset test doesn't apply, everyone is asset-eligible - cash_assets = person.spm_unit("spm_unit_cash_assets", period.this_year) - married = person.spm_unit("spm_unit_is_married", period) + personal_resources = person("ssi_countable_resources", period.this_year) + married = person.spm_unit("spm_unit_is_married", period.this_year) + countable_resources = where( + married, + person.marital_unit.sum(personal_resources), + personal_resources, + ) asset_limit = where(married, p.asset.couple, p.asset.individual) - meets_asset_test = cash_assets <= asset_limit + meets_asset_test = countable_resources <= asset_limit return where(asset_test_applies, meets_asset_test, True) diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_eligible.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_eligible.py index 8e3a8b31650..bf41b5c0e90 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_eligible.py @@ -8,13 +8,9 @@ class msp_eligible(Variable): definition_period = MONTH reference = ( "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", - "https://www.law.cornell.edu/cfr/text/42/435.121", + "https://www.law.cornell.edu/uscode/text/42/1396d#p", ) def formula(person, period, parameters): - # Must be Medicare eligible - medicare_eligible = person("is_medicare_eligible", period.this_year) - income_eligible = person("msp_income_eligible", period) - asset_eligible = person("msp_asset_eligible", period) - - return medicare_eligible & income_eligible & asset_eligible + category = person("msp_category", period) + return category != category.possible_values.NONE diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_income_eligible.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_income_eligible.py index 9dcaf06e362..7f59caeba01 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_income_eligible.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/eligibility/msp_income_eligible.py @@ -8,7 +8,8 @@ class msp_income_eligible(Variable): definition_period = MONTH reference = ( "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", - "https://www.law.cornell.edu/cfr/text/42/435.121", + "https://www.law.cornell.edu/uscode/text/42/1396d#p", + "https://www.law.cornell.edu/cfr/text/42/435.125", ) def formula(person, period, parameters): @@ -21,4 +22,4 @@ def formula(person, period, parameters): # Use QI threshold (135% FPL) as the outer bound qi_income_limit = fpg * p.fpl_limit - return countable_income <= qi_income_limit + return countable_income < qi_income_limit diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_countable_income.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_countable_income.py index 7453671ebc5..73a0ce26fb3 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_countable_income.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_countable_income.py @@ -12,6 +12,7 @@ class msp_countable_income(Variable): definition_period = MONTH reference = ( "https://www.law.cornell.edu/uscode/text/42/1396d#p", + "https://www.law.cornell.edu/cfr/text/20/416.1163", "https://secure.ssa.gov/apps10/poms.nsf/lnx/0501715010", "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", ) @@ -31,29 +32,57 @@ class msp_countable_income(Variable): """ def formula(person, period, parameters): - # MSP uses SSI income methodology per 42 U.S.C. 1396d(p)(1)(B). - # When both spouses are Medicare-eligible, SSI couple rules apply: - # combine both incomes and apply the $20 exclusion once to the couple. - is_medicare_eligible = person("is_medicare_eligible", period.this_year) + year = period.this_year + earned_income = person("ssi_earned_income", year) + unearned_income = person("ssi_unearned_income", year) + is_medicare_eligible = person("is_medicare_eligible", year) both_medicare_eligible = person.marital_unit.sum(is_medicare_eligible) == 2 - earned = person("ssi_earned_income", period.this_year) - unearned = person("ssi_unearned_income", period.this_year) + blind_or_disabled_working_student_exclusion = person( + "ssi_blind_or_disabled_working_student_exclusion", year + ) + personal_earned_income = max_( + earned_income - blind_or_disabled_working_student_exclusion, + 0, + ) + deeming_applies = person("is_ssi_spousal_deeming_applies", year) - # Aggregate couple income when both spouses are Medicare-eligible - combined_earned = where( - both_medicare_eligible, - person.marital_unit.sum(earned), - earned, + spouse_earned_income = person( + "ssi_earned_income_deemed_from_ineligible_spouse", year + ) + spouse_unearned_income = person( + "ssi_unearned_income_deemed_from_ineligible_spouse", year ) - combined_unearned = where( - both_medicare_eligible, - person.marital_unit.sum(unearned), - unearned, + personal_countable_income = _apply_ssi_exclusions( + personal_earned_income, + unearned_income, + parameters, + year, + ) + + couple_countable = _apply_ssi_exclusions( + max_( + person.marital_unit.sum(earned_income) + - person.marital_unit.sum(blind_or_disabled_working_student_exclusion), + 0, + ), + person.marital_unit.sum(unearned_income), + parameters, + year, + ) + + deemed_countable = _apply_ssi_exclusions( + personal_earned_income + spouse_earned_income, + unearned_income + spouse_unearned_income, + parameters, + year, + ) + + single_countable = where( + deeming_applies, deemed_countable, personal_countable_income ) - # Apply SSI exclusions once to the (possibly combined) income - annual_countable = _apply_ssi_exclusions( - combined_earned, combined_unearned, parameters, period.this_year + annual_countable = where( + both_medicare_eligible, couple_countable, single_countable ) return annual_countable / MONTHS_IN_YEAR diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_fpg.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_fpg.py index 9e7ca20bed5..b1cb24efc20 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_fpg.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/income/msp_fpg.py @@ -29,7 +29,7 @@ def formula(person, period, parameters): # Note: These are raw FPL values. Published limits are $20 higher # because we apply the $20 exclusion to income (msp_countable_income) # rather than adding it to the threshold. - married = person.spm_unit("spm_unit_is_married", period) + married = person.spm_unit("spm_unit_is_married", period.this_year) state_group = person.household("state_group_str", period) p = parameters(period).gov.hhs.fpg p1 = p.first_person[state_group]