From 652b8e04757d158b7828a9bcafbbcaed94330622 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 18 Oct 2026 14:28:42 -0400 Subject: [PATCH 01/13] Count eligible long-term care premiums in Schedule A medical expenses 26 U.S.C. 213(d)(1)(D) and (d)(10): premiums for a qualified long-term care insurance contract count as medical care up to an age-based limit per insured person. 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. Co-Authored-By: Claude Opus 5.5 --- .../ltc-premiums-itemized-medical.fixed.md | 1 + .../medical/long_term_care_premium_limit.yaml | 177 ++++++++++++++++++ ...lf_employed_health_insurance_ald_person.py | 22 ++- .../eligible_long_term_care_premiums.py | 33 ++++ .../itemizing/itemized_medical_expenses.py | 17 +- ...ized_medical_expenses_applied_exclusion.py | 11 +- ...penses_excluded_long_term_care_premiums.py | 34 ++++ ...ized_medical_expenses_excluded_premiums.py | 5 +- ...edical_expenses_long_term_care_premiums.py | 37 ++++ .../long_term_health_insurance_premiums.py | 19 +- .../self_employed_long_term_care_premiums.py | 24 +++ 11 files changed, 365 insertions(+), 15 deletions(-) create mode 100644 changelog.d/ltc-premiums-itemized-medical.fixed.md create mode 100644 policyengine_us/parameters/gov/irs/deductions/itemized/medical/long_term_care_premium_limit.yaml create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/eligible_long_term_care_premiums.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_excluded_long_term_care_premiums.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_long_term_care_premiums.py create mode 100644 policyengine_us/variables/household/expense/health/self_employed_long_term_care_premiums.py 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..574acbadd4d --- /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. 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/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 8ecda97461a..c7232f0d9c0 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 @@ -6,11 +6,27 @@ class self_employed_health_insurance_ald_person(Variable): entity = Person label = "Self-employed health insurance ALD for each person" unit = USD - documentation = "Personal above-the-line deduction for self-employed health insurance contributions." + documentation = ( + "Personal above-the-line deduction for self-employed health insurance " + "premiums, including eligible long-term care premiums (Form 7206 " + "lines 1 to 3)." + ) definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/162#l" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/162#l", + "https://www.irs.gov/pub/irs-prior/f7206--2024.pdf#page=1", # Lines 1-3 + ) def formula(person, period, parameters): earnings = max_(0, person("total_self_employment_income", period)) - 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", + ], + ) return min_(earnings, premiums) 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 91538c9b38a..f7be87931b4 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 " @@ -39,5 +43,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 f034c37903a..7334895915a 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 @@ -26,4 +26,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..63c1601a4af --- /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,34 @@ +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. Taking the deduction from " + "medical insurance premiums first leaves the Schedule A subtotal " + "unchanged; it fixes which premiums a state that treats long-term " + "care premiums separately sees as already deducted." + ) + + 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 c07cab1c17e..6a14fb5e408 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 = [ @@ -26,4 +26,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 new file mode 100644 index 00000000000..6f969e3a944 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses_long_term_care_premiums.py @@ -0,0 +1,37 @@ +from policyengine_us.model_api import * + + +class itemized_medical_expenses_long_term_care_premiums(Variable): + value_type = float + entity = TaxUnit + 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.law.cornell.edu/uscode/text/26/162#l_3", + "https://www.irs.gov/pub/irs-prior/i1040sca--2022.pdf#page=2", + ) + documentation = ( + "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 self-employed health insurance deduction took. " + "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. " + "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 + ) + return premiums - excluded 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"] From e29589dbd785f5f70a49c1615f918196a580bff2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 18 Oct 2026 14:43:44 -0400 Subject: [PATCH 02/13] Test eligible long-term care premiums against the Rev. Proc. limits Co-Authored-By: Claude Opus 5.5 --- .../eligible_long_term_care_premiums.yaml | 234 ++++++++++++++++++ .../itemizing/itemized_medical_expenses.yaml | 164 ++++++++++++ ...self_employed_long_term_care_premiums.yaml | 42 ++++ 3 files changed, 440 insertions(+) create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/eligible_long_term_care_premiums.yaml create mode 100644 policyengine_us/tests/policy/baseline/household/expense/health/self_employed_long_term_care_premiums.yaml 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..96c82dcfdab 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,167 @@ 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: + # min(4,000, 3,000 + 1,800). + self_employed_health_insurance_ald: 4_000 + # The deduction is taken from medical premiums first: 3,000, then 1,000 + # of the 1,800 eligible long-term care premiums. + itemized_medical_expenses_excluded_premiums: 3_000 + itemized_medical_expenses_excluded_long_term_care_premiums: 1_000 + itemized_medical_expenses_applied_exclusion: 4_000 + itemized_medical_expenses_long_term_care_premiums: 800 + itemized_medical_expenses: 800 + +- 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/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 From 68f1371e70412f80034b3232cf483a2e787ec833 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 18 Oct 2026 14:59:03 -0400 Subject: [PATCH 03/13] Add property tests for eligible long-term care premiums Differential check of the limit parameter against the Rev. Proc. tables, plus accounting, conservation, ordering, bounds and monotonicity properties over generated tax units. Co-Authored-By: Claude Opus 5.5 --- .../test_long_term_care_premium_properties.py | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 policyengine_us/tests/core/test_long_term_care_premium_properties.py 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..78bfeafd85c --- /dev/null +++ b/policyengine_us/tests/core/test_long_term_care_premium_properties.py @@ -0,0 +1,418 @@ +"""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 + other expenses, and the + long-term care premiums inside it lie between the dependents' eligible + premiums and all members' eligible premiums. +4. Conservation (derived deduction): every eligible premium is deducted once, + on Schedule A or as the self-employed deduction, never both. +5. The derived person deduction is min(max(0, earnings), health premiums + + eligible long-term care premiums) for the self-employed, else 0. +6. Monotonicity: raising long-term care premiums never lowers the subtotal + and raises it by at most the rise in eligible premiums; for units with no + 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", + "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) + 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. + inside = run["itemized_medical_expenses_long_term_care_premiums"] + all_eligible = unit_sum(eligible) + np.testing.assert_allclose( + inside, all_eligible - long_term_care_part, atol=TOLERANCE + ) + assert (inside >= unit_sum(eligible * ~filer.astype(bool)) - 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. + np.testing.assert_allclose( + subtotal - unit_sum(other) + run["self_employed_health_insurance_ald"], + 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(np.maximum(earnings, 0), 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() + no_deduction = (deduction[base] == 0) & (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 + ) From 740261da703cd27533f843787eca7faed70410a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 21:17:57 +0000 Subject: [PATCH 04/13] Keep Ohio from deducting long-term care premiums the federal deduction took With long-term care premiums in the federal self-employed health insurance deduction, Ohio's unreimbursed medical care worksheet added them again in full, though federal AGI, Ohio's starting point, already excluded them. oh_long_term_care_insurance_premiums subtracts the part of the person's own deduction left after their medical premiums, as oh_medical_care_insurance_premiums does for medical premiums. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6GA1FXMB3qVPzGgvWQ --- .../oh_long_term_care_insurance_premiums.yaml | 68 +++++++++++++++++++ .../oh_long_term_care_insurance_premiums.py | 37 ++++++++++ ...d_medical_care_expense_deduction_person.py | 2 +- 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_long_term_care_insurance_premiums.yaml create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_long_term_care_insurance_premiums.py 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..c3427240206 --- /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,68 @@ +# 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 + +- 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 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..8b9b7edfb01 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_long_term_care_insurance_premiums.py @@ -0,0 +1,37 @@ +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) + self_employed_deduction = max_( + 0, person("self_employed_health_insurance_ald_person", period) + ) + # The part of the deduction left after the person's medical insurance + # premiums (Form 7206 line 1) took long-term care premiums (line 2). + medical_part = min_( + person("self_employed_health_insurance_premiums", period), + self_employed_deduction, + ) + long_term_care_part = self_employed_deduction - medical_part + return max_(premiums - long_term_care_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", ] From 5d013c8df71e2760485f9f7ffdea11867a3e598a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 21:18:03 +0000 Subject: [PATCH 05/13] Mention the Ohio fix in the changelog fragment Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6GA1FXMB3qVPzGgvWQ --- changelog.d/ltc-premiums-itemized-medical.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/ltc-premiums-itemized-medical.fixed.md b/changelog.d/ltc-premiums-itemized-medical.fixed.md index 574acbadd4d..6d39945dcb6 100644 --- a/changelog.d/ltc-premiums-itemized-medical.fixed.md +++ b/changelog.d/ltc-premiums-itemized-medical.fixed.md @@ -1 +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. +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, and keep Ohio from deducting again the long-term care premiums that deduction took. From cb662a947649f8205371e4d491db54447874acb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 22:04:06 +0000 Subject: [PATCH 06/13] Check the Ohio deduction itself for a self-employed person's long-term care premiums A mutant that added the premiums in full survived, because the cases checked only the new variable. The self-employed case now also checks the person's worksheet amount: 700, not 2,500. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6GA1FXMB3qVPzGgvWQ --- .../medical_expense/oh_long_term_care_insurance_premiums.yaml | 3 +++ 1 file changed, 3 insertions(+) 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 index c3427240206..8254a0f702e 100644 --- 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 @@ -43,6 +43,9 @@ # 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: max(3,000 - 4,800, 0) = 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 From fd45aa7bd49e69e7cc86ed60f2da53cb944938f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 22:16:11 +0000 Subject: [PATCH 07/13] Update Montana invariants for long-term care premiums in the subtotal The computed Schedule A subtotal now holds eligible long-term care premiums, so property 6 subtracts them and property 9 supplies the computed path's own values instead of adding them a second time. Co-Authored-By: Claude Opus 5.5 --- ...mt_medical_premium_deduction_invariants.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) 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..4fc5dcd3033 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,9 @@ 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. 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 +302,7 @@ def _situation(units, year, mode, unit_inputs=None): PERSON_VARIABLES = [ "other_medical_expenses", "long_term_health_insurance_premiums", + "eligible_long_term_care_premiums", "medical_expense_health_insurance_premiums", "self_employed_health_insurance_ald_person", "mt_eligible_medical_insurance_premiums", @@ -441,15 +443,16 @@ 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. + eligible_long_term_care = run["eligible_long_term_care_premiums"] 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 +546,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 +561,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) From ebd61a7478d99e65491edced0ada534697517025 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 22:18:58 +0000 Subject: [PATCH 08/13] Keep Idaho's medical premiums when a deduction took long-term care premiums The self-employed deduction now includes Form 7206 line 2. That part took no medical insurance premiums, so Idaho subtracts only the rest from the filers' medical premiums. A supplied tax-unit deduction still comes off in full, less any long-term care part the person amounts show. Co-Authored-By: Claude Opus 5.5 --- ...health_insurance_premiums_subtraction.yaml | 30 +++++++++++++++++++ .../id_qualified_health_insurance_premiums.py | 17 ++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) 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..d1371ff3ea7 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,36 @@ 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 + 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. 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..8c89472d5ab 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,20 @@ 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. + person = tax_unit.members + person_deduction = max_( + 0, person("self_employed_health_insurance_ald_person", period) + ) + long_term_care_part = person_deduction - min_( + person("self_employed_health_insurance_premiums", period), + person_deduction, + ) + filer = not_(person("is_tax_unit_dependent", period)) + deducted_elsewhere = max_( + 0, + tax_unit("self_employed_health_insurance_ald", period) + - tax_unit.sum(long_term_care_part * filer), + ) return max_(0, premiums - deducted_elsewhere) From a9866b21b0317ee1e4893e8ce5db2d03a1bf3661 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 22:36:05 +0000 Subject: [PATCH 09/13] Take only the medical part of the deduction off Ohio's medical premiums The long-term care part (Form 7206 line 2) already comes off oh_long_term_care_insurance_premiums, so subtracting the whole person deduction from medical premiums took it twice. Co-Authored-By: Claude Opus 5.5 --- .../oh_long_term_care_insurance_premiums.yaml | 4 +-- .../oh_medical_care_insurance_premiums.yaml | 36 +++++++++++++++++++ .../oh_medical_care_insurance_premiums.py | 13 +++++-- 3 files changed, 48 insertions(+), 5 deletions(-) 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 index 8254a0f702e..dc03a025289 100644 --- 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 @@ -43,8 +43,8 @@ # 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: max(3,000 - 4,800, 0) = 0, and no other expenses, so - # the worksheet takes only the 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 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/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..587334fe8f9 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,14 @@ 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; its long-term care part (line 2) 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 = min_( + person("self_employed_health_insurance_premiums", period), + self_employed_deduction, + ) + return max_(premiums - medical_part, 0) From 7c80b3269c4331734bc9e9174efc3fc5319b4337 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 23:16:31 +0000 Subject: [PATCH 10/13] Leave a dependent's deducted long-term care premiums out of Schedule A A self-employed dependent's own deduction takes the dependent's long-term care premiums on the dependent's return, so the filers' subtotal no longer counts them. New Jersey then removes only the medical part of each dependent's deduction. A person-level variable holds the long-term care part (Form 7206 line 2), capped at the person's line 2 premiums so a supplied deduction above lines 1 and 2 cannot take other expenses out of the subtotal; Idaho reads it too. The return-level exclusion keeps its pooled split: Montana's 2021-2023 line 1 recovers the medical part of a supplied exclusion by that rule, and a per-person split in the computed path gave Montana line 1 other expenses that do not exist. Its documentation now says so. Co-Authored-By: Claude Opus 5.5 --- .../test_long_term_care_premium_properties.py | 49 ++++++++++---- ...lth_insurance_ald_long_term_care_part.yaml | 64 +++++++++++++++++++ ...health_insurance_premiums_subtraction.yaml | 3 + .../nj_medical_expense_deduction.yaml | 36 +++++++++++ ...mt_medical_premium_deduction_invariants.py | 14 ++-- ...ealth_insurance_ald_long_term_care_part.py | 32 ++++++++++ ...penses_excluded_long_term_care_premiums.py | 13 ++-- ...edical_expenses_long_term_care_premiums.py | 14 +++- .../id_qualified_health_insurance_premiums.py | 12 +--- .../nj_medical_expense_deduction.py | 13 ++-- 10 files changed, 215 insertions(+), 35 deletions(-) create mode 100644 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 create mode 100644 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 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 index 3cf7211bcaf..59577442142 100644 --- a/policyengine_us/tests/core/test_long_term_care_premium_properties.py +++ b/policyengine_us/tests/core/test_long_term_care_premium_properties.py @@ -24,19 +24,24 @@ 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 + other expenses, and the - long-term care premiums inside it lie between the dependents' eligible - premiums and all members' eligible premiums. + 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 or as the self-employed deduction, never both. + 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 with no - deduction it raises it by exactly that amount. + 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, @@ -277,6 +282,13 @@ def unit_sum(values): 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, @@ -298,13 +310,20 @@ def unit_sum(values): atol=TOLERANCE, ) - # 3. Accounting identity for the subtotal, from the raw inputs. + # 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, atol=TOLERANCE + inside, + all_eligible - long_term_care_part - dependents_part, + atol=TOLERANCE, ) - assert (inside >= unit_sum(eligible * ~filer.astype(bool)) - TOLERANCE).all() + 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( @@ -314,9 +333,13 @@ def unit_sum(values): ) if not supplied_deduction: - # 4. Each eligible premium is deducted once. + # 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"], + subtotal + - unit_sum(other) + + run["self_employed_health_insurance_ald"] + + dependents_part, unit_sum(health) + all_eligible, atol=TOLERANCE, ) @@ -338,7 +361,9 @@ def unit_sum(values): rise_in_subtotal = subtotal[raised] - subtotal[base] assert (rise_in_subtotal >= -TOLERANCE).all() assert (rise_in_subtotal <= rise_in_eligible + TOLERANCE).all() - no_deduction = (deduction[base] == 0) & (deduction[raised] == 0) + # 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], 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/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 d1371ff3ea7..8305a54fbc3 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 @@ -246,6 +246,9 @@ 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 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..c82c05740c7 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,39 @@ 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 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 4fc5dcd3033..5365c30c21d 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 @@ -56,8 +56,9 @@ 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 medical and eligible - long-term care premiums. Line 1 therefore tracks the federal definition - apart from the premiums. + 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 @@ -303,6 +304,7 @@ def _situation(units, year, mode, unit_inputs=None): "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", @@ -445,8 +447,12 @@ def _check(units, year, mode): # 6. Line 1 base tracks the federal definition apart from the premiums. # 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. - eligible_long_term_care = run["eligible_long_term_care_premiums"] + # 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"] 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/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 index 63c1601a4af..7044738ebe4 100644 --- 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 @@ -17,10 +17,15 @@ class itemized_medical_expenses_excluded_long_term_care_premiums(Variable): "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. Taking the deduction from " - "medical insurance premiums first leaves the Schedule A subtotal " - "unchanged; it fixes which premiums a state that treats long-term " - "care premiums separately sees as already deducted." + "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): 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 123d3895265..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 @@ -16,7 +16,9 @@ class itemized_medical_expenses_long_term_care_premiums(Variable): "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 self-employed health insurance deduction took. " + "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 " @@ -39,4 +41,12 @@ def formula(tax_unit, period, parameters): excluded = tax_unit( "itemized_medical_expenses_excluded_long_term_care_premiums", period ) - return premiums - excluded + # 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 8c89472d5ab..6083c46c19c 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 @@ -33,18 +33,12 @@ def formula(tax_unit, period, parameters): # so they must not reduce the filers' qualifying premiums again. # The deduction's long-term care part (Form 7206 line 2) took no # medical insurance premiums, so it does not reduce them. - person = tax_unit.members - person_deduction = max_( - 0, person("self_employed_health_insurance_ald_person", period) + long_term_care_part = tax_unit_non_dep_sum( + "self_employed_health_insurance_ald_long_term_care_part", tax_unit, period ) - long_term_care_part = person_deduction - min_( - person("self_employed_health_insurance_premiums", period), - person_deduction, - ) - filer = not_(person("is_tax_unit_dependent", period)) deducted_elsewhere = max_( 0, tax_unit("self_employed_health_insurance_ald", period) - - tax_unit.sum(long_term_care_part * filer), + - long_term_care_part, ) return max_(0, premiums - deducted_elsewhere) 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..f6e1f3d607e 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,17 @@ 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, 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), + person("self_employed_health_insurance_premiums", 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 From 679027d9e6d608023ef92b6103a59073130182e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 23:23:11 +0000 Subject: [PATCH 11/13] Keep Michigan line 31 premiums that a long-term care deduction did not take Line 31 drops premiums already deducted on line 30. The long-term care part (Form 7206 line 2) of the self-employed deduction took long-term care premiums, which line 31 does not hold, so only the rest of the head's and spouse's deduction comes off line 31. Co-Authored-By: Claude Opus 5.5 --- .../mi/tax/income/mi_household_resources.yaml | 35 +++++++++++++++++++ .../mi/tax/income/mi_household_resources.py | 24 ++++++++++--- 2 files changed, 54 insertions(+), 5 deletions(-) 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..9daca542157 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,38 @@ # 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 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..444bb47dc41 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,31 @@ 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 any long-term + # care part the person amounts show. 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. premiums_on_line_30 = ( - tax_unit("self_employed_health_insurance_ald", period) + max_( + 0, + tax_unit("self_employed_health_insurance_ald", period) + - tax_unit_non_dep_sum( + "self_employed_health_insurance_ald_long_term_care_part", + tax_unit, + period, + ), + ) if "self_employed_health_insurance_ald" in deductions else 0 ) From 437dbfef8b864a516496fd9f02c4dde272e25cdf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 23:56:39 +0000 Subject: [PATCH 12/13] Describe the state readers in the changelog fragment Co-Authored-By: Claude Opus 5.5 --- changelog.d/ltc-premiums-itemized-medical.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/ltc-premiums-itemized-medical.fixed.md b/changelog.d/ltc-premiums-itemized-medical.fixed.md index 6d39945dcb6..9e3ca50e965 100644 --- a/changelog.d/ltc-premiums-itemized-medical.fixed.md +++ b/changelog.d/ltc-premiums-itemized-medical.fixed.md @@ -1 +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, and keep Ohio from deducting again the long-term care premiums that deduction took. +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. From 145dc194233aed2550bdc0604be8059275d69c3b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 00:54:08 +0000 Subject: [PATCH 13/13] Take the medical part as the deduction less its long-term care part New Jersey and Ohio took the medical part as min(line 1 premiums, deduction), so a person deduction supplied for someone who is not self-employed removed no premiums, and test_nj_excludes_each_dependents_ deduction_only_from_their_own_premiums failed. The deduction less its capped long-term care part equals min(line 1, deduction) for a derived deduction and keeps a supplied one removing that person's own premiums. Co-Authored-By: Claude Opus 5.5 --- .../income/deductions/nj_medical_expense_deduction.py | 6 ++++-- .../oh_medical_care_insurance_premiums.py | 9 ++++----- 2 files changed, 8 insertions(+), 7 deletions(-) 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 f6e1f3d607e..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 @@ -42,11 +42,13 @@ def formula(tax_unit, period, parameters): # 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, limited to that person's own paid medical premiums. + # 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), - person("self_employed_health_insurance_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( dependent_medical_part * person("is_tax_unit_dependent", period) 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 587334fe8f9..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 @@ -22,13 +22,12 @@ def formula(person, period, parameters): # Premiums already deducted in computing federal AGI are not deducted # again (worksheet line 1 note). # Only the medical insurance part (Form 7206 line 1) of the - # deduction; its long-term care part (line 2) comes off - # oh_long_term_care_insurance_premiums. + # 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) ) - medical_part = min_( - person("self_employed_health_insurance_premiums", period), - self_employed_deduction, + medical_part = self_employed_deduction - person( + "self_employed_health_insurance_ald_long_term_care_part", period ) return max_(premiums - medical_part, 0)