From 2ab4c3a4d7ab7959d9f51f1fbf5bc6b5ae16f2ad Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Wed, 27 May 2026 16:52:50 -0400 Subject: [PATCH 1/3] Include Medicare Part A and Part D premiums in MOOP --- changelog.d/fixed/8529.md | 1 + .../eligibility/medicare_part_a_premium.yaml | 26 ++++++++++ .../msp_part_a_premium_coverage.yaml | 26 ++++++++++ ...ut_of_pocket_expenses_medicare_part_b.yaml | 52 +++++++++++++++++++ .../part_a/medicare_part_a_premium.py | 20 +++++++ .../msp_part_a_premium_coverage.py | 31 +++++++++++ .../spm_unit_health_insurance_premiums.py | 5 +- ...spm_unit_medical_out_of_pocket_expenses.py | 8 +-- 8 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fixed/8529.md create mode 100644 policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_a_premium.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.yaml create mode 100644 policyengine_us/variables/gov/hhs/medicare/eligibility/part_a/medicare_part_a_premium.py create mode 100644 policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.py diff --git a/changelog.d/fixed/8529.md b/changelog.d/fixed/8529.md new file mode 100644 index 00000000000..ffcf3346cbf --- /dev/null +++ b/changelog.d/fixed/8529.md @@ -0,0 +1 @@ +Include paid Medicare Part A premiums and Part D IRMAA in SPM medical out-of-pocket expenses. diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_a_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_a_premium.yaml new file mode 100644 index 00000000000..ec1c29f2589 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_a_premium.yaml @@ -0,0 +1,26 @@ +- name: Medicare Part A premium equals base premium when no MSP coverage. + period: 2026 + input: + medicare_enrolled: true + base_part_a_premium: 6_780 + msp_part_a_premium_coverage: 0 + output: + medicare_part_a_premium: 6_780 + +- name: Medicare Part A premium is net of MSP coverage. + period: 2026 + input: + medicare_enrolled: true + base_part_a_premium: 6_780 + msp_part_a_premium_coverage: 6_780 + output: + medicare_part_a_premium: 0 + +- name: Medicare Part A premium floors at zero after MSP coverage. + period: 2026 + input: + medicare_enrolled: true + base_part_a_premium: 6_780 + msp_part_a_premium_coverage: 7_000 + output: + medicare_part_a_premium: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.yaml new file mode 100644 index 00000000000..6a7ece615b0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.yaml @@ -0,0 +1,26 @@ +- name: MSP Part A coverage pays the Part A premium for QMB enrollees. + period: 2026 + input: + medicare_enrolled: true + is_qmb_eligible: true + base_part_a_premium: 6_780 + output: + msp_part_a_premium_coverage: 6_780 + +- name: MSP Part A coverage is zero for non-QMB enrollees. + period: 2026 + input: + medicare_enrolled: true + is_qmb_eligible: false + base_part_a_premium: 6_780 + output: + msp_part_a_premium_coverage: 0 + +- name: MSP Part A coverage is zero when not enrolled. + period: 2026 + input: + medicare_enrolled: false + is_qmb_eligible: true + base_part_a_premium: 6_780 + output: + msp_part_a_premium_coverage: 0 diff --git a/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses_medicare_part_b.yaml b/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses_medicare_part_b.yaml index fcdc9838348..b73055cd7e7 100644 --- a/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses_medicare_part_b.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses_medicare_part_b.yaml @@ -43,3 +43,55 @@ output: spm_unit_health_insurance_premiums: 5_528 spm_unit_medical_out_of_pocket_expenses: 5_528 + +- name: SPM unit MOOP includes paid Part A premium and Part D IRMAA. + period: 2026 + input: + people: + retiree: + age: 70 + medicare_enrolled: true + other_health_insurance_premiums: 500 + base_part_a_premium: 6_780 + msp_part_a_premium_coverage: 0 + medicare_part_b_premium: 4_654.80 + income_adjusted_part_d_premium_surcharge: 423.60 + tax_units: + tax_unit: + members: [retiree] + spm_units: + spm_unit: + members: [retiree] + households: + household: + members: [retiree] + state_code: CA + output: + medicare_part_a_premium: 6_780 + spm_unit_health_insurance_premiums: 12_358.40 + spm_unit_medical_out_of_pocket_expenses: 12_358.40 + +- name: SPM unit MOOP excludes QMB-covered Part A premium. + period: 2026 + input: + people: + retiree: + age: 70 + medicare_enrolled: true + base_part_a_premium: 6_780 + msp_part_a_premium_coverage: 6_780 + medicare_part_b_premium: 2_434.80 + tax_units: + tax_unit: + members: [retiree] + spm_units: + spm_unit: + members: [retiree] + households: + household: + members: [retiree] + state_code: CA + output: + medicare_part_a_premium: 0 + spm_unit_health_insurance_premiums: 2_434.80 + spm_unit_medical_out_of_pocket_expenses: 2_434.80 diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_a/medicare_part_a_premium.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_a/medicare_part_a_premium.py new file mode 100644 index 00000000000..d190322ee2c --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_a/medicare_part_a_premium.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * + + +class medicare_part_a_premium(Variable): + value_type = float + entity = Person + label = "Medicare Part A premium" + unit = USD + definition_period = YEAR + defined_for = "medicare_enrolled" + reference = "https://www.medicare.gov/basics/costs/medicare-costs" + documentation = ( + "Annual Medicare Part A premium paid out of pocket by the enrollee, " + "net of Medicare Savings Program coverage." + ) + + def formula(person, period, parameters): + base_premium = person("base_part_a_premium", period) + msp_coverage = person("msp_part_a_premium_coverage", period) + return max_(base_premium - msp_coverage, 0) diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.py new file mode 100644 index 00000000000..37d42fe198d --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * + + +class msp_part_a_premium_coverage(Variable): + value_type = float + entity = Person + unit = USD + label = "Medicare Part A premium amount covered by MSP" + definition_period = YEAR + reference = ( + "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", + ) + documentation = ( + "Annual Part A premium amount paid on the enrollee's behalf through " + "Qualified Medicare Beneficiary coverage." + ) + + def formula(person, period, parameters): + enrolled = person("medicare_enrolled", period) + monthly_part_a_premium = ( + person("base_part_a_premium", period) / MONTHS_IN_YEAR + ) + monthly_coverage = 0 + for month in period.get_subperiods(MONTH): + qmb_eligible = person("is_qmb_eligible", month) + monthly_coverage += where( + enrolled & qmb_eligible, + monthly_part_a_premium, + 0, + ) + return monthly_coverage diff --git a/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py b/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py index 22949382701..3ae1e153899 100644 --- a/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py +++ b/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py @@ -10,7 +10,8 @@ class spm_unit_health_insurance_premiums(Variable): documentation = ( "Health insurance premium expenses for an SPM unit, combining a " "data-imputed other premium component with modeled premium components " - "that can respond to policy reforms." + "that can respond to policy reforms, including Medicare premiums " + "paid out of pocket." ) adds = [ @@ -18,5 +19,7 @@ class spm_unit_health_insurance_premiums(Variable): "chip_premium", "medicaid_premium", "marketplace_net_premium", + "medicare_part_a_premium", "medicare_part_b_premium", + "income_adjusted_part_d_premium_surcharge", ] diff --git a/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py b/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py index f54a75562af..9ea3e17a124 100644 --- a/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py +++ b/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py @@ -11,10 +11,10 @@ class spm_unit_medical_out_of_pocket_expenses(Variable): "Total medical out-of-pocket expenses at the SPM unit level, " "combining health insurance premiums with non-premium medical " "expenses. Health insurance premiums include other health insurance " - "premiums plus modeled Marketplace, CHIP, Medicaid, and Medicare Part " - "B premiums net of Medicare Savings Program coverage. Non-premium " - "expenses include other medical expenses and over-the-counter health " - "expenses." + "premiums plus modeled Marketplace, CHIP, Medicaid, and Medicare " + "premiums net of Medicare Savings Program coverage where modeled. " + "Non-premium expenses include other medical expenses and " + "over-the-counter health expenses." ) adds = [ From 3aff88b9fb15d5716add80fe5127ceb48d1178d0 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Thu, 28 May 2026 10:24:54 -0400 Subject: [PATCH 2/3] Fix lint and changelog fragment --- changelog.d/{fixed/8529.md => 8529.fixed.md} | 0 .../medicare/savings_programs/msp_part_a_premium_coverage.py | 4 +--- 2 files changed, 1 insertion(+), 3 deletions(-) rename changelog.d/{fixed/8529.md => 8529.fixed.md} (100%) diff --git a/changelog.d/fixed/8529.md b/changelog.d/8529.fixed.md similarity index 100% rename from changelog.d/fixed/8529.md rename to changelog.d/8529.fixed.md diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.py index 37d42fe198d..988e617f37f 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.py @@ -17,9 +17,7 @@ class msp_part_a_premium_coverage(Variable): def formula(person, period, parameters): enrolled = person("medicare_enrolled", period) - monthly_part_a_premium = ( - person("base_part_a_premium", period) / MONTHS_IN_YEAR - ) + monthly_part_a_premium = person("base_part_a_premium", period) / MONTHS_IN_YEAR monthly_coverage = 0 for month in period.get_subperiods(MONTH): qmb_eligible = person("is_qmb_eligible", month) From 44c1b50434acf8a1dc175e0fd15c385a5b83b0f9 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Fri, 29 May 2026 16:11:03 -0400 Subject: [PATCH 3/3] Clarify Medicare premium MOOP docs --- .../income/spm_unit/spm_unit_health_insurance_premiums.py | 4 ++-- .../spm_unit/spm_unit_medical_out_of_pocket_expenses.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py b/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py index 3ae1e153899..6be89db1ae8 100644 --- a/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py +++ b/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py @@ -10,8 +10,8 @@ class spm_unit_health_insurance_premiums(Variable): documentation = ( "Health insurance premium expenses for an SPM unit, combining a " "data-imputed other premium component with modeled premium components " - "that can respond to policy reforms, including Medicare premiums " - "paid out of pocket." + "that can respond to policy reforms, including Medicare Part A and " + "Part B premiums and the Part D IRMAA surcharge paid out of pocket." ) adds = [ diff --git a/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py b/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py index 9ea3e17a124..f06025f553f 100644 --- a/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py +++ b/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py @@ -12,7 +12,8 @@ class spm_unit_medical_out_of_pocket_expenses(Variable): "combining health insurance premiums with non-premium medical " "expenses. Health insurance premiums include other health insurance " "premiums plus modeled Marketplace, CHIP, Medicaid, and Medicare " - "premiums net of Medicare Savings Program coverage where modeled. " + "Part A and Part B premiums net of Medicare Savings Program coverage " + "where modeled, plus the Part D IRMAA surcharge. " "Non-premium expenses include other medical expenses and " "over-the-counter health expenses." )