diff --git a/changelog.d/codex-medicare-partb-msp-clean.fixed.md b/changelog.d/codex-medicare-partb-msp-clean.fixed.md new file mode 100644 index 00000000000..b41d35b01e1 --- /dev/null +++ b/changelog.d/codex-medicare-partb-msp-clean.fixed.md @@ -0,0 +1 @@ +Net Medicare Part B premiums in baseline SPM expenses against cycle-free Medicare Savings Program coverage, preserve reported premiums as an audit input, and stop treating reported health insurance premiums as pre-tax wage deductions in federal AGI. diff --git a/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/applies.yaml b/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/applies.yaml index d306f048d9d..4042920630b 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/applies.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/savings_programs/eligibility/asset/applies.yaml @@ -10,106 +10,158 @@ metadata: href: https://www.medicare.gov/basics/costs/help/medicare-savings-programs - title: Medicare Interactive - Medicare Savings Program Income and Asset Limits href: https://www.medicareinteractive.org/understanding-medicare/cost-saving-programs/medicare-savings-programs-qmb-slmb-qi/medicare-savings-program-income-and-asset-limits -# States that have eliminated the asset test are set to false +# Federal MSP rules apply the asset test by default; states that have +# eliminated it in current law are set to false from 2024. AL: + 0000-01-01: true 2024-01-01: false AK: + 0000-01-01: true 2024-01-01: true AZ: + 0000-01-01: true 2024-01-01: false AR: + 0000-01-01: true 2024-01-01: true CA: + 0000-01-01: true 2024-01-01: false CO: + 0000-01-01: true 2024-01-01: true CT: + 0000-01-01: true 2024-01-01: false DE: + 0000-01-01: true 2024-01-01: false DC: + 0000-01-01: true 2024-01-01: false FL: + 0000-01-01: true 2024-01-01: true GA: + 0000-01-01: true 2024-01-01: true HI: + 0000-01-01: true 2024-01-01: true ID: + 0000-01-01: true 2024-01-01: true IL: + 0000-01-01: true 2024-01-01: true IN: + 0000-01-01: true 2024-01-01: true IA: + 0000-01-01: true 2024-01-01: true KS: + 0000-01-01: true 2024-01-01: true KY: + 0000-01-01: true 2024-01-01: true LA: + 0000-01-01: true 2024-01-01: false ME: + 0000-01-01: true 2024-01-01: true MD: + 0000-01-01: true 2024-01-01: true MA: + 0000-01-01: true 2024-01-01: true MI: + 0000-01-01: true 2024-01-01: true MN: + 0000-01-01: true 2024-01-01: true MS: + 0000-01-01: true 2024-01-01: false MO: + 0000-01-01: true 2024-01-01: true MT: + 0000-01-01: true 2024-01-01: true NE: + 0000-01-01: true 2024-01-01: true NV: + 0000-01-01: true 2024-01-01: true NH: + 0000-01-01: true 2024-01-01: true NJ: + 0000-01-01: true 2024-01-01: true NM: + 0000-01-01: true 2024-01-01: false NY: + 0000-01-01: true 2024-01-01: false NC: + 0000-01-01: true 2024-01-01: true ND: + 0000-01-01: true 2024-01-01: true OH: + 0000-01-01: true 2024-01-01: true OK: + 0000-01-01: true 2024-01-01: true OR: + 0000-01-01: true 2024-01-01: false PA: + 0000-01-01: true 2024-01-01: true RI: + 0000-01-01: true 2024-01-01: true SC: + 0000-01-01: true 2024-01-01: true SD: + 0000-01-01: true 2024-01-01: true TN: + 0000-01-01: true 2024-01-01: true TX: + 0000-01-01: true 2024-01-01: true UT: + 0000-01-01: true 2024-01-01: true VT: + 0000-01-01: true 2024-01-01: false VA: + 0000-01-01: true 2024-01-01: true WA: + 0000-01-01: true 2024-01-01: true WV: + 0000-01-01: true 2024-01-01: true WI: + 0000-01-01: true 2024-01-01: true WY: + 0000-01-01: true 2024-01-01: true 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 8d5e5ce2a0e..b6c246e4f73 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 @@ -1,5 +1,18 @@ description: The Department of Health and Human Services limits resources to this amount for couples under the Medicare Savings Programs. values: + 0000-01-01: 9_910 + 2011-01-01: 10_020 + 2012-01-01: 10_410 + 2013-01-01: 10_620 + 2014-01-01: 10_750 + 2015-01-01: 10_930 + 2017-01-01: 11_090 + 2018-01-01: 11_340 + 2019-01-01: 11_600 + 2020-01-01: 11_800 + 2021-01-01: 11_960 + 2022-01-01: 12_600 + 2023-01-01: 13_630 2024-01-01: 14_130 2025-01-01: 14_470 2026-01-01: 14_910 @@ -13,3 +26,7 @@ metadata: href: https://www.medicare.gov/basics/costs/help/medicare-savings-programs - title: SSA POMS SI 01715.010 - Resource Eligibility href: https://secure.ssa.gov/apps10/poms.nsf/lnx/0501715010 + - title: "CMS SMDL #10-003: 2010 Medicare Savings Program resource standards" + href: https://downloads.cms.gov/cmsgov/archived-downloads/smdl/downloads/smd10003.pdf + - title: Missouri DSS Historical QMB Resource Limits + href: https://dss.mo.gov/fsd/iman/medasst/0865-010-15.html 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 5787d7e2255..baedf21f20f 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 @@ -1,5 +1,18 @@ description: The Department of Health and Human Services limits resources to this amount for individuals under the Medicare Savings Programs. values: + 0000-01-01: 6_600 + 2011-01-01: 6_680 + 2012-01-01: 6_940 + 2013-01-01: 7_080 + 2014-01-01: 7_160 + 2015-01-01: 7_280 + 2017-01-01: 7_390 + 2018-01-01: 7_560 + 2019-01-01: 7_730 + 2020-01-01: 7_860 + 2021-01-01: 7_970 + 2022-01-01: 8_400 + 2023-01-01: 9_090 2024-01-01: 9_430 2025-01-01: 9_660 2026-01-01: 9_950 @@ -13,3 +26,7 @@ metadata: href: https://www.medicare.gov/basics/costs/help/medicare-savings-programs - title: SSA POMS SI 01715.010 - Resource Eligibility href: https://secure.ssa.gov/apps10/poms.nsf/lnx/0501715010 + - title: "CMS SMDL #10-003: 2010 Medicare Savings Program resource standards" + href: https://downloads.cms.gov/cmsgov/archived-downloads/smdl/downloads/smd10003.pdf + - title: Missouri DSS Historical QMB Resource Limits + href: https://dss.mo.gov/fsd/iman/medasst/0865-010-15.html diff --git a/policyengine_us/parameters/gov/irs/gross_income/pre_tax_contributions.yaml b/policyengine_us/parameters/gov/irs/gross_income/pre_tax_contributions.yaml index 07771735119..9caa7374de8 100644 --- a/policyengine_us/parameters/gov/irs/gross_income/pre_tax_contributions.yaml +++ b/policyengine_us/parameters/gov/irs/gross_income/pre_tax_contributions.yaml @@ -4,8 +4,8 @@ values: # Assumes all are pre-tax. - traditional_401k_contributions - traditional_403b_contributions - # Assumes employer for now. - - health_insurance_premiums + # Only explicit pre-tax payroll health premiums reduce taxable wages. + - pre_tax_health_insurance_premiums # HSA contributions can be either through pre-tax. - health_savings_account_payroll_contributions metadata: diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml index 66f300d8f3c..43bad78d187 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml @@ -71,6 +71,6 @@ is_medicare_eligible: true medicare_enrolled: true medicare_quarters_of_coverage: 40 - medicare_part_b_premium: 3_000 + gross_medicare_part_b_premium: 3_000 output: medicare_cost: 11_500 # $14,500 spending - $3,000 Part B premium diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml index aba17915046..ebce76aadfe 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml @@ -8,6 +8,8 @@ tax_exempt_interest_income: 2023: 0 is_medicare_eligible: true + msp_income_eligible: false + msp_asset_eligible: false output: medicare_part_b_premium: 2_220 # $185 * 12 months @@ -21,6 +23,8 @@ tax_exempt_interest_income: 2023: 0 is_medicare_eligible: true + msp_income_eligible: false + msp_asset_eligible: false output: medicare_part_b_premium: 4_440 # $370 * 12 months @@ -34,6 +38,8 @@ tax_exempt_interest_income: 2023: 0 is_medicare_eligible: true + msp_income_eligible: false + msp_asset_eligible: false output: medicare_part_b_premium: 7_546.8 # $628.90 * 12 months @@ -47,6 +53,8 @@ tax_exempt_interest_income: 2023: 0 is_medicare_eligible: true + msp_income_eligible: false + msp_asset_eligible: false output: medicare_part_b_premium: 3_108 # $259 * 12 months @@ -60,6 +68,8 @@ tax_exempt_interest_income: 2023: 0 is_medicare_eligible: true + msp_income_eligible: false + msp_asset_eligible: false output: medicare_part_b_premium: 2_220 # $185 * 12 months @@ -73,5 +83,33 @@ tax_exempt_interest_income: 2023: 0 is_medicare_eligible: true + msp_income_eligible: false + msp_asset_eligible: false output: medicare_part_b_premium: 7_546.8 # $628.90 * 12 months + +- name: MSP coverage fully offsets standard Part B premium + period: 2025 + input: + age: 65 + is_medicare_eligible: true + medicare_enrolled: true + gross_medicare_part_b_premium: 2_220 + base_part_b_premium: 2_220 + msp_income_eligible: true + msp_asset_eligible: true + output: + medicare_part_b_premium: 0 + +- name: MSP coverage preserves IRMAA above the standard Part B premium + period: 2025 + input: + age: 65 + is_medicare_eligible: true + medicare_enrolled: true + gross_medicare_part_b_premium: 4_440 + base_part_b_premium: 2_220 + msp_income_eligible: true + msp_asset_eligible: true + output: + medicare_part_b_premium: 2_220 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml new file mode 100644 index 00000000000..0c450945ee9 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml @@ -0,0 +1,29 @@ +- name: MSP part B coverage pays the standard premium for income- and asset-eligible enrollees + period: 2025 + input: + medicare_enrolled: true + msp_income_eligible: true + msp_asset_eligible: true + base_part_b_premium: 2_220 + output: + msp_part_b_premium_coverage: 2_220 + +- name: MSP part B coverage is zero for ineligible enrollees + period: 2025 + input: + medicare_enrolled: true + msp_income_eligible: false + msp_asset_eligible: true + base_part_b_premium: 2_220 + output: + msp_part_b_premium_coverage: 0 + +- name: MSP part B coverage is zero when not enrolled + period: 2025 + input: + medicare_enrolled: false + msp_income_eligible: true + msp_asset_eligible: true + base_part_b_premium: 2_220 + output: + msp_part_b_premium_coverage: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py new file mode 100644 index 00000000000..55cd5a8410a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py @@ -0,0 +1,294 @@ +import pytest + +from policyengine_us import CountryTaxBenefitSystem, Simulation + + +SYSTEM = CountryTaxBenefitSystem() +PERIOD = "2025" + + +def make_simulation( + *, + medicare_enrolled: bool, + gross_part_b_premium: float, + base_part_b_premium: float, + msp_income_eligible: bool, + msp_asset_eligible: bool, +) -> Simulation: + return Simulation( + tax_benefit_system=SYSTEM, + situation={ + "people": { + "person": { + "age": {PERIOD: 65}, + "is_medicare_eligible": {PERIOD: True}, + "medicare_enrolled": {PERIOD: medicare_enrolled}, + "medicare_quarters_of_coverage": {PERIOD: 40}, + "gross_medicare_part_b_premium": {PERIOD: gross_part_b_premium}, + "base_part_b_premium": {PERIOD: base_part_b_premium}, + "msp_income_eligible": {f"{PERIOD}-01": msp_income_eligible}, + "msp_asset_eligible": {f"{PERIOD}-01": msp_asset_eligible}, + } + }, + "households": {"household": {"members": ["person"]}}, + "tax_units": {"tax_unit": {"members": ["person"]}}, + "spm_units": {"spm_unit": {"members": ["person"]}}, + "families": {"family": {"members": ["person"]}}, + "marital_units": {"marital_unit": {"members": ["person"]}}, + }, + ) + + +def test_msp_part_b_premium_coverage_pays_standard_premium(): + sim = make_simulation( + medicare_enrolled=True, + gross_part_b_premium=4_440, + base_part_b_premium=2_220, + msp_income_eligible=True, + msp_asset_eligible=True, + ) + + assert sim.calculate("msp_part_b_premium_coverage", PERIOD)[0] == pytest.approx( + 2_220 + ) + + +def test_medicare_part_b_premium_preserves_only_irmaa_above_msp_support(): + sim = make_simulation( + medicare_enrolled=True, + gross_part_b_premium=4_440, + base_part_b_premium=2_220, + msp_income_eligible=True, + msp_asset_eligible=True, + ) + + assert sim.calculate("medicare_part_b_premium", PERIOD)[0] == pytest.approx(2_220) + + +def test_medicare_part_b_premium_is_zero_when_msp_covers_standard_premium(): + sim = make_simulation( + medicare_enrolled=True, + gross_part_b_premium=2_220, + base_part_b_premium=2_220, + msp_income_eligible=True, + msp_asset_eligible=True, + ) + + assert sim.calculate("medicare_part_b_premium", PERIOD)[0] == pytest.approx(0) + + +def test_medicare_cost_uses_gross_part_b_before_msp_offset(): + sim = make_simulation( + medicare_enrolled=True, + gross_part_b_premium=2_220, + base_part_b_premium=2_220, + msp_income_eligible=True, + msp_asset_eligible=True, + ) + + assert sim.calculate("medicare_part_b_premium", PERIOD)[0] == pytest.approx(0) + assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(12_280) + + +def test_medicare_part_b_premium_is_zero_when_not_enrolled(): + sim = make_simulation( + medicare_enrolled=False, + gross_part_b_premium=2_220, + base_part_b_premium=2_220, + msp_income_eligible=True, + msp_asset_eligible=True, + ) + + assert sim.calculate("msp_part_b_premium_coverage", PERIOD)[0] == pytest.approx(0) + assert sim.calculate("medicare_part_b_premium", PERIOD)[0] == pytest.approx(0) + + +def test_msp_part_b_premium_coverage_scales_with_eligible_months(): + monthly_eligibility = { + f"{PERIOD}-{month:02d}": month <= 3 for month in range(1, 13) + } + sim = Simulation( + tax_benefit_system=SYSTEM, + situation={ + "people": { + "person": { + "age": {PERIOD: 65}, + "is_medicare_eligible": {PERIOD: True}, + "medicare_enrolled": {PERIOD: True}, + "base_part_b_premium": {PERIOD: 2_220}, + "msp_income_eligible": monthly_eligibility, + "msp_asset_eligible": monthly_eligibility, + } + }, + "households": {"household": {"members": ["person"]}}, + "tax_units": {"tax_unit": {"members": ["person"]}}, + "spm_units": {"spm_unit": {"members": ["person"]}}, + "families": {"family": {"members": ["person"]}}, + "marital_units": {"marital_unit": {"members": ["person"]}}, + }, + ) + + assert sim.calculate("msp_part_b_premium_coverage", PERIOD)[0] == pytest.approx( + 555, + abs=1e-6, + ) + + +def test_historical_msp_asset_eligibility_uses_federal_default(): + sim = Simulation( + tax_benefit_system=SYSTEM, + situation={ + "people": {"person": {"ssi_countable_resources": {"2014": 0}}}, + "households": {"household": {"members": ["person"], "state_code": "AR"}}, + "tax_units": {"tax_unit": {"members": ["person"]}}, + "spm_units": {"spm_unit": {"members": ["person"]}}, + "families": {"family": {"members": ["person"]}}, + "marital_units": {"marital_unit": {"members": ["person"]}}, + }, + ) + + assert sim.calculate("msp_asset_eligible", "2014-01")[0] + + +def test_historical_msp_asset_limits_before_2013(): + sim = Simulation( + tax_benefit_system=SYSTEM, + situation={ + "people": { + "below_limit": {"ssi_countable_resources": {"2012": 6_940}}, + "above_limit": {"ssi_countable_resources": {"2012": 7_000}}, + "spouse_1": {"ssi_countable_resources": {"2012": 5_000}}, + "spouse_2": {"ssi_countable_resources": {"2012": 5_500}}, + }, + "households": { + "household": { + "members": [ + "below_limit", + "above_limit", + "spouse_1", + "spouse_2", + ], + "state_code": "AR", + } + }, + "tax_units": { + "tax_unit_1": {"members": ["below_limit"]}, + "tax_unit_2": {"members": ["above_limit"]}, + "tax_unit_3": {"members": ["spouse_1", "spouse_2"]}, + }, + "spm_units": { + "spm_unit_1": {"members": ["below_limit"]}, + "spm_unit_2": {"members": ["above_limit"]}, + "spm_unit_3": {"members": ["spouse_1", "spouse_2"]}, + }, + "families": { + "family_1": {"members": ["below_limit"]}, + "family_2": {"members": ["above_limit"]}, + "family_3": {"members": ["spouse_1", "spouse_2"]}, + }, + "marital_units": { + "marital_unit_1": {"members": ["below_limit"]}, + "marital_unit_2": {"members": ["above_limit"]}, + "marital_unit_3": {"members": ["spouse_1", "spouse_2"]}, + }, + }, + ) + + assert sim.calculate("msp_asset_eligible", "2012-01").tolist() == [ + True, + False, + False, + False, + ] + + +def test_medicare_part_b_premium_does_not_depend_on_calculation_order(): + no_msp_eligibility = { + f"{year}-{month:02d}": False + for year in ("2025", "2026") + for month in range(1, 13) + } + situation = { + "people": { + "person": { + "age": {"2025": 65, "2026": 66}, + "is_medicare_eligible": {"2025": True, "2026": True}, + "medicare_enrolled": {"2025": True, "2026": True}, + "gross_medicare_part_b_premium": {"2025": 2_220, "2026": 2_220}, + "base_part_b_premium": {"2025": 2_220, "2026": 2_220}, + "msp_income_eligible": no_msp_eligibility, + "msp_asset_eligible": no_msp_eligibility, + } + }, + "households": {"household": {"members": ["person"]}}, + "tax_units": {"tax_unit": {"members": ["person"]}}, + "spm_units": {"spm_unit": {"members": ["person"]}}, + "families": {"family": {"members": ["person"]}}, + "marital_units": {"marital_unit": {"members": ["person"]}}, + } + + ordered_sim = Simulation(tax_benefit_system=SYSTEM, situation=situation) + ordered_sim.calculate("medicare_part_b_premium", "2025") + ordered_result = ordered_sim.calculate("medicare_part_b_premium", "2026")[0] + + fresh_sim = Simulation(tax_benefit_system=SYSTEM, situation=situation) + fresh_result = fresh_sim.calculate("medicare_part_b_premium", "2026")[0] + + assert ordered_result == pytest.approx(fresh_result) + assert ordered_result == pytest.approx(2_220) + + +def test_gross_medicare_part_b_premium_handles_direct_filing_status_inputs(): + sim = Simulation( + tax_benefit_system=SYSTEM, + situation={ + "people": { + "person_1": { + "age": {PERIOD: 65}, + "base_part_b_premium": {PERIOD: 2_220}, + "is_medicare_eligible": {PERIOD: True}, + "tax_exempt_interest_income": {"2023": 0}, + }, + "person_2": { + "age": {PERIOD: 65}, + "base_part_b_premium": {PERIOD: 2_220}, + "is_medicare_eligible": {PERIOD: True}, + "tax_exempt_interest_income": {"2023": 0}, + }, + "person_3": { + "age": {PERIOD: 65}, + "base_part_b_premium": {PERIOD: 2_220}, + "is_medicare_eligible": {PERIOD: True}, + "tax_exempt_interest_income": {"2023": 0}, + }, + }, + "households": { + "household": {"members": ["person_1", "person_2", "person_3"]} + }, + "tax_units": { + "joint_tax_unit": { + "members": ["person_1", "person_2"], + "filing_status": {PERIOD: "JOINT"}, + "adjusted_gross_income": {"2023": 1_000_000}, + }, + "single_tax_unit": { + "members": ["person_3"], + "filing_status": {PERIOD: "SINGLE"}, + "adjusted_gross_income": {"2023": 50_000}, + }, + }, + "spm_units": { + "spm_unit": {"members": ["person_1", "person_2", "person_3"]} + }, + "families": {"family": {"members": ["person_1", "person_2", "person_3"]}}, + "marital_units": { + "marital_unit_1": {"members": ["person_1", "person_2"]}, + "marital_unit_2": {"members": ["person_3"]}, + }, + }, + ) + + result = sim.calculate("gross_medicare_part_b_premium", PERIOD) + assert result[0] == pytest.approx(7_546.8) + assert result[1] == pytest.approx(7_546.8) + assert result[2] == pytest.approx(2_220) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/irs_employment_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/irs_employment_income.yaml index 1f23a615716..06f2f3c5a7c 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/irs_employment_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/irs_employment_income.yaml @@ -14,3 +14,32 @@ output: irs_employment_income: 70 +- name: Health insurance premiums do not reduce IRS employment income. + period: 2024 + input: + employment_income: 100 + health_insurance_premiums_without_medicare_part_b: 20 + medicare_part_b_premium: 30 + output: + pre_tax_contributions: 0 + irs_employment_income: 100 + +- name: Explicit pre-tax health insurance premiums reduce IRS employment income. + period: 2024 + input: + employment_income: 100 + pre_tax_health_insurance_premiums: 20 + output: + pre_tax_contributions: 20 + irs_employment_income: 80 + +- name: Retirement and HSA payroll deductions reduce IRS employment income. + period: 2024 + input: + employment_income: 100 + traditional_401k_contributions: 10 + traditional_403b_contributions: 5 + health_savings_account_payroll_contributions: 3 + output: + pre_tax_contributions: 18 + irs_employment_income: 82 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/credits/homestead_property_tax/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/credits/homestead_property_tax/integration.yaml index edf07d41ba2..b7d29725070 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/credits/homestead_property_tax/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/credits/homestead_property_tax/integration.yaml @@ -198,6 +198,7 @@ employment_income: 56_000 real_estate_taxes: 2_500 age: 66 + medicare_enrolled: false state_code: MI output: # (2500 - 56000 * 0.032) * 0.6 * 0.5 mi_homestead_property_tax_credit_countable_property_tax: 2_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/integration.yaml index 8919f13c7c4..7fa06e4db59 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/integration.yaml @@ -5,6 +5,7 @@ people: person1: age: 67 + medicare_enrolled: false employment_income: 10_010 taxable_interest_income: 11_010 taxable_private_pension_income: 7_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_pension_and_ss_or_ssd_deduction/integration_tests/mo_pension_and_ss_or_ssd.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_pension_and_ss_or_ssd_deduction/integration_tests/mo_pension_and_ss_or_ssd.yaml index 67686256a6c..8be4d48c067 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_pension_and_ss_or_ssd_deduction/integration_tests/mo_pension_and_ss_or_ssd.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_pension_and_ss_or_ssd_deduction/integration_tests/mo_pension_and_ss_or_ssd.yaml @@ -5,11 +5,13 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_head: true taxable_social_security: 10_000 person2: age: 72 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_spouse: true taxable_public_pension_income: 10_000 @@ -35,11 +37,13 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_head: true taxable_social_security: 10_000 person2: age: 72 + medicare_enrolled: false employment_income: 15_000 is_tax_unit_spouse: true taxable_social_security: 10_000 @@ -65,11 +69,13 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_head: true taxable_social_security: 6_000 person2: age: 72 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_spouse: true taxable_social_security: 6_000 @@ -95,11 +101,13 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_head: true taxable_social_security: 10_000 person2: age: 72 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_spouse: true taxable_private_pension_income: 10_000 @@ -125,6 +133,7 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_head: true taxable_public_pension_income: 5_000 @@ -132,6 +141,7 @@ taxable_social_security: 10_000 person2: age: 72 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_spouse: true taxable_public_pension_income: 5_000 @@ -158,6 +168,7 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_head: true taxable_public_pension_income: 2_500 @@ -165,6 +176,7 @@ taxable_social_security: 10_000 person2: age: 72 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_spouse: true taxable_public_pension_income: 2_500 @@ -192,6 +204,7 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_head: true taxable_public_pension_income: 5_000 @@ -199,6 +212,7 @@ taxable_social_security: 11_000 person2: age: 72 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_spouse: true taxable_public_pension_income: 5_000 @@ -226,11 +240,13 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_head: true taxable_public_pension_income: 10_000 person2: age: 72 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_spouse: true taxable_public_pension_income: 10_000 @@ -256,11 +272,13 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_head: true taxable_private_pension_income: 10_000 person2: age: 72 + medicare_enrolled: false employment_income: 25_000 is_tax_unit_spouse: true taxable_private_pension_income: 10_000 @@ -286,12 +304,14 @@ people: person1: age: 78 + medicare_enrolled: false employment_income: 5_000 is_tax_unit_head: true taxable_private_pension_income: 5_000 taxable_public_pension_income: 5_000 person2: age: 72 + medicare_enrolled: false employment_income: 5_000 is_tax_unit_spouse: true taxable_private_pension_income: 5_000 @@ -382,6 +402,7 @@ person1: is_tax_unit_head: true age: 69 + medicare_enrolled: false employment_income: 15_010 taxable_interest_income: 5_505 taxable_private_pension_income: 3_000 @@ -392,6 +413,7 @@ person2: is_tax_unit_spouse: true age: 69 + medicare_enrolled: false employment_income: 3_010 taxable_interest_income: 5_505 taxable_private_pension_income: 3_000 @@ -429,6 +451,7 @@ people: person1: age: 75 + medicare_enrolled: false employment_income: 0 taxable_interest_income: 131 taxable_private_pension_income: 25_717 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/income_tax/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/income_tax/integration.yaml index 74d6e01951e..8548ed28940 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/income_tax/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/income_tax/integration.yaml @@ -150,6 +150,7 @@ people: person1: age: 78 + medicare_enrolled: false taxable_interest_income: 21_896.16 short_term_capital_gains: 6_010.3447 long_term_capital_gains: 44_087.02 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml index 0cd712f65dd..98420dc467f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration.yaml @@ -5,6 +5,7 @@ people: person1: age: 69 + medicare_enrolled: false employment_income: 29_010 taxable_interest_income: 11_010 ssi: 0 # not in TAXSIM35 diff --git a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py index 53cfb392666..70d9b721a67 100644 --- a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py +++ b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py @@ -21,9 +21,10 @@ def formula(person, period, parameters): period ).calibration.gov.hhs.medicare.per_capita_cost - # Premiums paid by beneficiary + # Premium offsets to Medicare program cost. Use gross Part B premiums + # before MSP offsets so MSP support does not inflate Medicare's value. part_a_premium = person("base_part_a_premium", period) - part_b_premium = person("medicare_part_b_premium", period) + part_b_premium = person("gross_medicare_part_b_premium", period) total_premiums = part_a_premium + part_b_premium # Net benefit = spending - premiums diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/gross_medicare_part_b_premium.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/gross_medicare_part_b_premium.py new file mode 100644 index 00000000000..4c583bf54f1 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/gross_medicare_part_b_premium.py @@ -0,0 +1,54 @@ +from policyengine_us.model_api import * + + +class gross_medicare_part_b_premium(Variable): + value_type = float + entity = Person + label = "Gross Medicare Part B premium" + unit = USD + definition_period = YEAR + defined_for = "is_medicare_eligible" + reference = "https://www.medicare.gov/your-medicare-costs/part-b-costs" + documentation = "Annual Medicare Part B premium before Medicare Savings Program coverage, including any income-related monthly adjustment amount. Based on modified adjusted gross income from 2 years prior." + + def formula(person, period, parameters): + tax_unit = person.tax_unit + filing_status_holder = tax_unit.simulation.get_holder("filing_status") + filing_status = tax_unit("filing_status", period) + status = filing_status_holder.variable.possible_values + is_single = filing_status == status.SINGLE + is_joint = filing_status == status.JOINT + is_head_of_household = filing_status == status.HEAD_OF_HOUSEHOLD + is_surviving_spouse = filing_status == status.SURVIVING_SPOUSE + is_separated = filing_status == status.SEPARATE + # Medicare Part B IRMAA is based on MAGI from 2 years prior. + # MAGI = AGI + tax-exempt interest. + prior_period = period.offset(-2, "year") + agi = tax_unit("adjusted_gross_income", prior_period) + tax_exempt_interest = tax_unit("tax_exempt_interest_income", prior_period) + magi = agi + tax_exempt_interest + base = person("base_part_b_premium", period) + + p = parameters(period).gov.hhs.medicare.part_b.irmaa + + irmaa_amount = select( + [ + is_single, + is_joint, + is_head_of_household, + is_surviving_spouse, + is_separated, + ], + [ + p.single.calc(magi), + p.joint.calc(magi), + p.head_of_household.calc(magi), + p.surviving_spouse.calc(magi), + p.separate.calc(magi), + ], + ) + + # IRMAA amounts are monthly, multiply by MONTHS_IN_YEAR to get annual. + # Base is already annual. + annual_irmaa = irmaa_amount * MONTHS_IN_YEAR + return base + annual_irmaa diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py index 836af3a5048..4eae71074ba 100644 --- a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py @@ -7,46 +7,11 @@ class medicare_part_b_premium(Variable): label = "Medicare Part B premium" unit = USD definition_period = YEAR - defined_for = "is_medicare_eligible" + defined_for = "medicare_enrolled" reference = "https://www.medicare.gov/your-medicare-costs/part-b-costs" - documentation = "Annual Medicare Part B premium, including any income-related monthly adjustment amount. Based on modified adjusted gross income from 2 years prior." + documentation = "Annual Medicare Part B premium paid out of pocket by the enrollee, net of Medicare Savings Program coverage." def formula(person, period, parameters): - tax_unit = person.tax_unit - filing_status = tax_unit("filing_status", period) - # Medicare Part B IRMAA is based on MAGI from 2 years prior - # MAGI = AGI + tax-exempt interest - prior_period = period.offset(-2, "year") - agi = tax_unit("adjusted_gross_income", prior_period) - tax_exempt_interest = tax_unit("tax_exempt_interest_income", prior_period) - magi = agi + tax_exempt_interest - base = person("base_part_b_premium", period) - - # Build boolean masks for each status - status = filing_status.possible_values - statuses = [ - status.SINGLE, - status.JOINT, - status.HEAD_OF_HOUSEHOLD, - status.SURVIVING_SPOUSE, - status.SEPARATE, - ] - in_status = [filing_status == s for s in statuses] - - p = parameters(period).gov.hhs.medicare.part_b.irmaa - - irmaa_amount = select( - in_status, - [ - p.single.calc(magi), - p.joint.calc(magi), - p.head_of_household.calc(magi), - p.surviving_spouse.calc(magi), - p.separate.calc(magi), - ], - ) - - # IRMAA amounts are monthly, multiply by MONTHS_IN_YEAR to get annual - # Base is already annual - annual_irmaa = irmaa_amount * MONTHS_IN_YEAR - return base + annual_irmaa + gross_premium = person("gross_medicare_part_b_premium", period) + msp_coverage = person("msp_part_b_premium_coverage", period) + return max_(gross_premium - msp_coverage, 0) diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.py new file mode 100644 index 00000000000..cac89f2d0d1 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.py @@ -0,0 +1,41 @@ +from policyengine_us.model_api import * + + +class msp_part_b_premium_coverage(Variable): + value_type = float + entity = Person + unit = USD + label = "Medicare Part B premium amount covered by MSP" + definition_period = YEAR + reference = ( + "https://www.medicare.gov/basics/costs/help/medicare-savings-programs", + ) + documentation = """ + Annual standard Part B premium amount paid on the enrollee's behalf through a + Medicare Savings Program-like pathway. + + This uses the MSP income and asset rules directly and intentionally avoids the + modeled Medicaid exclusion used in QI eligibility because that path reaches the + medically needy Medicaid formula, which depends on medical_out_of_pocket_expenses + and would create a cycle in SPM MOOP calculations. + + The coverage amount is capped at the standard Part B premium. Any IRMAA amount + above the standard premium remains the enrollee's responsibility. + """ + + def formula(person, period, parameters): + enrolled = person("medicare_enrolled", period) + monthly_standard_premium = ( + person("base_part_b_premium", period) / MONTHS_IN_YEAR + ) + monthly_coverage = 0 + for month in period.get_subperiods(MONTH): + income_eligible = person("msp_income_eligible", month) + asset_eligible = person("msp_asset_eligible", month) + eligible_for_coverage = enrolled & income_eligible & asset_eligible + monthly_coverage += where( + eligible_for_coverage, + monthly_standard_premium, + 0, + ) + return monthly_coverage diff --git a/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py b/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py new file mode 100644 index 00000000000..500970c641d --- /dev/null +++ b/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py @@ -0,0 +1,10 @@ +from policyengine_us.model_api import * + + +class medicare_part_b_premiums_reported(Variable): + value_type = float + entity = Person + label = "Medicare Part B premiums (reported)" + definition_period = YEAR + unit = USD + uprating = "calibration.gov.hhs.cms.moop_per_capita" diff --git a/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py new file mode 100644 index 00000000000..57064157628 --- /dev/null +++ b/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py @@ -0,0 +1,15 @@ +from policyengine_us.model_api import * + + +class pre_tax_health_insurance_premiums(Variable): + value_type = float + entity = Person + label = "Pre-tax health insurance premiums" + unit = USD + documentation = ( + "Health insurance premiums paid through pre-tax payroll deductions. " + "This excludes Medicare Part B premiums and other post-tax medical " + "out-of-pocket premiums." + ) + definition_period = YEAR + uprating = "calibration.gov.hhs.cms.moop_per_capita" 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 3f2b5242a7a..f54a75562af 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,8 +12,9 @@ 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 Part " - "B premiums. Non-premium expenses include other medical expenses and " - "over-the-counter health expenses." + "B premiums net of Medicare Savings Program coverage. Non-premium " + "expenses include other medical expenses and over-the-counter health " + "expenses." ) adds = [