diff --git a/changelog.d/ltc-premiums-itemized-medical.fixed.md b/changelog.d/ltc-premiums-itemized-medical.fixed.md new file mode 100644 index 00000000000..9e3ca50e965 --- /dev/null +++ b/changelog.d/ltc-premiums-itemized-medical.fixed.md @@ -0,0 +1 @@ +Count eligible long-term care insurance premiums, up to the 26 U.S.C. 213(d)(10) limit for the insured person's age, in Schedule A medical expenses and the self-employed health insurance deduction. Ohio, Idaho, New Jersey and Michigan remove only the part of that deduction that took the premiums each one counts. diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/medical/long_term_care_premium_limit.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/medical/long_term_care_premium_limit.yaml new file mode 100644 index 00000000000..a27131ff084 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/medical/long_term_care_premium_limit.yaml @@ -0,0 +1,177 @@ +description: The IRS counts premiums for a qualified long-term care insurance contract as medical care up to this amount for each insured person, based on that person's age at the end of the year. +# Each threshold is the top of an age band: "40 or less", "more than 40 but +# not more than 50", and so on. Look ages up with right=True so an age equal +# to a threshold stays in the lower band. +brackets: + - threshold: + values: + 2013-01-01: -.inf + amount: + values: + 2013-01-01: 360 + 2014-01-01: 370 + 2015-01-01: 380 + 2016-01-01: 390 + 2017-01-01: 410 + 2018-01-01: 420 + 2019-01-01: 420 + 2020-01-01: 430 + 2021-01-01: 450 + 2022-01-01: 450 + 2023-01-01: 480 + 2024-01-01: 470 + 2025-01-01: 480 + 2026-01-01: 500 + metadata: + uprating: + parameter: gov.bls.cpi.cpi_u_medical_care + rounding: + type: nearest + interval: 10 + - threshold: + values: + 2013-01-01: 40 + amount: + values: + 2013-01-01: 680 + 2014-01-01: 700 + 2015-01-01: 710 + 2016-01-01: 730 + 2017-01-01: 770 + 2018-01-01: 780 + 2019-01-01: 790 + 2020-01-01: 810 + 2021-01-01: 850 + 2022-01-01: 850 + 2023-01-01: 890 + 2024-01-01: 880 + 2025-01-01: 900 + 2026-01-01: 930 + metadata: + uprating: + parameter: gov.bls.cpi.cpi_u_medical_care + rounding: + type: nearest + interval: 10 + - threshold: + values: + 2013-01-01: 50 + amount: + values: + 2013-01-01: 1_360 + 2014-01-01: 1_400 + 2015-01-01: 1_430 + 2016-01-01: 1_460 + 2017-01-01: 1_530 + 2018-01-01: 1_560 + 2019-01-01: 1_580 + 2020-01-01: 1_630 + 2021-01-01: 1_690 + 2022-01-01: 1_690 + 2023-01-01: 1_790 + 2024-01-01: 1_760 + 2025-01-01: 1_800 + 2026-01-01: 1_860 + metadata: + uprating: + parameter: gov.bls.cpi.cpi_u_medical_care + rounding: + type: nearest + interval: 10 + - threshold: + values: + 2013-01-01: 60 + amount: + values: + 2013-01-01: 3_640 + 2014-01-01: 3_720 + 2015-01-01: 3_800 + 2016-01-01: 3_900 + 2017-01-01: 4_090 + 2018-01-01: 4_160 + 2019-01-01: 4_220 + 2020-01-01: 4_350 + 2021-01-01: 4_520 + 2022-01-01: 4_510 + 2023-01-01: 4_770 + 2024-01-01: 4_710 + 2025-01-01: 4_810 + 2026-01-01: 4_960 + metadata: + uprating: + parameter: gov.bls.cpi.cpi_u_medical_care + rounding: + type: nearest + interval: 10 + - threshold: + values: + 2013-01-01: 70 + amount: + values: + 2013-01-01: 4_550 + 2014-01-01: 4_660 + 2015-01-01: 4_750 + 2016-01-01: 4_870 + 2017-01-01: 5_110 + 2018-01-01: 5_200 + 2019-01-01: 5_270 + 2020-01-01: 5_430 + 2021-01-01: 5_640 + 2022-01-01: 5_640 + 2023-01-01: 5_960 + 2024-01-01: 5_880 + 2025-01-01: 6_020 + 2026-01-01: 6_200 + metadata: + uprating: + parameter: gov.bls.cpi.cpi_u_medical_care + rounding: + type: nearest + interval: 10 +metadata: + type: single_amount + threshold_unit: year + amount_unit: currency-USD + period: year + label: Eligible long-term care premium limit + # 26 U.S.C. 213(d)(10)(B) indexes these amounts by the medical care + # component of the chained CPI-U for August of the preceding year, + # rounding each increase to the nearest $10. Neither CBO nor CMS projects + # that index, so years after the last published values use CBO's + # projection of the CPI-U medical care component, by calendar year, as a + # proxy (as long_term_health_insurance_premiums does). + reference: + - title: 26 U.S. Code ยง 213(d)(10) - Eligible long-term care premiums + href: https://www.law.cornell.edu/uscode/text/26/213#d_10 + - title: Rev. Proc. 2012-41, section 3.11 (2013) + href: https://www.irs.gov/irb/2012-45_IRB#RP-2012-41 + - title: Rev. Proc. 2013-35, section 3.24 (2014) + href: https://www.irs.gov/pub/irs-drop/rp-13-35.pdf#page=16 + - title: Rev. Proc. 2014-61, section 3.25 (2015) + href: https://www.irs.gov/pub/irs-drop/rp-14-61.pdf#page=16 + - title: Rev. Proc. 2015-53, section 3.25 (2016) + href: https://www.irs.gov/pub/irs-drop/rp-15-53.pdf#page=17 + - title: Rev. Proc. 2016-55, section 3.26 (2017) + href: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=19 + - title: Rev. Proc. 2017-58, section 3.26 (2018) + href: https://www.irs.gov/pub/irs-drop/rp-17-58.pdf#page=17 + - title: Publication 502 (2018), Long-Term Care (2018 amounts after Rev. Proc. 2018-18) + href: https://www.irs.gov/pub/irs-prior/p502--2018.pdf#page=11 + - title: Rev. Proc. 2018-57, section 3.28 (2019) + href: https://www.irs.gov/pub/irs-drop/rp-18-57.pdf#page=19 + - title: Rev. Proc. 2019-44, section 3.28 (2020) + href: https://www.irs.gov/pub/irs-drop/rp-19-44.pdf#page=16 + - title: Rev. Proc. 2020-45, section 3.28 (2021) + href: https://www.irs.gov/pub/irs-drop/rp-20-45.pdf#page=17 + - title: Rev. Proc. 2021-45, section 3.28 (2022) + href: https://www.irs.gov/pub/irs-drop/rp-21-45.pdf#page=17 + - title: Rev. Proc. 2022-38, section 3.28 (2023) + href: https://www.irs.gov/pub/irs-drop/rp-22-38.pdf#page=17 + - title: Rev. Proc. 2023-34, section 3.28 (2024) + href: https://www.irs.gov/pub/irs-drop/rp-23-34.pdf#page=16 + - title: Rev. Proc. 2024-40, section 3.28 (2025) + href: https://www.irs.gov/pub/irs-drop/rp-24-40.pdf#page=15 + - title: Rev. Proc. 2025-32, section 3.27 (2026) + href: https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=22 + - title: 2022 Instructions for Schedule A (Form 1040), line 1 (age at the end of the year) + href: https://www.irs.gov/pub/irs-prior/i1040sca--2022.pdf#page=2 diff --git a/policyengine_us/tests/core/test_long_term_care_premium_properties.py b/policyengine_us/tests/core/test_long_term_care_premium_properties.py new file mode 100644 index 00000000000..59577442142 --- /dev/null +++ b/policyengine_us/tests/core/test_long_term_care_premium_properties.py @@ -0,0 +1,451 @@ +"""Properties of eligible long-term care premiums on Schedule A. + +26 U.S.C. 213(d)(1)(D) counts premiums for a qualified long-term care +insurance contract as medical care up to the 213(d)(10) limit for the insured +person's age at the end of the year. 162(l)(2)(C) lets the same eligible +premiums into the self-employed health insurance deduction, and 162(l)(3) +keeps the part deducted there off Schedule A. + +The limits below are typed from the Rev. Proc. tables, independently of the +parameter file, so the first test is a differential check of the parameter +against the published amounts. The other properties run generated tax units +(single, joint, with dependents; self-employed or not; the deduction derived +from self-employment income or supplied for the return) through one +vectorized simulation per example. Every unit appears twice: as drawn, and +with each person's long-term care premiums raised. Medicare enrollment is off, +so a person's medical insurance premiums are exactly their direct input. For +every tax unit: + +1. Eligible premiums equal min(premiums, published limit for the age), with + ages at each band edge; they are never negative or above the premiums. +2. The self-employed exclusion takes medical insurance premiums first: + medical part = min(filers' medical premiums, deduction); long-term care + part = min(filers' eligible premiums, deduction - medical part); together + they equal min(filers' medical + eligible premiums, deduction), and the + applied exclusion of a computed subtotal is their sum. +3. Accounting: the subtotal is members' medical premiums - medical part + + members' eligible premiums - long-term care part - dependents' own + long-term care parts + other expenses. A person's own long-term care part + is what that person's deduction leaves after their medical premiums, + limited to their eligible premiums. The long-term care premiums + inside it lie between what the dependents' own deductions left of their + eligible premiums and all members' eligible premiums. +4. Conservation (derived deduction): every eligible premium is deducted once, + on Schedule A, as the return's self-employed deduction or as a + dependent's own, never twice. +5. The derived person deduction is min(earned income limit, health premiums + + eligible long-term care premiums) for the self-employed, else 0. The + limit (Form 7206 line 13: net profit less the deductible part of + self-employment tax and plan contributions) is the model's own + self_employed_health_insurance_earned_income_limit, tested on its own. +6. Monotonicity: raising long-term care premiums never lowers the subtotal + and raises it by at most the rise in eligible premiums; for units where + neither the return nor a dependent has a deduction it raises it by + exactly that amount. +7. A supplied subtotal holds no computed long-term care premiums and no + applied exclusion. +8. Years after the last published table: every amount is a multiple of $10, + amounts rise with age, and no amount falls from one year to the next. +""" + +import numpy as np +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.system import system + +TOLERANCE = 0.01 + +# Rev. Proc. tables: 40 or less, 41-50, 51-60, 61-70, over 70. +PUBLISHED_LIMITS = { + 2013: (360, 680, 1_360, 3_640, 4_550), # Rev. Proc. 2012-41 + 2014: (370, 700, 1_400, 3_720, 4_660), # Rev. Proc. 2013-35 + 2015: (380, 710, 1_430, 3_800, 4_750), # Rev. Proc. 2014-61 + 2016: (390, 730, 1_460, 3_900, 4_870), # Rev. Proc. 2015-53 + 2017: (410, 770, 1_530, 4_090, 5_110), # Rev. Proc. 2016-55 + 2018: (420, 780, 1_560, 4_160, 5_200), # Rev. Proc. 2017-58 + 2019: (420, 790, 1_580, 4_220, 5_270), # Rev. Proc. 2018-57 + 2020: (430, 810, 1_630, 4_350, 5_430), # Rev. Proc. 2019-44 + 2021: (450, 850, 1_690, 4_520, 5_640), # Rev. Proc. 2020-45 + 2022: (450, 850, 1_690, 4_510, 5_640), # Rev. Proc. 2021-45 + 2023: (480, 890, 1_790, 4_770, 5_960), # Rev. Proc. 2022-38 + 2024: (470, 880, 1_760, 4_710, 5_880), # Rev. Proc. 2023-34 + 2025: (480, 900, 1_800, 4_810, 6_020), # Rev. Proc. 2024-40 + 2026: (500, 930, 1_860, 4_960, 6_200), # Rev. Proc. 2025-32 +} +BAND_TOPS = (40, 50, 60, 70) +LAST_PUBLISHED = max(PUBLISHED_LIMITS) +EDGE_AGES = [0, 18, 39, 40, 41, 50, 51, 60, 61, 70, 71, 95] + + +def _band(age): + return sum(age > top for top in BAND_TOPS) + + +def _published_limit(year, age): + return PUBLISHED_LIMITS[year][_band(age)] + + +def _limit_parameter(year): + return system.parameters( + f"{year}-01-01" + ).gov.irs.deductions.itemized.medical.long_term_care_premium_limit + + +def test_parameter_matches_published_limits(): + for year in PUBLISHED_LIMITS: + scale = _limit_parameter(year) + ages = np.array(EDGE_AGES, dtype=float) + expected = [_published_limit(year, age) for age in EDGE_AGES] + np.testing.assert_array_equal(scale.calc(ages, right=True), expected) + + +def test_limits_after_the_last_published_year(): + previous = np.array(PUBLISHED_LIMITS[LAST_PUBLISHED], dtype=float) + ages = np.array([30, 45, 55, 65, 75], dtype=float) + for year in range(LAST_PUBLISHED + 1, LAST_PUBLISHED + 10): + amounts = _limit_parameter(year).calc(ages, right=True) + assert (amounts % 10 == 0).all(), (year, amounts) + assert (np.diff(amounts) > 0).all(), (year, amounts) + assert (amounts >= previous).all(), (year, amounts, previous) + previous = amounts + + +premium = st.one_of(st.just(0), st.integers(1, 12_000)) +expense = st.one_of(st.just(0), st.integers(1, 15_000)) + + +@st.composite +def people(draw, adult=True): + self_employed = draw(st.booleans()) + return { + "age": draw(st.sampled_from(EDGE_AGES[2:] if adult else EDGE_AGES)), + "self_employed": self_employed, + "earnings": draw(st.integers(-10_000, 60_000)) if self_employed else 0, + "health_premiums": draw(expense), + "long_term_care": draw(premium), + "long_term_care_rise": draw(st.integers(0, 8_000)), + "other": draw(expense), + } + + +@st.composite +def tax_units(draw): + kind = draw(st.sampled_from(["single", "joint", "joint_dependents", "hoh"])) + n_dependents = draw(st.integers(1, 2)) if kind in ("joint_dependents", "hoh") else 0 + return { + "head": draw(people()), + "spouse": draw(people()) if kind.startswith("joint") else None, + "dependents": [draw(people(adult=False)) for _ in range(n_dependents)], + "supplied_deduction": draw(st.integers(0, 30_000)), + } + + +def _entries(unit): + entries = [("head", unit["head"])] + if unit["spouse"] is not None: + entries.append(("spouse", unit["spouse"])) + return entries + [("dependent", d) for d in unit["dependents"]] + + +VARIANTS = ["base", "raised"] + + +def _situation(units, year, supplied_deduction, supplied_subtotal=False): + people_, tax_units_, households, marital_units = {}, {}, {}, {} + for variant in VARIANTS: + for i, unit in enumerate(units): + names, couple = [], [] + for j, (role, values) in enumerate(_entries(unit)): + name = f"{variant}_{i}_{j}" + names.append(name) + long_term_care = values["long_term_care"] + ( + values["long_term_care_rise"] if variant == "raised" else 0 + ) + people_[name] = { + key: {year: value} + for key, value in { + "age": values["age"], + "is_tax_unit_head": role == "head", + "is_tax_unit_spouse": role == "spouse", + "is_tax_unit_dependent": role == "dependent", + "is_self_employed": values["self_employed"], + "self_employment_income": values["earnings"], + "medicare_enrolled": False, + "health_insurance_premiums": values["health_premiums"], + "long_term_health_insurance_premiums": long_term_care, + "other_medical_expenses": values["other"], + }.items() + } + if role == "dependent": + marital_units[f"{name}_single"] = {"members": [name]} + else: + couple.append(name) + marital_units[f"{variant}_{i}_couple"] = {"members": couple} + tax_unit = {"members": names} + if supplied_deduction: + tax_unit["self_employed_health_insurance_ald"] = { + year: unit["supplied_deduction"] + } + if supplied_subtotal: + tax_unit["itemized_medical_expenses"] = {year: 1_000} + tax_units_[f"{variant}_{i}_tax_unit"] = tax_unit + households[f"{variant}_{i}_household"] = { + "members": names, + "state_code": {year: "TX"}, + } + return { + "people": people_, + "tax_units": tax_units_, + "households": households, + "marital_units": marital_units, + } + + +PERSON_VARIABLES = [ + "eligible_long_term_care_premiums", + "self_employed_health_insurance_ald_person", + "self_employed_health_insurance_earned_income_limit", + "is_tax_unit_dependent", +] +UNIT_VARIABLES = [ + "self_employed_health_insurance_ald", + "itemized_medical_expenses_excluded_premiums", + "itemized_medical_expenses_excluded_long_term_care_premiums", + "itemized_medical_expenses_applied_exclusion", + "itemized_medical_expenses_long_term_care_premiums", + "itemized_medical_expenses", +] + + +def _run(units, year, supplied_deduction): + sim = Simulation( + tax_benefit_system=system, + situation=_situation(units, year, supplied_deduction), + ) + out = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in PERSON_VARIABLES + UNIT_VARIABLES + } + out["unit"] = sim.populations["tax_unit"].members_entity_id + return out + + +def _person_inputs(units): + rows = [] + for variant in VARIANTS: + for unit in units: + for role, values in _entries(unit): + long_term_care = values["long_term_care"] + ( + values["long_term_care_rise"] if variant == "raised" else 0 + ) + rows.append( + ( + values["age"], + role != "dependent", + values["self_employed"], + values["earnings"], + values["health_premiums"], + long_term_care, + values["other"], + ) + ) + return np.array(rows, dtype=float).T + + +def _check(units, year, supplied_deduction): + run = _run(units, year, supplied_deduction) + age, filer, self_employed, earnings, health, long_term_care, other = _person_inputs( + units + ) + unit = run["unit"] + n_units = unit.max() + 1 + + def unit_sum(values): + return np.bincount(unit, weights=values, minlength=n_units) + + # 1. Eligible premiums: min(premiums, published limit), within bounds. + eligible = run["eligible_long_term_care_premiums"] + if year in PUBLISHED_LIMITS: + limit = np.array([_published_limit(year, a) for a in age]) + np.testing.assert_allclose( + eligible, np.minimum(long_term_care, limit), atol=TOLERANCE + ) + assert (eligible >= 0).all() + assert (eligible <= long_term_care + TOLERANCE).all() + + # 2. The exclusion takes medical premiums first, then eligible premiums. + deduction = np.maximum(run["self_employed_health_insurance_ald"], 0) + filer_health = unit_sum(health * filer) + filer_eligible = unit_sum(eligible * filer) + medical_part = np.minimum(filer_health, deduction) + long_term_care_part = np.minimum(filer_eligible, deduction - medical_part) + # Each person's own deduction takes that person's medical premiums (Form + # 7206 line 1) first; the rest, up to the eligible premiums, is line 2. + person_deduction = np.maximum(run["self_employed_health_insurance_ald_person"], 0) + own_long_term_care_part = np.minimum( + eligible * self_employed, + person_deduction - np.minimum(health * self_employed, person_deduction), + ) + np.testing.assert_allclose( + run["itemized_medical_expenses_excluded_premiums"], + medical_part, + atol=TOLERANCE, + ) + np.testing.assert_allclose( + run["itemized_medical_expenses_excluded_long_term_care_premiums"], + long_term_care_part, + atol=TOLERANCE, + ) + np.testing.assert_allclose( + medical_part + long_term_care_part, + np.minimum(filer_health + filer_eligible, deduction), + atol=TOLERANCE, + ) + np.testing.assert_allclose( + run["itemized_medical_expenses_applied_exclusion"], + medical_part + long_term_care_part, + atol=TOLERANCE, + ) + + # 3. Accounting identity for the subtotal, from the raw inputs. Each + # dependent's own deduction took that dependent's long-term care part. + inside = run["itemized_medical_expenses_long_term_care_premiums"] + all_eligible = unit_sum(eligible) + dependent = ~filer.astype(bool) + dependents_part = unit_sum(own_long_term_care_part * dependent) + np.testing.assert_allclose( + inside, + all_eligible - long_term_care_part - dependents_part, + atol=TOLERANCE, + ) + assert ( + inside >= unit_sum((eligible - own_long_term_care_part) * dependent) - TOLERANCE + ).all() + assert (inside <= all_eligible + TOLERANCE).all() + subtotal = run["itemized_medical_expenses"] + np.testing.assert_allclose( + subtotal, + unit_sum(health) - medical_part + inside + unit_sum(other), + atol=TOLERANCE, + ) + + if not supplied_deduction: + # 4. Each eligible premium is deducted once, on the return or on a + # dependent's own return. + np.testing.assert_allclose( + subtotal + - unit_sum(other) + + run["self_employed_health_insurance_ald"] + + dependents_part, + unit_sum(health) + all_eligible, + atol=TOLERANCE, + ) + # 5. The derived person deduction (Form 7206 lines 1-3). + np.testing.assert_allclose( + run["self_employed_health_insurance_ald_person"], + self_employed + * np.minimum( + run["self_employed_health_insurance_earned_income_limit"], + health + eligible, + ), + atol=TOLERANCE, + ) + + # 6. Monotonicity in long-term care premiums. + half = n_units // 2 + base, raised = slice(0, half), slice(half, n_units) + rise_in_eligible = all_eligible[raised] - all_eligible[base] + rise_in_subtotal = subtotal[raised] - subtotal[base] + assert (rise_in_subtotal >= -TOLERANCE).all() + assert (rise_in_subtotal <= rise_in_eligible + TOLERANCE).all() + # Neither the return's deduction nor a dependent's own takes any premium. + any_deduction = deduction + unit_sum(person_deduction * dependent) + no_deduction = (any_deduction[base] == 0) & (any_deduction[raised] == 0) + np.testing.assert_allclose( + rise_in_subtotal[no_deduction], + rise_in_eligible[no_deduction], + atol=TOLERANCE, + ) + + +SETTINGS = dict(max_examples=10, deadline=None, derandomize=True) +YEARS = list(range(2015, LAST_PUBLISHED + 4)) + + +@settings(**SETTINGS) +@given( + st.sampled_from(YEARS), + st.booleans(), + st.lists(tax_units(), min_size=3, max_size=12), +) +def test_long_term_care_premium_properties(year, supplied_deduction, units): + _check(units, year, supplied_deduction) + + +@pytest.mark.parametrize("year", [2018, 2022, 2025]) +@pytest.mark.parametrize("supplied_deduction", [False, True]) +def test_seeded_population(year, supplied_deduction): + rng = np.random.default_rng(20261018 + year) + + def person(adult=True): + self_employed = bool(rng.random() < 0.4) + return { + "age": int(rng.choice(EDGE_AGES[2:] if adult else EDGE_AGES)), + "self_employed": self_employed, + "earnings": int(rng.integers(-10_000, 60_001)) if self_employed else 0, + "health_premiums": int(rng.integers(0, 15_001)), + "long_term_care": int(rng.integers(0, 12_001)) * (rng.random() < 0.7), + "long_term_care_rise": int(rng.integers(0, 8_001)), + "other": int(rng.integers(0, 15_001)), + } + + units = [] + for _ in range(60): + kind = rng.choice(["single", "joint", "joint_dependents", "hoh"]) + n_dependents = ( + int(rng.integers(1, 3)) if kind in ("joint_dependents", "hoh") else 0 + ) + units.append( + { + "head": person(), + "spouse": person() if kind.startswith("joint") else None, + "dependents": [person(adult=False) for _ in range(n_dependents)], + "supplied_deduction": int(rng.integers(0, 30_001)), + } + ) + _check(units, year, supplied_deduction) + + +def test_supplied_subtotal_holds_no_computed_long_term_care_premiums(): + units = [ + { + "head": { + "age": 75, + "self_employed": True, + "earnings": 40_000, + "health_premiums": 2_000, + "long_term_care": 9_000, + "long_term_care_rise": 0, + "other": 500, + }, + "spouse": None, + "dependents": [], + "supplied_deduction": 0, + } + ] + sim = Simulation( + tax_benefit_system=system, + situation=_situation(units, 2025, False, supplied_subtotal=True), + ) + np.testing.assert_array_equal( + sim.calculate("itemized_medical_expenses_long_term_care_premiums", 2025), 0 + ) + np.testing.assert_array_equal( + sim.calculate("itemized_medical_expenses_applied_exclusion", 2025), 0 + ) + np.testing.assert_array_equal( + sim.calculate("itemized_medical_expenses", 2025), 1_000 + ) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_long_term_care_part.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_long_term_care_part.yaml new file mode 100644 index 00000000000..3479e5cbb78 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_long_term_care_part.yaml @@ -0,0 +1,64 @@ +# The long-term care part (Form 7206 line 2) of a person's self-employed +# health insurance deduction: what the deduction leaves after the person's +# medical insurance premiums (line 1), limited to the line 2 premiums. The +# 2025 limit at age 51-60 is $1,800 (Rev. Proc. 2024-40). +- name: Medical premiums take the deduction first + period: 2025 + input: + age: 55 + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 3_000 + long_term_health_insurance_premiums: 2_500 + output: + # 3,000 + min(2,500, 1,800) = 4,800 deducted; 4,800 - 3,000 = 1,800. + self_employed_health_insurance_ald_person: 4_800 + self_employed_health_insurance_ald_long_term_care_part: 1_800 + +- name: The earned income limit cuts the long-term care part + period: 2025 + absolute_error_margin: 0.01 + input: + age: 55 + is_self_employed: true + self_employment_income: 4_000 + health_insurance_premiums: 3_000 + long_term_health_insurance_premiums: 2_500 + output: + # Limit 4,000 - 4,000 x 0.9235 x 0.153 / 2 = 3,717.41; 3,717.41 - 3,000. + self_employed_health_insurance_ald_long_term_care_part: 717.41 + +- name: Survey premium inputs are not on line 1 + period: 2025 + input: + age: 55 + is_self_employed: true + self_employment_income: 120_000 + health_insurance_premiums_without_medicare_part_b: 3_000 + long_term_health_insurance_premiums: 1_800 + output: + self_employed_health_insurance_ald_person: 1_800 + self_employed_health_insurance_ald_long_term_care_part: 1_800 + +- name: A supplied deduction above lines 1 and 2 adds nothing + period: 2025 + input: + age: 55 + is_self_employed: true + health_insurance_premiums: 1_000 + long_term_health_insurance_premiums: 500 + self_employed_health_insurance_ald_person: 5_000 + output: + # min(500, 5,000 - min(1,000, 5,000)) = 500. + self_employed_health_insurance_ald_long_term_care_part: 500 + +- name: An employee has no long-term care part + period: 2025 + input: + age: 55 + employment_income: 50_000 + long_term_health_insurance_premiums: 1_800 + self_employed_health_insurance_ald_person: 1_800 + output: + # Not self-employed: no line 2 premiums. + self_employed_health_insurance_ald_long_term_care_part: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/eligible_long_term_care_premiums.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/eligible_long_term_care_premiums.yaml new file mode 100644 index 00000000000..c45de374373 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/eligible_long_term_care_premiums.yaml @@ -0,0 +1,234 @@ +# 26 U.S.C. 213(d)(10): premiums for a qualified long-term care insurance +# contract count as medical care up to a limit set by the insured person's +# attained age before the close of the taxable year. The limits below are the +# Rev. Proc. tables ("40 or less", "more than 40 but not more than 50", ...); +# the 2022 Schedule A instructions (PDF p. 2) restate them by "age at the end +# of 2022": 40 or under $450, 41-50 $850, 51-60 $1,690, 61-70 $4,510, 71 or +# older $5,640. + +- name: 2022, age 40 is in the lowest band. + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: 450 + +- name: 2022, age 41 starts the second band. + period: 2022 + absolute_error_margin: 0.01 + input: + age: 41 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: 850 + +- name: 2022, age 50 is still in the second band. + period: 2022 + absolute_error_margin: 0.01 + input: + age: 50 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: 850 + +- name: 2022, age 51 starts the third band. + period: 2022 + absolute_error_margin: 0.01 + input: + age: 51 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: 1_690 + +- name: 2022, age 60 is still in the third band. + period: 2022 + absolute_error_margin: 0.01 + input: + age: 60 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: 1_690 + +- name: 2022, age 61 starts the fourth band, which fell from 2021's 4,520. + period: 2022 + absolute_error_margin: 0.01 + input: + age: 61 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: 4_510 + +- name: 2022, age 70 is still in the fourth band. + period: 2022 + absolute_error_margin: 0.01 + input: + age: 70 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: 4_510 + +- name: 2022, age 71 is over 70. + period: 2022 + absolute_error_margin: 0.01 + input: + age: 71 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: 5_640 + +- name: Premiums under the limit count in full. + period: 2022 + absolute_error_margin: 0.01 + input: + age: 75 + long_term_health_insurance_premiums: 3_000 + output: + eligible_long_term_care_premiums: 3_000 + +- name: No premiums, nothing eligible. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 75 + output: + eligible_long_term_care_premiums: 0 + +- name: 2021 limits (Rev. Proc. 2020-45, section 3.28). + period: 2021 + absolute_error_margin: 0.01 + input: + people: + age_35: + age: 35 + long_term_health_insurance_premiums: 10_000 + age_45: + age: 45 + long_term_health_insurance_premiums: 10_000 + age_55: + age: 55 + long_term_health_insurance_premiums: 10_000 + age_65: + age: 65 + long_term_health_insurance_premiums: 10_000 + age_75: + age: 75 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: [450, 850, 1_690, 4_520, 5_640] + +- name: 2023 limits (Rev. Proc. 2022-38, section 3.28). + period: 2023 + absolute_error_margin: 0.01 + input: + people: + age_35: + age: 35 + long_term_health_insurance_premiums: 10_000 + age_45: + age: 45 + long_term_health_insurance_premiums: 10_000 + age_55: + age: 55 + long_term_health_insurance_premiums: 10_000 + age_65: + age: 65 + long_term_health_insurance_premiums: 10_000 + age_75: + age: 75 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: [480, 890, 1_790, 4_770, 5_960] + +- name: 2024 limits fell from 2023 (Rev. Proc. 2023-34, section 3.28). + period: 2024 + absolute_error_margin: 0.01 + input: + people: + age_35: + age: 35 + long_term_health_insurance_premiums: 10_000 + age_45: + age: 45 + long_term_health_insurance_premiums: 10_000 + age_55: + age: 55 + long_term_health_insurance_premiums: 10_000 + age_65: + age: 65 + long_term_health_insurance_premiums: 10_000 + age_75: + age: 75 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: [470, 880, 1_760, 4_710, 5_880] + +- name: 2025 limits (Rev. Proc. 2024-40, section 3.28). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + age_35: + age: 35 + long_term_health_insurance_premiums: 10_000 + age_45: + age: 45 + long_term_health_insurance_premiums: 10_000 + age_55: + age: 55 + long_term_health_insurance_premiums: 10_000 + age_65: + age: 65 + long_term_health_insurance_premiums: 10_000 + age_75: + age: 75 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: [480, 900, 1_800, 4_810, 6_020] + +- name: 2026 limits (Rev. Proc. 2025-32, section 3.27). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + age_35: + age: 35 + long_term_health_insurance_premiums: 10_000 + age_45: + age: 45 + long_term_health_insurance_premiums: 10_000 + age_55: + age: 55 + long_term_health_insurance_premiums: 10_000 + age_65: + age: 65 + long_term_health_insurance_premiums: 10_000 + age_75: + age: 75 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: [500, 930, 1_860, 4_960, 6_200] + +- name: 2018 limits (Rev. Proc. 2017-58, section 3.26; Publication 502 (2018)). + period: 2018 + absolute_error_margin: 0.01 + input: + people: + age_35: + age: 35 + long_term_health_insurance_premiums: 10_000 + age_45: + age: 45 + long_term_health_insurance_premiums: 10_000 + age_55: + age: 55 + long_term_health_insurance_premiums: 10_000 + age_65: + age: 65 + long_term_health_insurance_premiums: 10_000 + age_75: + age: 75 + long_term_health_insurance_premiums: 10_000 + output: + eligible_long_term_care_premiums: [420, 780, 1_560, 4_160, 5_200] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.yaml index 580d1f7e672..d24d8e7ebbc 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.yaml @@ -171,3 +171,170 @@ standard_deduction: 15_750 # 58,000 - 15,750. taxable_income: 42_250 + +# Long-term care insurance premiums. +# +# 26 U.S.C. 213(d)(1)(D) counts premiums for a qualified long-term care +# insurance contract as medical care, but 213(d)(10) limits them by the +# insured person's age at the end of the year. 2025 limits (Rev. Proc. 2024-40, +# section 3.28): 40 or less $480, 41-50 $900, 51-60 $1,800, 61-70 $4,810, +# over 70 $6,020. 26 U.S.C. 162(l)(2)(C) lets the same eligible premiums into +# the self-employed health insurance deduction, and 162(l)(3) keeps that part +# off Schedule A. medicare_enrolled is false so no modeled Part B premium +# enters these examples. + +- name: Long-term care premiums count up to the limit for the insured person's age. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 72 + medicare_enrolled: false + employment_income: 50_000 + long_term_health_insurance_premiums: 8_000 + other_medical_expenses: 10_000 + output: + eligible_long_term_care_premiums: 6_020 + itemized_medical_expenses_long_term_care_premiums: 6_020 + # 10,000 + 6,020. + itemized_medical_expenses: 16_020 + # 16,020 - 7.5% of 50,000. + medical_expense_deduction: 12_270 + +- name: A dependent's contract is limited by the dependent's age, not the payer's. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + employment_income: 80_000 + is_tax_unit_head: true + parent: + age: 80 + medicare_enrolled: false + is_tax_unit_dependent: true + # Recorded on the insured person, whoever in the tax unit paid it. + long_term_health_insurance_premiums: 7_000 + tax_units: + tax_unit: + members: [head, parent] + output: + eligible_long_term_care_premiums: [0, 6_020] + itemized_medical_expenses: 6_020 + # 6,020 - 7.5% of 80,000. + medical_expense_deduction: 20 + +- name: Each spouse's contract has its own limit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 66 + medicare_enrolled: false + is_tax_unit_head: true + long_term_health_insurance_premiums: 5_000 + spouse: + age: 58 + is_tax_unit_spouse: true + long_term_health_insurance_premiums: 2_500 + tax_units: + tax_unit: + members: [head, spouse] + output: + # 61-70: 4,810. 51-60: 1,800. + eligible_long_term_care_premiums: [4_810, 1_800] + itemized_medical_expenses: 6_610 + +- name: Premiums taken as the self-employed health insurance deduction are not on Schedule A. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 55 + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 3_000 + long_term_health_insurance_premiums: 2_000 + other_medical_expenses: 500 + output: + eligible_long_term_care_premiums: 1_800 + # Form 7206 line 2. + self_employed_long_term_care_premiums: 1_800 + # Line 3: 3,000 + 1,800, under the 50,000 limit. + self_employed_health_insurance_ald: 4_800 + itemized_medical_expenses_excluded_premiums: 3_000 + itemized_medical_expenses_excluded_long_term_care_premiums: 1_800 + itemized_medical_expenses_applied_exclusion: 4_800 + itemized_medical_expenses_long_term_care_premiums: 0 + itemized_medical_expenses: 500 + +- name: Eligible premiums the earnings limit keeps out of the deduction stay on Schedule A. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 55 + is_self_employed: true + self_employment_income: 4_000 + health_insurance_premiums: 3_000 + long_term_health_insurance_premiums: 2_000 + output: + # Form 7206 line 13: 4,000 less half of self-employment tax, + # 4,000 x 0.9235 x 0.153 / 2 = 282.59, so 3,717.41. + # min(3,717.41, 3,000 + 1,800). + self_employed_health_insurance_ald: 3_717.41 + # The deduction is taken from medical premiums first: 3,000, then 717.41 + # of the 1,800 eligible long-term care premiums. + itemized_medical_expenses_excluded_premiums: 3_000 + itemized_medical_expenses_excluded_long_term_care_premiums: 717.41 + itemized_medical_expenses_applied_exclusion: 3_717.41 + # 1,800 - 717.41. + itemized_medical_expenses_long_term_care_premiums: 1_082.59 + itemized_medical_expenses: 1_082.59 + +- name: A supplied deduction cannot exclude more long-term care premiums than are eligible. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_tax_unit_head: true + health_insurance_premiums: 1_000 + long_term_health_insurance_premiums: 3_000 + other_medical_expenses: 2_000 + tax_units: + tax_unit: + members: [head] + self_employed_health_insurance_ald: 2_500 + output: + eligible_long_term_care_premiums: 900 + itemized_medical_expenses_excluded_premiums: 1_000 + # min(900, 2,500 - 1,000). + itemized_medical_expenses_excluded_long_term_care_premiums: 900 + itemized_medical_expenses_long_term_care_premiums: 0 + # The other expenses are untouched. + itemized_medical_expenses: 2_000 + +- name: A supplied subtotal holds no computed long-term care premiums. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 75 + medicare_enrolled: false + long_term_health_insurance_premiums: 3_000 + itemized_medical_expenses: 5_000 + output: + itemized_medical_expenses: 5_000 + itemized_medical_expenses_long_term_care_premiums: 0 + itemized_medical_expenses_applied_exclusion: 0 + +- name: Supplying the long-term care premiums sets the amount the computed subtotal counts. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 75 + medicare_enrolled: false + other_medical_expenses: 2_000 + itemized_medical_expenses_long_term_care_premiums: 1_000 + output: + itemized_medical_expenses: 3_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml index e2cd3149159..64e8371a130 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml @@ -218,6 +218,39 @@ dependents_self_employed_health_insurance_ald: 500 id_health_insurance_premiums_subtraction: 1_000 +- name: A long-term care deduction does not reduce a spouse's medical premiums + # The head's eligible long-term care premiums (1,800, the 2025 limit at + # age 55) are the head's whole $1,800 self-employed deduction (Form 7206 + # line 2). That part took no medical premiums, so the spouse's $3,000 of + # employee premiums all qualify. + period: 2025 + input: + people: + head: + age: 55 + self_employment_income: 50_000 + is_self_employed: true + long_term_health_insurance_premiums: 1_800 + is_tax_unit_head: true + spouse: + age: 55 + employment_income: 50_000 + health_insurance_premiums: 3_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: ID + output: + self_employed_health_insurance_ald: 1_800 + self_employed_health_insurance_ald_long_term_care_part: [1_800, 0] + # Schedule A: the spouse's 3,000 plus 1,800 - 1,800 of long-term care. + itemized_medical_expenses: 3_000 + id_qualified_health_insurance_premiums: 3_000 + - name: Dependent-paid premiums cannot reduce the parent's itemized subtraction # Idaho Code 63-3022P; 2025 Form 39R worksheet, PDF page 49, # lines 1-6 and 8: $3,000 claimant costs - $2,250 floor = $750 overlap. @@ -495,3 +528,30 @@ id_health_insurance_premiums_subtraction: [1_750, 1_750] id_deductions: [20_000, 20_000] id_taxable_income: [8_250, 8_250] + +- name: A supplied deduction below the person amounts takes medical premiums first + period: 2025 + input: + people: + head: + age: 55 + self_employment_income: 50_000 + is_self_employed: true + health_insurance_premiums: 3_000 + long_term_health_insurance_premiums: 1_800 + is_tax_unit_head: true + spouse: + age: 55 + employment_income: 50_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + self_employed_health_insurance_ald: 3_000 + households: + household: + members: [head, spouse] + state_code: ID + output: + itemized_medical_expenses_excluded_premiums: 3_000 + id_qualified_health_insurance_premiums: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml index bbb0a8b826d..2024b8f58f7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml @@ -1574,3 +1574,68 @@ # Line 21: the claimant's and spouse's own benefits, 12,000 + 6,000 + # 3,000 = 21,000. The child's 5,000 is the child's own. mi_household_resources: 21_000 + +- name: A long-term care deduction on line 30 leaves the spouse's premiums on line 31 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 55 + is_tax_unit_head: true + is_self_employed: true + self_employment_income: 50_000 + long_term_health_insurance_premiums: 1_800 + spouse: + age: 55 + is_tax_unit_spouse: true + employment_income: 50_000 + health_insurance_premiums: 3_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: MI + output: + # Income: 50,000 + 50,000 = 100,000. + # Line 30: half of self-employment tax, 50,000 x 0.9235 x 0.153 / 2 = + # 3,532.39, plus the head's self-employed health insurance deduction, + # 1,800, all of it long-term care premiums (Form 7206 line 2) at the + # 2025 limit for age 51-60. + self_employed_health_insurance_ald: 1_800 + # Line 31: the spouse's 3,000 of premiums. The deduction took none of + # them, so none leave line 31. + # 100,000 - 5,332.39 - 3,000 = 91,667.61. + mi_household_resources: 91_667.61 + +- name: A supplied deduction below the person amounts takes medical premiums first + absolute_error_margin: 0.01 + period: 2025 + input: + people: + head: + age: 55 + self_employment_income: 50_000 + is_self_employed: true + health_insurance_premiums: 3_000 + long_term_health_insurance_premiums: 1_800 + is_tax_unit_head: true + spouse: + age: 55 + employment_income: 50_000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + self_employed_health_insurance_ald: 3_000 + households: + household: + members: [head, spouse] + state_code: MI + output: + itemized_medical_expenses_excluded_premiums: 3_000 + # Line 30: 3,532.39 + 3,000 = 6,532.39. The 3,000 took all the head's + # medical premiums, so line 31 is 0: 100,000 - 6,532.39 = 93,467.61. + mi_household_resources: 93_467.61 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml index 5e3482dc63a..e2164500ed8 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml @@ -224,3 +224,54 @@ nj_agi: 40_000 # 1,000 + (5,500 + 3,000 - 0.02 * 40,000) = 8,700. nj_medical_expense_deduction: 8_700 + +- name: A dependent's long-term care deduction is not counted again in New Jersey + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + employment_income: 100_000 + dependent: + age: 72 + is_tax_unit_dependent: true + is_self_employed: true + self_employment_income: 4_000 + long_term_health_insurance_premiums: 3_000 + tax_units: + tax_unit: + members: [head, dependent] + households: + household: + members: [head, dependent] + state_code: NJ + output: + # Eligible long-term care premiums: min(3,000, 6,020 above age 70) = + # 3,000. The earned income limit is 4,000 - 4,000 x 0.9235 x 0.153 / 2 = + # 3,717.41, so the dependent's own deduction is 3,000, all of it Form + # 7206 line 2. + dependents_self_employed_health_insurance_ald: 3_000 + # The dependent's deduction took those premiums, so the subtotal holds + # only the dependent's Medicare Part B premiums: $185 x 12 = $2,220. + itemized_medical_expenses_long_term_care_premiums: 0 + itemized_medical_expenses: 2_220 + nj_agi: 104_000 + # 3,000 + max(0, 2,220 - 0.02 x 104,000) = 3,000 + 140 = 3,140. The + # deduction took none of the Part B premiums. + nj_medical_expense_deduction: 3_140 + +- name: A filer's own deduction is not removed again from the federal base + period: 2025 + input: + state_code: NJ + age: 45 + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 5_000 + other_medical_expenses: 10_000 + output: + # The federal subtotal already leaves out the 5,000 deducted; New Jersey + # deducts it separately: 5,000 + (10,000 - 0.02 x 50,000). + itemized_medical_expenses: 10_000 + nj_medical_expense_deduction: 14_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_long_term_care_insurance_premiums.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_long_term_care_insurance_premiums.yaml new file mode 100644 index 00000000000..84294c5d9b6 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_long_term_care_insurance_premiums.yaml @@ -0,0 +1,102 @@ +# Long-term care premiums the federal self-employed health insurance +# deduction took already reduced federal AGI, Ohio's starting point, so the +# Ohio worksheet counts only the rest. The federal deduction takes medical +# insurance premiums (Form 7206 line 1) first, then eligible long-term care +# premiums (line 2), limited by 26 U.S.C. 213(d)(10): $1,800 at age 51-60 in +# 2025 (Rev. Proc. 2024-40). +- name: An employee's long-term care premiums count in full + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 55 + employment_income: 50_000 + health_insurance_premiums: 3_000 + long_term_health_insurance_premiums: 2_500 + households: + household: + members: [head] + state_code: OH + output: + # No federal self-employed deduction: 2,500. + oh_long_term_care_insurance_premiums: 2_500 + +- name: A self-employed person's deducted long-term care premiums are not counted again + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 55 + is_self_employed: true + self_employment_income: 50_000 + health_insurance_premiums: 3_000 + long_term_health_insurance_premiums: 2_500 + households: + household: + members: [head] + state_code: OH + output: + # Eligible long-term care premiums = min(2,500, 1,800) = 1,800. + # Federal deduction = 3,000 + 1,800 = 4,800, below the earned income limit. + # Long-term care part = 4,800 - 3,000 = 1,800; Ohio = 2,500 - 1,800 = 700. + self_employed_health_insurance_ald_person: 4_800 + oh_long_term_care_insurance_premiums: 700 + # Medical premiums: 3,000 less the deduction's medical part (line 1), + # 3,000, = 0, and no other expenses, so the worksheet takes only the 700. + oh_unreimbursed_medical_care_expense_deduction_person: 700 + +- name: When the earned income limit binds, medical premiums take the deduction first + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 55 + is_self_employed: true + self_employment_income: 4_000 + health_insurance_premiums: 3_000 + long_term_health_insurance_premiums: 2_500 + households: + household: + members: [head] + state_code: OH + output: + # SE tax = 4,000 x 0.9235 x 0.153 = 565.18; half = 282.59. + # Earned income limit = 4,000 - 282.59 = 3,717.41 < 4,800. + # Long-term care part = 3,717.41 - 3,000 = 717.41. + # Ohio = 2,500 - 717.41 = 1,782.59. + self_employed_health_insurance_ald_person: 3_717.41 + oh_long_term_care_insurance_premiums: 1_782.59 + +- name: A supplied deduction is removed once across the two premium lines + period: 2025 + input: + state_code: OH + age: 55 + is_self_employed: true + self_employment_income: 120_000 + health_insurance_premiums_without_medicare_part_b: 3_000 + long_term_health_insurance_premiums: 3_000 + self_employed_health_insurance_ald_person: 2_300 + output: + # Line 1 is 0 and line 2 is 1,800, so the long-term care part is 1,800 + # and the other 500 is medical: 3,000 - 500 and 3,000 - 1,800. + oh_medical_care_insurance_premiums: 2_500 + oh_long_term_care_insurance_premiums: 1_200 + +- name: A supplied deduction for a person who is not self-employed is removed once + period: 2025 + input: + state_code: OH + age: 55 + employment_income: 50_000 + health_insurance_premiums: 300 + long_term_health_insurance_premiums: 1_000 + self_employed_health_insurance_ald_person: 800 + output: + # The medical premiums absorb 300 of the 800; the other 500 comes off + # the long-term care premiums. + oh_medical_care_insurance_premiums: 0 + oh_long_term_care_insurance_premiums: 500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml index 77175d10352..1421ec8274c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml @@ -53,3 +53,39 @@ health_insurance_premiums: 5_000 output: oh_medical_care_insurance_premiums: 0 + +- name: A deduction that took only long-term care premiums leaves medical premiums + period: 2025 + input: + state_code: OH + age: 55 + is_self_employed: true + self_employment_income: 120_000 + health_insurance_premiums_without_medicare_part_b: 3_000 + long_term_health_insurance_premiums: 1_800 + output: + # The survey premium input is not on Form 7206 line 1 + # (self_employed_health_insurance_premiums is 0), so the whole $1,800 + # deduction is line 2: eligible long-term care premiums at the 2025 + # limit for age 51-60. It took no medical premiums. + self_employed_health_insurance_ald_person: 1_800 + oh_medical_care_insurance_premiums: 3_000 + # 1,800 - 1,800 taken federally. + oh_long_term_care_insurance_premiums: 0 + oh_unreimbursed_medical_care_expense_deduction: 3_000 + +- name: A long-term care deduction leaves Medicare Part B premiums + period: 2025 + input: + state_code: OH + age: 66 + medicare_enrolled: true + is_self_employed: true + self_employment_income: 120_000 + long_term_health_insurance_premiums: 4_000 + output: + # Eligible long-term care premiums: min(4,000, 4,810 at age 61-70) = + # 4,000, all on line 2. Part B: $185 x 12 = $2,220, with no income-related + # adjustment (no income two years earlier), and none of it on line 1. + self_employed_health_insurance_ald_person: 4_000 + oh_medical_care_insurance_premiums: 2_220 diff --git a/policyengine_us/tests/policy/baseline/household/expense/health/self_employed_long_term_care_premiums.yaml b/policyengine_us/tests/policy/baseline/household/expense/health/self_employed_long_term_care_premiums.yaml new file mode 100644 index 00000000000..06b150bcd48 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/expense/health/self_employed_long_term_care_premiums.yaml @@ -0,0 +1,42 @@ +# Form 7206 line 2: for each person covered by a qualified long-term care +# insurance contract, the premiums paid or the 26 U.S.C. 213(d)(10) limit for +# that person's age, whichever is less (26 U.S.C. 162(l)(2)(C)). 2024 limits +# (Rev. Proc. 2023-34, section 3.28): 61-70 $4,710, over 70 $5,880. + +- name: A self-employed person's eligible long-term care premiums count toward the deduction. + period: 2024 + absolute_error_margin: 0.01 + input: + age: 64 + is_self_employed: true + self_employment_income: 40_000 + long_term_health_insurance_premiums: 6_000 + output: + self_employed_long_term_care_premiums: 4_710 + self_employed_health_insurance_ald_person: 4_710 + +- name: Premiums under the limit count in full, alongside health insurance premiums. + period: 2024 + absolute_error_margin: 0.01 + input: + age: 72 + medicare_enrolled: false + is_self_employed: true + self_employment_income: 40_000 + health_insurance_premiums: 7_000 + long_term_health_insurance_premiums: 2_000 + output: + self_employed_long_term_care_premiums: 2_000 + # Line 3: 7,000 + 2,000. + self_employed_health_insurance_ald_person: 9_000 + +- name: A person who is not self-employed has none. + period: 2024 + absolute_error_margin: 0.01 + input: + age: 64 + employment_income: 40_000 + long_term_health_insurance_premiums: 6_000 + output: + self_employed_long_term_care_premiums: 0 + self_employed_health_insurance_ald_person: 0 diff --git a/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py b/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py index a5a90d8dceb..8b9de251ccc 100644 --- a/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py +++ b/policyengine_us/tests/unit/test_mt_medical_premium_deduction_invariants.py @@ -55,8 +55,10 @@ Montana AGI is unchanged by those increases (asserted). 5. Pre-tax premiums never change lines 2 or 3. 6. Line 1 base: other expenses equal federal itemized medical expenses plus - the self-employed premiums those exclude, less the premiums. Line 1 - therefore tracks the federal definition apart from the premiums. + the self-employed premiums those exclude, less the medical and eligible + long-term care premiums (a dependent's less the long-term care part of the + dependent's own deduction). Line 1 therefore tracks the federal + definition apart from the premiums. 7. Differential (2021-2023): for single filers whose deduction is recorded per person, the joint and separate variants agree. 8. From 2024 the joint deduction is the federal medical expense deduction and @@ -301,6 +303,8 @@ def _situation(units, year, mode, unit_inputs=None): PERSON_VARIABLES = [ "other_medical_expenses", "long_term_health_insurance_premiums", + "eligible_long_term_care_premiums", + "self_employed_health_insurance_ald_long_term_care_part", "medical_expense_health_insurance_premiums", "self_employed_health_insurance_ald_person", "mt_eligible_medical_insurance_premiums", @@ -441,15 +445,20 @@ def _check(units, year, mode): ) # 6. Line 1 base tracks the federal definition apart from the premiums. - # The generator never supplies the subtotal, so it holds no long-term care - # premiums. - assert (run["itemized_medical_expenses_long_term_care_premiums"] == 0).all() + # The computed subtotal holds the members' eligible long-term care + # premiums less the part the self-employed deduction took, and the + # applied exclusion restores that part. A dependent's own deduction + # took the dependent's long-term care part, which stays out. + eligible_long_term_care = ( + run["eligible_long_term_care_premiums"] + - ~filer * run["self_employed_health_insurance_ald_long_term_care_part"] + ) np.testing.assert_allclose( _unit_sum(run, other), run["itemized_medical_expenses"] + run["itemized_medical_expenses_applied_exclusion"] - _unit_sum(run, premium) - - run["itemized_medical_expenses_long_term_care_premiums"], + - _unit_sum(run, eligible_long_term_care), atol=TOLERANCE, ) @@ -543,11 +552,11 @@ def test_seeded_population(year, mode): def _check_net_subtotal(run, units, year, mode): """9. A net Schedule A subtotal gives the computed path's deduction. - Supply, for every return, the subtotal as Schedule A line 1 would hold it - (the computed subtotal plus the filers' long-term care premiums the - self-employed deduction did not take), the exclusion as all premiums that - deduction took (medical first), and the long-term care premiums still - inside. Montana's deduction must not change. + Supply, for every return, the computed path's own values: the subtotal, + which already holds the eligible long-term care premiums the + self-employed deduction did not take, the applied exclusion of all + premiums that deduction took, and the long-term care premiums still + inside the subtotal. Montana's deduction must not change. """ filer = run["role"] != "dependent" return_deduction = np.maximum(run["self_employed_health_insurance_ald"], 0) @@ -558,16 +567,18 @@ def _check_net_subtotal(run, units, year, mode): long_term_care_excluded = np.minimum( filer_long_term_care, return_deduction - medical_excluded ) - inside = filer_long_term_care - long_term_care_excluded net = _run( units, year, mode, { - "itemized_medical_expenses": run["itemized_medical_expenses"] + inside, - "itemized_medical_expenses_applied_exclusion": medical_excluded - + long_term_care_excluded, - "itemized_medical_expenses_long_term_care_premiums": inside, + "itemized_medical_expenses": run["itemized_medical_expenses"], + "itemized_medical_expenses_applied_exclusion": run[ + "itemized_medical_expenses_applied_exclusion" + ], + "itemized_medical_expenses_long_term_care_premiums": run[ + "itemized_medical_expenses_long_term_care_premiums" + ], }, ) np.testing.assert_allclose(net["mt_agi_joint"], run["mt_agi_joint"], atol=TOLERANCE) @@ -578,8 +589,8 @@ def _check_net_subtotal(run, units, year, mode): ) # Non-vacuity: with supplied deductions, some exclusions cover long-term # care premiums on returns with dependents' premiums, where taking the - # exclusion from every member's premiums would differ. A derived - # deduction never exceeds the filers' own medical premiums. + # exclusion from every member's premiums would differ. The derived mode + # is left out here; the supplied modes reach these returns. if mode != "derived": dependent_premiums = _unit_sum( run, ~filer * run["medical_expense_health_insurance_premiums"] diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_long_term_care_part.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_long_term_care_part.py new file mode 100644 index 00000000000..35fa44640e9 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_long_term_care_part.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class self_employed_health_insurance_ald_long_term_care_part(Variable): + value_type = float + entity = Person + label = "Long-term care part of a person's self-employed health insurance deduction" + unit = USD + documentation = ( + "The part of a person's self-employed health insurance deduction " + "(self_employed_health_insurance_ald_person) that took eligible " + "long-term care premiums (Form 7206 line 2): the deduction left after " + "the person's medical insurance premiums (line 1), limited to the " + "person's line 2 premiums. The rest of the deduction took no " + "long-term care premiums, so a supplied person deduction above lines " + "1 and 2 adds nothing here." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l_2_C", + "https://www.irs.gov/pub/irs-prior/f7206--2024.pdf#page=1", # Lines 1-3 + ) + + def formula(person, period, parameters): + deduction = max_(0, person("self_employed_health_insurance_ald_person", period)) + medical_part = min_( + person("self_employed_health_insurance_premiums", period), deduction + ) + return min_( + person("self_employed_long_term_care_premiums", period), + deduction - medical_part, + ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py index 939b3ba1e96..166ea120079 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py @@ -10,8 +10,9 @@ class self_employed_health_insurance_ald_person(Variable): "Personal above-the-line deduction for self-employed health insurance " "premiums: the smaller of the premiums and the earned income from the " "business under which the plan is established (Form 7206 line 14). " - "The premiums exclude qualified long-term care insurance (line 2), " - "which the model does not include in self-employed premiums." + "The premiums are the medical insurance premiums (line 1) plus " + "eligible long-term care premiums up to the 26 U.S.C. 213(d)(10) " + "limit (line 2)." ) definition_period = YEAR reference = ( @@ -21,7 +22,16 @@ class self_employed_health_insurance_ald_person(Variable): ) def formula(person, period, parameters): - premiums = person("self_employed_health_insurance_premiums", period) + # Line 3: health insurance premiums (line 1) plus eligible long-term + # care premiums (line 2). + premiums = add( + person, + period, + [ + "self_employed_health_insurance_premiums", + "self_employed_long_term_care_premiums", + ], + ) earned_income_limit = person( "self_employed_health_insurance_earned_income_limit", period ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/eligible_long_term_care_premiums.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/eligible_long_term_care_premiums.py new file mode 100644 index 00000000000..0fbaa5a2a63 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/eligible_long_term_care_premiums.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * + + +class eligible_long_term_care_premiums(Variable): + value_type = float + entity = Person + label = "Eligible long-term care premiums" + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/213#d_1_D", + "https://www.law.cornell.edu/uscode/text/26/213#d_10", + "https://www.irs.gov/pub/irs-prior/i1040sca--2022.pdf#page=2", + "https://www.irs.gov/pub/irs-prior/p502--2018.pdf#page=11", + ) + documentation = ( + "Premiums for a person's qualified long-term care insurance contract " + "that count as medical care under 26 U.S.C. 213(d)(1)(D): the " + "premiums up to the 213(d)(10) limit for the person's age at the end " + "of the year. The limit applies to each insured person, so " + "long_term_health_insurance_premiums is recorded on the person the " + "contract covers. These premiums enter the Schedule A medical " + "subtotal and, for a self-employed person, the self-employed health " + "insurance deduction (26 U.S.C. 162(l)(2)(C))." + ) + + def formula(person, period, parameters): + premiums = person("long_term_health_insurance_premiums", period) + p = parameters(period).gov.irs.deductions.itemized.medical + # The limit depends on the age, at the end of the year, of the person + # for whom the premiums were paid (Schedule A instructions, line 1). + limit = p.long_term_care_premium_limit.calc(person("age", period), right=True) + return min_(premiums, limit) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py index f953f779949..c046795c4f2 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py @@ -11,6 +11,7 @@ class itemized_medical_expenses(Variable): "https://www.law.cornell.edu/uscode/text/26/213", "https://www.law.cornell.edu/uscode/text/26/162#l_3", "https://www.irs.gov/instructions/i1040sca", + "https://www.irs.gov/pub/irs-prior/i1040sca--2022.pdf#page=2", ] documentation = ( "Medical expenses counted before applying the itemized medical " @@ -18,9 +19,12 @@ class itemized_medical_expenses(Variable): "to include amounts paid for diagnosis, cure, mitigation, treatment, " "or prevention of disease; transportation primarily for essential " "medical care; qualified long-term care services; and insurance " - "premiums covering medical care. Current modeling uses health " - "insurance premiums and other medical expenses, excluding general " - "over-the-counter health expenses. Premiums deducted through the " + "premiums covering medical care, with long-term care insurance " + "premiums counted up to the 213(d)(10) limit for the insured person's " + "age. Current modeling uses health insurance premiums, those eligible " + "long-term care premiums (recorded on the insured person) and other " + "medical expenses, excluding general over-the-counter health " + "expenses. Premiums deducted through the " "filer's self-employed health insurance deduction are excluded under " "IRC Section 162(l)(3) before the medical expense floor is applied. " "Premium inputs use payer attribution: each person reports premiums " @@ -50,5 +54,10 @@ def formula(tax_unit, period, parameters): excluded_premiums = tax_unit( "itemized_medical_expenses_excluded_premiums", period ) + # Eligible long-term care premiums less the part the same deduction + # took. + long_term_care_premiums = tax_unit( + "itemized_medical_expenses_long_term_care_premiums", period + ) other_expenses = add(tax_unit, period, ["other_medical_expenses"]) - return premiums - excluded_premiums + other_expenses + return premiums - excluded_premiums + long_term_care_premiums + other_expenses diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_applied_exclusion.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_applied_exclusion.py index e0078156600..a2b0efd8469 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_applied_exclusion.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_applied_exclusion.py @@ -34,4 +34,13 @@ def formula(tax_unit, period, parameters): tax_unit.simulation, "itemized_medical_expenses", period ): return 0 - return tax_unit("itemized_medical_expenses_excluded_premiums", period) + # Medical insurance premiums and eligible long-term care premiums the + # self-employed health insurance deduction took. + return add( + tax_unit, + period, + [ + "itemized_medical_expenses_excluded_premiums", + "itemized_medical_expenses_excluded_long_term_care_premiums", + ], + ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_long_term_care_premiums.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_long_term_care_premiums.py new file mode 100644 index 00000000000..7044738ebe4 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_long_term_care_premiums.py @@ -0,0 +1,39 @@ +from policyengine_us.model_api import * + + +class itemized_medical_expenses_excluded_long_term_care_premiums(Variable): + value_type = float + entity = TaxUnit + label = "Self-employed long-term care premiums excluded from Schedule A" + unit = USD + definition_period = YEAR + reference = [ + "https://www.law.cornell.edu/uscode/text/26/162#l_2_C", + "https://www.law.cornell.edu/uscode/text/26/162#l_3", + "https://www.irs.gov/pub/irs-prior/i1040sca--2022.pdf#page=2", + ] + documentation = ( + "The part of the federal self-employed health insurance deduction " + "left after the head's and spouse's medical insurance premiums " + "(itemized_medical_expenses_excluded_premiums), limited to their " + "eligible long-term care premiums. 26 U.S.C. 162(l)(3) keeps premiums " + "taken as that deduction off Schedule A. The split is for the return " + "as a whole: it takes the deduction from the head's and spouse's " + "pooled medical insurance premiums first, so it can book one filer's " + "long-term care deduction against the other filer's medical premiums. " + "Any split leaves the Schedule A subtotal unchanged. A supplied " + "subtotal carries only the total exclusion, and Montana's 2021-2023 " + "schedule recovers its medical part by this same pooled rule, so the " + "computed and supplied paths agree. Each person's own long-term care " + "part is self_employed_health_insurance_ald_long_term_care_part." + ) + + def formula(tax_unit, period, parameters): + filer_premiums = tax_unit_non_dep_sum( + "eligible_long_term_care_premiums", tax_unit, period + ) + deduction = max_(0, tax_unit("self_employed_health_insurance_ald", period)) + medical_premiums = tax_unit( + "itemized_medical_expenses_excluded_premiums", period + ) + return min_(filer_premiums, deduction - medical_premiums) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_premiums.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_premiums.py index 05fdcb6893d..3c09e3ccfc4 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_premiums.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_premiums.py @@ -4,7 +4,7 @@ class itemized_medical_expenses_excluded_premiums(Variable): value_type = float entity = TaxUnit - label = "Self-employed health insurance premiums excluded from Schedule A" + label = "Self-employed medical insurance premiums excluded from Schedule A" unit = USD definition_period = YEAR reference = [ @@ -27,4 +27,7 @@ def formula(tax_unit, period, parameters): "medical_expense_health_insurance_premiums", tax_unit, period ) se_health_insurance_ald = tax_unit("self_employed_health_insurance_ald", period) + # The deduction is taken from medical insurance premiums first; any + # rest covers eligible long-term care premiums + # (itemized_medical_expenses_excluded_long_term_care_premiums). return min_(filer_premiums, max_(0, se_health_insurance_ald)) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_long_term_care_premiums.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_long_term_care_premiums.py index b6ed728983e..33a68a8acdd 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_long_term_care_premiums.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_long_term_care_premiums.py @@ -4,24 +4,49 @@ class itemized_medical_expenses_long_term_care_premiums(Variable): value_type = float entity = TaxUnit - label = "Long-term care premiums included in supplied itemized medical expenses" + label = "Long-term care premiums in itemized medical expenses" unit = USD definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/213#d_10", - "https://www.irs.gov/instructions/i1040sca", + "https://www.law.cornell.edu/uscode/text/26/162#l_3", + "https://www.irs.gov/pub/irs-prior/i1040sca--2022.pdf#page=2", ) documentation = ( - "The long-term care insurance premiums that a supplied " - "itemized_medical_expenses includes, such as eligible premiums " - "counted on Schedule A line 1 and not taken as the self-employed " - "health insurance deduction. The computed subtotal counts no " - "long-term care premiums, so leave this at 0 unless you supply a " - "subtotal that includes them. States that deduct long-term care " - "premiums separately remove this amount from the subtotal first. With " - "a subtotal that leaves out premiums taken as the self-employed " - "deduction, follow the split itemized_medical_expenses_applied_exclusion " - "uses, whatever the return itself did: this is the eligible long-term " - "care premiums less any part of that exclusion above the head's and " - "spouse's medical insurance premiums." + "The eligible long-term care insurance premiums inside the Schedule A " + "medical subtotal (itemized_medical_expenses): every member's " + "premiums up to the 26 U.S.C. 213(d)(10) limit for that member's age, " + "less the part the head's and spouse's self-employed health insurance " + "deduction took and the long-term care part of each dependent's own " + "deduction. " + "The computed subtotal adds this amount, so supplying it sets the " + "long-term care premiums the subtotal counts. A supplied subtotal " + "bypasses this formula, and the amount is 0 unless it is supplied " + "too: the long-term care premiums inside the supplied subtotal. With " + "a supplied subtotal that leaves out premiums taken as the " + "self-employed deduction, follow the split " + "itemized_medical_expenses_applied_exclusion uses, whatever the return " + "itself did: the eligible long-term care premiums less any part of " + "that exclusion above the head's and spouse's medical insurance " + "premiums. States that deduct long-term care premiums separately take " + "this amount out of the subtotal first." ) + + def formula(tax_unit, period, parameters): + if has_input_for_period( + tax_unit.simulation, "itemized_medical_expenses", period + ): + return 0 + premiums = add(tax_unit, period, ["eligible_long_term_care_premiums"]) + excluded = tax_unit( + "itemized_medical_expenses_excluded_long_term_care_premiums", period + ) + # A dependent's own deduction, on the dependent's own return, took + # its long-term care part (Form 7206 line 2) out of the dependent's + # premiums (26 U.S.C. 162(l)(3)). + person = tax_unit.members + dependents_part = tax_unit.sum( + person("self_employed_health_insurance_ald_long_term_care_part", period) + * person("is_tax_unit_dependent", period) + ) + return premiums - excluded - dependents_part diff --git a/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_qualified_health_insurance_premiums.py b/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_qualified_health_insurance_premiums.py index c523305214a..6e6b0115a6d 100644 --- a/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_qualified_health_insurance_premiums.py +++ b/policyengine_us/variables/gov/states/id/tax/income/subtractions/id_qualified_health_insurance_premiums.py @@ -31,5 +31,25 @@ def formula(tax_unit, period, parameters): ) # Dependents' federal deductions concern their excluded payments, # so they must not reduce the filers' qualifying premiums again. - deducted_elsewhere = tax_unit("self_employed_health_insurance_ald", period) + # The deduction's long-term care part (Form 7206 line 2) took no + # medical insurance premiums, so it does not reduce them. + # The return's deduction takes the filers' medical parts (Form 7206 + # line 1) before their long-term care parts (line 2), as + # itemized_medical_expenses_excluded_premiums does, so a supplied + # deduction below the person amounts takes medical premiums first. + person = tax_unit.members + filer = not_(person("is_tax_unit_dependent", period)) + own_deduction = max_( + 0, person("self_employed_health_insurance_ald_person", period) + ) + own_long_term_care_part = person( + "self_employed_health_insurance_ald_long_term_care_part", period + ) + medical_parts = tax_unit.sum((own_deduction - own_long_term_care_part) * filer) + deduction = max_(0, tax_unit("self_employed_health_insurance_ald", period)) + long_term_care_taken = min_( + tax_unit.sum(own_long_term_care_part * filer), + max_(0, deduction - medical_parts), + ) + deducted_elsewhere = deduction - long_term_care_taken return max_(0, premiums - deducted_elsewhere) diff --git a/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py b/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py index c045589e87a..710da4cac95 100644 --- a/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py +++ b/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py @@ -161,17 +161,40 @@ def formula(tax_unit, period, parameters): premiums = tax_unit_non_dep_add(tax_unit, period, ["health_insurance_premiums"]) # "Do not include any insurance premiums deducted on lines 21 or 30" # (2025 MI-1040 book, page 33). Line 30 includes the self-employed - # health insurance deduction (Schedule 1 line 17), so those premiums - # leave line 31 and each premium dollar is subtracted once. + # health insurance deduction (Schedule 1 line 17). Its medical part + # (Form 7206 line 1) took premiums that are in + # health_insurance_premiums, so those leave line 31 and each premium + # dollar is subtracted once. Its long-term care part (line 2) took + # long-term care premiums, which line 31 does not hold, so it takes + # nothing off line 31: the head's and spouse's deduction less the + # long-term care parts of their own deductions comes off. # A supplied tax-unit deduction is assumed to cover premiums also - # represented in health_insurance_premiums. The aggregate inputs do - # not identify the overlap if the deducted plan's premiums are omitted. + # represented in health_insurance_premiums, apart from the long-term + # care part the person amounts show it reached after their medical + # parts. The aggregate inputs do not + # identify the overlap if the deducted plan's premiums are omitted. # Line 21 nets out Medicare premiums withheld from Social Security or # railroad retirement benefits (MCL 206.510(1)(h)). Here line 21 has # gross benefits and line 31 the premiums, which subtracts them once # as well. + person = tax_unit.members + filer = not_(person("is_tax_unit_dependent", period)) + own_deduction = max_( + 0, person("self_employed_health_insurance_ald_person", period) + ) + own_long_term_care_part = person( + "self_employed_health_insurance_ald_long_term_care_part", period + ) + medical_parts = tax_unit.sum((own_deduction - own_long_term_care_part) * filer) + return_deduction = max_( + 0, tax_unit("self_employed_health_insurance_ald", period) + ) + long_term_care_taken = min_( + tax_unit.sum(own_long_term_care_part * filer), + max_(0, return_deduction - medical_parts), + ) premiums_on_line_30 = ( - tax_unit("self_employed_health_insurance_ald", period) + return_deduction - long_term_care_taken if "self_employed_health_insurance_ald" in deductions else 0 ) diff --git a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py index 2ac5a575634..e8e57c100b9 100644 --- a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py +++ b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py @@ -39,12 +39,19 @@ def formula(tax_unit, period, parameters): ) # C.54A:3-3 excludes premiums counted by the separate SE deduction # under C.54A:3-5, including dependents' deductions recognized by NJ. - # The federal base already excludes the filers' deduction. Apply - # each dependent's deduction only to that person's own paid premiums. + # The federal base already excludes the filers' deduction and the + # long-term care part (Form 7206 line 2) of each dependent's own + # deduction. Remove the medical part (line 1) of each dependent's + # deduction (the deduction less its long-term care part), limited to + # that person's own paid medical premiums. person = tax_unit.members + dependent_medical_part = min_( + person("self_employed_health_insurance_ald_excluded_premiums", period), + max_(0, person("self_employed_health_insurance_ald_person", period)) + - person("self_employed_health_insurance_ald_long_term_care_part", period), + ) excluded_dependent_premiums = tax_unit.sum( - person("self_employed_health_insurance_ald_excluded_premiums", period) - * person("is_tax_unit_dependent", period) + dependent_medical_part * person("is_tax_unit_dependent", period) ) # New Jersey includes salary-reduction cafeteria-plan premiums in # taxable wages (TB-39(R)), so these employee-paid premiums also diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_long_term_care_insurance_premiums.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_long_term_care_insurance_premiums.py new file mode 100644 index 00000000000..aa4bc562b6b --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_long_term_care_insurance_premiums.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class oh_long_term_care_insurance_premiums(Variable): + value_type = float + entity = Person + label = "Ohio long-term care insurance premiums for the unreimbursed medical care deduction" + unit = USD + definition_period = YEAR + reference = ( + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10) + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet + ) + documentation = ( + "Long-term care insurance premiums for the Ohio unreimbursed medical " + "care deduction, less the part of the person's federal self-employed " + "health insurance deduction that took them. That deduction already " + "reduced federal adjusted gross income, Ohio's starting point, so the " + "same premiums are not deducted twice. The federal deduction is taken " + "from medical insurance premiums first, as " + "itemized_medical_expenses_excluded_premiums does." + ) + defined_for = StateCode.OH + + def formula(person, period, parameters): + premiums = person("long_term_health_insurance_premiums", period) + # The long-term care part (Form 7206 line 2) of the person's + # deduction, the same part oh_medical_care_insurance_premiums leaves + # out. Any rest of a supplied deduction that the medical premiums + # cannot absorb also comes off here, so the two variables together + # remove the deduction once. + deduction = max_(0, person("self_employed_health_insurance_ald_person", period)) + long_term_care_part = person( + "self_employed_health_insurance_ald_long_term_care_part", period + ) + unabsorbed = max_( + 0, + deduction + - long_term_care_part + - person("medical_expense_health_insurance_premiums", period), + ) + return max_(premiums - long_term_care_part - unabsorbed, 0) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_medical_care_insurance_premiums.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_medical_care_insurance_premiums.py index 3094d5705a6..e51f8a04c9f 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_medical_care_insurance_premiums.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_medical_care_insurance_premiums.py @@ -21,7 +21,13 @@ def formula(person, period, parameters): premiums = person("medical_expense_health_insurance_premiums", period) # Premiums already deducted in computing federal AGI are not deducted # again (worksheet line 1 note). - self_employed_deduction = person( - "self_employed_health_insurance_ald_person", period + # Only the medical insurance part (Form 7206 line 1) of the + # deduction: the deduction less its long-term care part (line 2), + # which comes off oh_long_term_care_insurance_premiums. + self_employed_deduction = max_( + 0, person("self_employed_health_insurance_ald_person", period) ) - return max_(premiums - self_employed_deduction, 0) + medical_part = self_employed_deduction - person( + "self_employed_health_insurance_ald_long_term_care_part", period + ) + return max_(premiums - medical_part, 0) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py index 5dc4426d205..aaca92f259c 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py @@ -20,5 +20,5 @@ class oh_unreimbursed_medical_care_expense_deduction_person(Variable): adds = [ "oh_insured_unreimbursed_medical_care_expenses_person", "oh_uninsured_unreimbursed_medical_care_expenses", - "long_term_health_insurance_premiums", + "oh_long_term_care_insurance_premiums", ] diff --git a/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py index c238be6ff58..442a9bd5ab8 100644 --- a/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py @@ -8,12 +8,19 @@ class long_term_health_insurance_premiums(Variable): unit = USD definition_period = YEAR documentation = ( - "Annual person-paid long-term care insurance premiums excluding pretax " - "payroll deductions and employer-paid premiums. Pretax payroll payments " - "belong only in pre_tax_health_insurance_premiums. The pretax and non- " - "pretax sets are disjoint; total person-paid premiums equal pretax " - "payments plus the applicable non-pretax aggregate. This component must " - "be counted only once when included in a broader premium input." + "Annual long-term care insurance premiums paid for this person's own " + "contract, excluding pretax payroll deductions and employer-paid " + "premiums. Unlike the other premium inputs, which sit on the person " + "who paid them, record these on the insured person, whoever in the " + "tax unit paid them: 26 U.S.C. 213(d)(10) limits the premiums that " + "count as medical care by the insured person's age. The model treats " + "the insured person as the payer, so these premiums enter only that " + "person's own self-employed health insurance deduction. Pretax payroll " + "payments belong only in pre_tax_health_insurance_premiums. The " + "pretax and non-pretax sets are disjoint; total person-paid premiums " + "equal pretax payments plus the applicable non-pretax aggregate. This " + "component must be counted only once when included in a broader " + "premium input." ) # No dataset fills this input, and CMS's health expenditure projections # have no long-term care insurance premium series. 26 U.S.C. diff --git a/policyengine_us/variables/household/expense/health/self_employed_long_term_care_premiums.py b/policyengine_us/variables/household/expense/health/self_employed_long_term_care_premiums.py new file mode 100644 index 00000000000..0eb73a9f3ba --- /dev/null +++ b/policyengine_us/variables/household/expense/health/self_employed_long_term_care_premiums.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class self_employed_long_term_care_premiums(Variable): + value_type = float + entity = Person + label = "Self-employed long-term care insurance premiums" + unit = USD + documentation = ( + "Eligible long-term care premiums that count toward the self-employed " + "health insurance deduction (Form 7206 line 2). 26 U.S.C. " + "162(l)(2)(C) takes in a qualified long-term care insurance contract " + "only up to the 26 U.S.C. 213(d)(10) limit for each person covered. " + "long_term_health_insurance_premiums is recorded on the insured " + "person, and the model treats that person as the payer, so a " + "self-employed person's deduction takes in that person's own contract." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l_2_C", + "https://www.irs.gov/pub/irs-prior/f7206--2024.pdf#page=1", # Line 2 + ) + defined_for = "is_self_employed" + adds = ["eligible_long_term_care_premiums"]