From 5f33d0be487c26ba49d2ad56239863d4fc280908 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:21:36 -0400 Subject: [PATCH 1/3] Include Medicare premiums in SPM MOOP --- changelog.d/fixed/8529.md | 1 + ...ut_of_pocket_expenses_medicare_part_b.yaml | 28 +++++++++++++++++++ .../spm_unit_health_insurance_premiums.py | 2 ++ ...spm_unit_medical_out_of_pocket_expenses.py | 8 +++--- 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fixed/8529.md diff --git a/changelog.d/fixed/8529.md b/changelog.d/fixed/8529.md new file mode 100644 index 00000000000..386be8aa63c --- /dev/null +++ b/changelog.d/fixed/8529.md @@ -0,0 +1 @@ +- Included paid Medicare Part A premiums and Medicare Part D IRMAA surcharges in SPM-unit health insurance premiums and medical out-of-pocket expenses. 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..59ca21bc557 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 @@ -6,6 +6,7 @@ age: 70 is_medicare_eligible: true other_health_insurance_premiums: 500 + base_part_a_premium: 0 medicare_part_b_premium: 2_220 tax_units: tax_unit: @@ -29,6 +30,7 @@ age: 70 is_medicare_eligible: true other_health_insurance_premiums: 500 + base_part_a_premium: 0 medicare_part_b_premium: 5_028 tax_units: tax_unit: @@ -43,3 +45,29 @@ 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 and Part D IRMAA premiums. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + retiree: + age: 70 + is_medicare_eligible: true + other_health_insurance_premiums: 500 + base_part_a_premium: 6_216 + medicare_part_b_premium: 2_220 + income_adjusted_part_d_premium_surcharge: 423.6 + tax_units: + tax_unit: + members: [retiree] + spm_units: + spm_unit: + members: [retiree] + households: + household: + members: [retiree] + state_code: CA + output: + spm_unit_health_insurance_premiums: 9_359.6 + spm_unit_medical_out_of_pocket_expenses: 9_359.6 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..d7a5d1618e1 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 @@ -18,5 +18,7 @@ class spm_unit_health_insurance_premiums(Variable): "chip_premium", "medicaid_premium", "marketplace_net_premium", + "base_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..10e8794ec49 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, Medicare Part A, " + "Medicare Part B net of Medicare Savings Program coverage, and " + "Medicare Part D IRMAA premiums. Non-premium expenses include other " + "medical expenses and over-the-counter health expenses." ) adds = [ From d4e7487057d3e80ce55dcebe0a4dd1705feab2e1 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:09:01 -0400 Subject: [PATCH 2/3] Fix changelog fragment path --- changelog.d/{fixed/8529.md => 8529.fixed.md} | 0 1 file changed, 0 insertions(+), 0 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 From 6e5383da64669858f9599a3aa488082acfe219b4 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:46:45 -0400 Subject: [PATCH 3/3] Net Medicare Part A premiums in SPM MOOP --- .../msp_part_a_premium_coverage.yaml | 19 ++++++++++++ ...ut_of_pocket_expenses_medicare_part_b.yaml | 29 ++++++++++++++++++ .../part_a/medicare_part_a_premium.py | 22 ++++++++++++++ .../msp_part_a_premium_coverage.py | 30 +++++++++++++++++++ .../spm_unit_health_insurance_premiums.py | 2 +- ...spm_unit_medical_out_of_pocket_expenses.py | 8 ++--- 6 files changed, 105 insertions(+), 5 deletions(-) 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/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..b72c2f4aa43 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.yaml @@ -0,0 +1,19 @@ +- name: MSP Part A coverage pays the base premium for QMB enrollees. + period: 2025 + input: + medicare_enrolled: true + is_qmb_eligible: true + base_part_a_premium: 6_216 + output: + msp_part_a_premium_coverage: 6_216 + medicare_part_a_premium: 0 + +- name: MSP Part A coverage is zero for non-QMB enrollees. + period: 2025 + input: + medicare_enrolled: true + is_qmb_eligible: false + base_part_a_premium: 6_216 + output: + msp_part_a_premium_coverage: 0 + medicare_part_a_premium: 6_216 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 59ca21bc557..b2a348b97c9 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 @@ -54,6 +54,7 @@ retiree: age: 70 is_medicare_eligible: true + is_qmb_eligible: false other_health_insurance_premiums: 500 base_part_a_premium: 6_216 medicare_part_b_premium: 2_220 @@ -71,3 +72,31 @@ output: spm_unit_health_insurance_premiums: 9_359.6 spm_unit_medical_out_of_pocket_expenses: 9_359.6 + +- name: SPM unit MOOP excludes Part A premiums covered by MSP. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + retiree: + age: 70 + is_medicare_eligible: true + other_health_insurance_premiums: 500 + base_part_a_premium: 6_216 + msp_part_a_premium_coverage: 6_216 + medicare_part_b_premium: 2_220 + income_adjusted_part_d_premium_surcharge: 423.6 + 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: 3_143.6 + spm_unit_medical_out_of_pocket_expenses: 3_143.6 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..ef4dadb42b4 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_a/medicare_part_a_premium.py @@ -0,0 +1,22 @@ +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/help/medicare-savings-programs", + ) + documentation = ( + "Annual Medicare Part A premium paid out of pocket by the enrollee, " + "net of Medicare Savings Program coverage." + ) + + def formula(person, period, parameters): + gross_premium = person("base_part_a_premium", period) + msp_coverage = person("msp_part_a_premium_coverage", period) + return max_(gross_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..4fba23f2b0d --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_a_premium_coverage.py @@ -0,0 +1,30 @@ +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", + "https://www.law.cornell.edu/uscode/text/42/1396d#p_3", + ) + documentation = ( + "Annual Part A premium amount paid on the enrollee's behalf through " + "the Qualified Medicare Beneficiary pathway." + ) + + 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 d7a5d1618e1..1ace570cf4c 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 @@ -18,7 +18,7 @@ class spm_unit_health_insurance_premiums(Variable): "chip_premium", "medicaid_premium", "marketplace_net_premium", - "base_part_a_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 10e8794ec49..9b749046757 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, Medicare Part A, " - "Medicare Part B net of Medicare Savings Program coverage, and " - "Medicare Part D IRMAA premiums. Non-premium expenses include other " - "medical expenses and over-the-counter health expenses." + "premiums plus modeled Marketplace, CHIP, Medicaid, Medicare Part A " + "and Part B net of Medicare Savings Program coverage, and Medicare " + "Part D IRMAA premiums. Non-premium expenses include other medical " + "expenses and over-the-counter health expenses." ) adds = [