From 78fb890c98af8232269189805b345e73113a1b60 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 11:13:29 -0400 Subject: [PATCH 1/5] Count Ohio's unreimbursed medical care deduction once per return oh_unreimbursed_medical_care_expense_deduction_person listed the TaxUnit variable oh_insured_unreimbursed_medical_care_expenses in its adds. policyengine-core evaluates adds with map_to="person", which projects a group value onto every member, so each member deducted the full worksheet line 8 amount and Ohio AGI (the sum over members) fell by n times it. Each spouse's joint filing credit qualifying income also lost the whole amount. Use the existing person-level oh_insured_unreimbursed_medical_care_expenses_person, which prorates line 8 by each member's share of line 5 and sums to the return amount. Also enter federal AGI below zero as zero on worksheet line 6, as every year's instructions say, so line 8 never exceeds line 5. Adds YAML tests that fail on main (person split, total once, Ohio AGI, joint filing credit, negative AGI) and Hypothesis invariants (conservation, an independent worksheet, bounds, once-only accounting). Updates the 2026 retired-couple case, whose Medicare Part B line 8 had been deducted twice. Co-Authored-By: Claude Opus 5.5 --- ...x-oh-medical-deduction-per-person.fixed.md | 1 + ...reimbursed_medical_deduction_invariants.py | 278 ++++++++++++++++++ ...ed_unreimbursed_medical_care_expenses.yaml | 20 ++ ...bursed_medical_care_expense_deduction.yaml | 145 +++++++++ .../oh/tax/income/integration_2025.yaml | 12 +- ...ured_unreimbursed_medical_care_expenses.py | 10 +- ...d_medical_care_expense_deduction_person.py | 11 +- 7 files changed, 465 insertions(+), 12 deletions(-) create mode 100644 changelog.d/fix-oh-medical-deduction-per-person.fixed.md create mode 100644 policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml diff --git a/changelog.d/fix-oh-medical-deduction-per-person.fixed.md b/changelog.d/fix-oh-medical-deduction-per-person.fixed.md new file mode 100644 index 00000000000..fba13acbb74 --- /dev/null +++ b/changelog.d/fix-oh-medical-deduction-per-person.fixed.md @@ -0,0 +1 @@ +Count Ohio's unreimbursed medical care deduction once per return: each member now takes their prorated share of worksheet line 8 rather than the full return amount, and federal AGI below zero is entered as zero on worksheet line 6. diff --git a/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py new file mode 100644 index 00000000000..2f4e8067ebb --- /dev/null +++ b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py @@ -0,0 +1,278 @@ +"""Invariants for Ohio's unreimbursed medical care deduction, R.C. 5747.01(A)(10). + +The Unreimbursed Medical Care Expenses Worksheet (2025 IT 1040 instructions, +p. 41) computes line 8 once for the return: line 5 expenses less 7.5% of +federal AGI, with AGI entered as zero if less than zero (line 6). Lines 1 and 2 +(premiums with no Medicare or employer plan, and long-term care premiums) are +deducted in full. Each person carries their own lines 1 and 2 and a share of +line 8 in proportion to their line 5 expenses, and Ohio AGI is the sum over +the members. + +Hypothesis draws batches of tax units (single, joint, head of household or +joint with dependents), each person with wages, medical expenses, uninsured +premiums and long-term care premiums, and some heads with a business loss +that makes federal AGI negative. A seeded population of 200 units adds +breadth. Each batch runs as one vectorized simulation, and again with every +medical amount set to zero. For every tax unit: + +1. Conservation: the members' line 8 shares sum to the return's line 8. +2. Differential: line 8 and each share equal an independent numpy worksheet, + with the 7.5% rate taken from the instructions rather than the parameters. +3. Bounds: 0 <= line 8 <= line 5, and 0 <= each share <= the person's own + line 5 expenses. +4. Accounting: the tax unit deduction is line 8 plus lines 1 and 2, counted + once. Removing the medical amounts raises Ohio AGI by exactly that + deduction, and raises each person's Ohio AGI (and joint filing credit + qualifying income) by exactly their own share, so a spouse never loses + qualifying income for the other spouse's expenses. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 # dollars + +# Worksheet line 7 multiplies line 6 by 7.5% in every year's instructions +# (2022 p. 27, 2023 p. 40, 2024 p. 41, 2025 p. 41). +PUBLISHED_RATE = 0.075 +YEARS = [2022, 2024, 2025] + +MEDICAL = [ + "other_medical_expenses", + "health_insurance_premiums", + "long_term_health_insurance_premiums", +] + +amounts = st.one_of( + st.just(0.0), + st.integers(1, 3_000).map(float), + st.integers(3_000, 40_000).map(float), +) +wages = st.one_of(st.just(0.0), st.integers(1, 200_000).map(float)) +losses = st.one_of(st.just(0.0), st.integers(-100_000, -1).map(float)) + + +@st.composite +def people(draw): + return { + "employment_income": draw(wages), + "other_medical_expenses": draw(amounts), + "health_insurance_premiums": draw(amounts), + "long_term_health_insurance_premiums": draw(amounts), + } + + +@st.composite +def tax_units(draw): + kind = draw(st.sampled_from(["single", "joint", "hoh", "joint_dependents"])) + n_dependents = draw(st.integers(1, 3)) if kind in ("hoh", "joint_dependents") else 0 + return { + "head": draw(people()), + "loss": draw(losses), + "spouse": draw(people()) if kind.startswith("joint") else None, + "dependents": [draw(people()) for _ in range(n_dependents)], + } + + +SEED = 20261006 + + +def _seeded_units(n=200): + rng = np.random.default_rng(SEED) + + def amount(): + return float( + [0, rng.integers(1, 3_001), rng.integers(3_000, 40_001)][rng.choice(3)] + ) + + def person(): + return { + "employment_income": float(rng.integers(0, 200_001)), + "other_medical_expenses": amount(), + "health_insurance_premiums": amount(), + "long_term_health_insurance_premiums": amount(), + } + + units = [] + for _ in range(n): + kind = rng.choice(["single", "joint", "hoh", "joint_dependents"]) + n_dependents = ( + int(rng.integers(1, 4)) if kind in ("hoh", "joint_dependents") else 0 + ) + units.append( + { + "head": person(), + "loss": float(-rng.integers(1, 100_001)) if rng.random() < 0.2 else 0.0, + "spouse": person() if kind.startswith("joint") else None, + "dependents": [person() for _ in range(n_dependents)], + } + ) + return units + + +def _situation(units, year, *, medical): + people, groups = {}, {"tax_units": {}, "households": {}, "marital_units": {}} + + def add(name, values, role): + people[name] = { + # Under 65 and not disabled, so no one is eligible for Medicare + # and worksheet line 3 is zero. + "age": {"head": 45, "spouse": 43, "dependent": 10}[role], + "is_tax_unit_head": role == "head", + "is_tax_unit_spouse": role == "spouse", + "is_tax_unit_dependent": role == "dependent", + "employment_income": values["employment_income"], + "employer_contribution_to_health_insurance_premiums_category": "NONE", + **{name: values[name] * medical for name in MEDICAL}, + } + + for i, unit in enumerate(units): + head = f"head_{i}" + add(head, unit["head"], "head") + people[head]["self_employment_income"] = unit["loss"] + members, couple = [head], [head] + if unit["spouse"] is not None: + spouse = f"spouse_{i}" + add(spouse, unit["spouse"], "spouse") + members.append(spouse) + couple.append(spouse) + groups["marital_units"][f"couple_{i}"] = {"members": couple} + for j, dependent in enumerate(unit["dependents"]): + name = f"dependent_{i}_{j}" + add(name, dependent, "dependent") + members.append(name) + groups["marital_units"][f"single_{i}_{j}"] = {"members": [name]} + groups["tax_units"][f"tax_unit_{i}"] = {"members": members} + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {year: "OH"}, + } + people = { + name: {key: {year: value} for key, value in values.items()} + for name, values in people.items() + } + return {"people": people, **groups} + + +PERSON = [ + "other_medical_expenses", + "oh_insured_unreimbursed_medical_care_expense_amount", + "oh_insured_unreimbursed_medical_care_expenses_person", + "oh_uninsured_unreimbursed_medical_care_expenses", + "long_term_health_insurance_premiums", + "oh_unreimbursed_medical_care_expense_deduction_person", + "oh_agi_person", + "oh_joint_filing_credit_qualifying_income", +] +TAX_UNIT = [ + "adjusted_gross_income", + "oh_insured_unreimbursed_medical_care_expenses", + "oh_unreimbursed_medical_care_expense_deduction", + "oh_agi", +] + + +def _run(units, year, *, medical=True): + sim = Simulation(situation=_situation(units, year, medical=medical)) + out = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in PERSON + TAX_UNIT + } + out["filer"] = np.asarray(sim.calculate("is_tax_unit_head_or_spouse", year)) + out["unit"] = sim.populations["tax_unit"].members_entity_id + return out + + +def _unit_sum(run, values): + return np.bincount(run["unit"], weights=values, minlength=len(run["oh_agi"])) + + +def _check(units, year): + base = _run(units, year) + unit = base["unit"] + line_5_person = base["oh_insured_unreimbursed_medical_care_expense_amount"] + line_8 = base["oh_insured_unreimbursed_medical_care_expenses"] + share = base["oh_insured_unreimbursed_medical_care_expenses_person"] + + # No one is Medicare eligible, so line 5 is each person's medical care. + np.testing.assert_allclose( + line_5_person, base["other_medical_expenses"], atol=TOLERANCE + ) + line_5 = _unit_sum(base, line_5_person) + + # 1. Conservation. + np.testing.assert_allclose(_unit_sum(base, share), line_8, atol=TOLERANCE) + + # 2. Differential against an independent worksheet. + line_6 = np.maximum(base["adjusted_gross_income"], 0) + expected_line_8 = np.maximum(line_5 - PUBLISHED_RATE * line_6, 0) + np.testing.assert_allclose(line_8, expected_line_8, atol=TOLERANCE) + expected_share = np.divide( + line_5_person * expected_line_8[unit], + line_5[unit], + out=np.zeros_like(line_5_person), + where=line_5[unit] > 0, + ) + np.testing.assert_allclose(share, expected_share, atol=TOLERANCE) + + # 3. Bounds. + assert (line_8 >= 0).all() + assert (line_8 <= line_5 + TOLERANCE).all() + assert (share >= 0).all() + assert (share <= line_5_person + TOLERANCE).all() + + # 4. Accounting: each amount counted once, and only for its own person. + person_deduction = base["oh_unreimbursed_medical_care_expense_deduction_person"] + np.testing.assert_allclose( + person_deduction, + share + + base["oh_uninsured_unreimbursed_medical_care_expenses"] + + base["long_term_health_insurance_premiums"], + atol=TOLERANCE, + ) + deduction = base["oh_unreimbursed_medical_care_expense_deduction"] + np.testing.assert_allclose( + deduction, _unit_sum(base, person_deduction), atol=TOLERANCE + ) + + without = _run(units, year, medical=False) + np.testing.assert_allclose( + without["adjusted_gross_income"], + base["adjusted_gross_income"], + atol=TOLERANCE, + ) + np.testing.assert_allclose( + without["oh_unreimbursed_medical_care_expense_deduction"], 0, atol=TOLERANCE + ) + np.testing.assert_allclose( + without["oh_agi"] - base["oh_agi"], deduction, atol=TOLERANCE + ) + for name in ["oh_agi_person", "oh_joint_filing_credit_qualifying_income"]: + np.testing.assert_allclose( + without[name] - base[name], person_deduction, atol=TOLERANCE, err_msg=name + ) + + +# A batch's cost is mostly per-variable overhead, so each example is a large +# batch and there are few examples. +SETTINGS = dict( + max_examples=6, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.sampled_from(YEARS), st.lists(tax_units(), min_size=5, max_size=30)) +def test_oh_unreimbursed_medical_deduction_invariants(year, units): + _check(units, year) + + +@pytest.mark.parametrize("year", YEARS) +def test_seeded_population(year): + _check(_seeded_units(), year) diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.yaml index 891a60ce10c..b6f9393d6dc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.yaml @@ -35,3 +35,23 @@ adjusted_gross_income: 1_000 output: oh_insured_unreimbursed_medical_care_expenses: 0 + +- name: Negative federal AGI is entered as zero on line 6 + period: 2025 + input: + state_code: OH + oh_insured_unreimbursed_medical_care_expense_amount: 1_000 + adjusted_gross_income: -20_000 + output: + # Worksheet line 6 is max(-20,000, 0) = 0, so line 7 is 0 and line 8 is + # 1,000 - 0 = 1,000. The deduction never exceeds the expenses paid. + oh_insured_unreimbursed_medical_care_expenses: 1_000 + +- name: Negative federal AGI with no expenses gives no deduction + period: 2025 + input: + state_code: OH + oh_insured_unreimbursed_medical_care_expense_amount: 0 + adjusted_gross_income: -20_000 + output: + oh_insured_unreimbursed_medical_care_expenses: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml new file mode 100644 index 00000000000..5691fa861dc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml @@ -0,0 +1,145 @@ +# The Unreimbursed Medical Care Expenses Worksheet (2025 instructions, p. 41) +# computes line 8 once for the return. Each person carries their share of it, +# in proportion to their line 5 expenses, so the return deducts it once. + +- name: Joint couple and child, head has all the expenses + period: 2025 + input: + people: + head: + age: 40 + employment_income: 40_000 + other_medical_expenses: 6_000 + spouse: + age: 38 + employment_income: 10_000 + child: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse, child] + state_code: OH + output: + # Line 5 = 6,000; line 6 = 50,000; line 7 = 50,000 * 0.075 = 3,750; + # line 8 = 6,000 - 3,750 = 2,250, all of it the head's share. + oh_insured_unreimbursed_medical_care_expenses: 2_250 + oh_insured_unreimbursed_medical_care_expenses_person: [2_250, 0, 0] + oh_unreimbursed_medical_care_expense_deduction_person: [2_250, 0, 0] + oh_unreimbursed_medical_care_expense_deduction: 2_250 + # 40,000 - 2,250; 10,000; 0. + oh_agi_person: [37_750, 10_000, 0] + # 50,000 - 2,250. + oh_agi: 47_750 + +- name: Expenses for every member are prorated by each person's line 5 share + period: 2025 + input: + people: + head: + age: 40 + employment_income: 40_000 + other_medical_expenses: 3_000 + spouse: + age: 38 + employment_income: 10_000 + other_medical_expenses: 1_000 + child: + age: 5 + is_tax_unit_dependent: true + other_medical_expenses: 2_000 + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse, child] + state_code: OH + output: + # Line 5 = 6,000; line 8 = 6,000 - 3,750 = 2,250. + oh_insured_unreimbursed_medical_care_expenses: 2_250 + # 2,250 * 3/6, 2,250 * 1/6, 2,250 * 2/6. + oh_unreimbursed_medical_care_expense_deduction_person: [1_125, 375, 750] + oh_unreimbursed_medical_care_expense_deduction: 2_250 + oh_agi_person: [38_875, 9_625, -750] + oh_agi: 47_750 + +- name: Uninsured and long-term care premiums stay with the person who paid them + period: 2025 + input: + people: + head: + age: 40 + employment_income: 40_000 + other_medical_expenses: 6_000 + health_insurance_premiums: 1_200 + employer_contribution_to_health_insurance_premiums_category: NONE + spouse: + age: 38 + employment_income: 10_000 + long_term_health_insurance_premiums: 500 + child: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse, child] + state_code: OH + output: + # Line 1 = 1,200 (head, no Medicare or employer plan); line 2 = 500 + # (spouse); line 8 = 6,000 - 3,750 = 2,250 (head). + oh_uninsured_unreimbursed_medical_care_expenses: [1_200, 0, 0] + oh_unreimbursed_medical_care_expense_deduction_person: [3_450, 500, 0] + # Line 9 = 1,200 + 500 + 2,250. + oh_unreimbursed_medical_care_expense_deduction: 3_950 + oh_agi: 46_050 + +- name: The head's medical deduction does not reduce the spouse's joint filing credit income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + other_medical_expenses: 10_000 + spouse: + age: 38 + employment_income: 2_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: OH + output: + # Line 5 = 10,000; line 7 = 62,000 * 0.075 = 4,650; line 8 = 5,350. + oh_unreimbursed_medical_care_expense_deduction_person: [5_350, 0] + oh_unreimbursed_medical_care_expense_deduction: 5_350 + oh_agi: 56_650 + # The spouse keeps 2,000 of qualifying income, above the 500 threshold. + oh_joint_filing_credit_qualifying_income: [54_650, 2_000] + oh_joint_filing_credit_eligible: true + # Exemptions 2 * 2,150 = 4,300 (p. 17, MAGI 40,001-80,000), taxable 52,350, + # tax 342 + 2.75% * (52,350 - 26,050) = 1,065.25 (2025 instructions, p. 18). + # MAGI less exemptions is 52,350, so the credit is 10% = 106.53. + oh_joint_filing_credit: 106.53 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration_2025.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration_2025.yaml index 3f85a33e02d..7490f01777e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration_2025.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration_2025.yaml @@ -266,9 +266,15 @@ members: [person1, person2] state_code: OH output: - oh_taxable_income: 35_038.3 - oh_income_tax_before_non_refundable_credits: 579.18 + # Unreimbursed medical care worksheet: line 3 is each spouse's Medicare + # Part B premium, 2 * 2,434.80 = 4,869.60; federal AGI is 40,000 + + # 23,850 taxable Social Security = 63,850, so line 7 is 4,788.75 and + # line 8 is 80.85, deducted once for the return. + # Ohio AGI = 63,850 - 23,850 - 80.85 = 39,919.15; less 2 * 2,400 exemptions. + oh_taxable_income: 35_119.15 + # 332.00 + 2.75% * (35,119.15 - 26,050). + oh_income_tax_before_non_refundable_credits: 581.40 oh_retirement_credit_potential: 200 oh_retirement_credit: 200 oh_non_refundable_credits: 250 - state_income_tax: 329.18 + state_income_tax: 331.40 diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.py index 56f9870f755..3a9acf8f65b 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.py @@ -8,9 +8,9 @@ class oh_insured_unreimbursed_medical_care_expenses(Variable): unit = USD definition_period = YEAR reference = ( - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=27", - "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(10) + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10)(b) ) defined_for = StateCode.OH @@ -21,8 +21,8 @@ def formula(tax_unit, period, parameters): period, ["oh_insured_unreimbursed_medical_care_expense_amount"], ) - # Line 6 - federal_agi = tax_unit("adjusted_gross_income", period) + # Line 6: federal AGI, entered as zero if less than zero. + federal_agi = max_(tax_unit("adjusted_gross_income", period), 0) # Can deduct medical expenses in excess of 7.5% of federal AGI. # Line 7 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 d20a345b203..5dc4426d205 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 @@ -8,14 +8,17 @@ class oh_unreimbursed_medical_care_expense_deduction_person(Variable): unit = USD definition_period = YEAR reference = ( - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=27", - "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(10) + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10) ) defined_for = StateCode.OH + # The worksheet computes line 8 once for the return. Each person takes + # their share of it, so the shares sum to the return's amount. Listing + # the tax unit amount here would give every member the full amount. adds = [ - "oh_insured_unreimbursed_medical_care_expenses", + "oh_insured_unreimbursed_medical_care_expenses_person", "oh_uninsured_unreimbursed_medical_care_expenses", "long_term_health_insurance_premiums", ] From 949dd8d728c89b6e5ef12a0c8f9b69bfa751f9b4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 14:19:53 -0400 Subject: [PATCH 2/5] Trim the Ohio medical invariants test to ten simulations The invariants do not vary by year, so run Hypothesis on 2022 and 2025 with four examples and the seeded population on 2024 only. Test time falls from about 50 s to 15 s locally; both bugs still fail it. Co-Authored-By: Claude Opus 5.5 --- ..._oh_unreimbursed_medical_deduction_invariants.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py index 2f4e8067ebb..e1be0da4318 100644 --- a/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py +++ b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py @@ -28,7 +28,6 @@ """ import numpy as np -import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st @@ -39,7 +38,10 @@ # Worksheet line 7 multiplies line 6 by 7.5% in every year's instructions # (2022 p. 27, 2023 p. 40, 2024 p. 41, 2025 p. 41). PUBLISHED_RATE = 0.075 -YEARS = [2022, 2024, 2025] +# Nothing here varies by year, so a few years suffice: each simulation costs +# far more than the checks on it. +YEARS = [2022, 2025] +SEEDED_YEAR = 2024 MEDICAL = [ "other_medical_expenses", @@ -260,7 +262,7 @@ def _check(units, year): # A batch's cost is mostly per-variable overhead, so each example is a large # batch and there are few examples. SETTINGS = dict( - max_examples=6, + max_examples=4, deadline=None, derandomize=True, suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], @@ -273,6 +275,5 @@ def test_oh_unreimbursed_medical_deduction_invariants(year, units): _check(units, year) -@pytest.mark.parametrize("year", YEARS) -def test_seeded_population(year): - _check(_seeded_units(), year) +def test_seeded_population(): + _check(_seeded_units(), SEEDED_YEAR) From 97a3b7ed06307dd0609f7a2a663f8cd45fd186e6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 18:52:45 -0400 Subject: [PATCH 3/5] Test Ohio worksheet lines 1 and 3 against R.C. 5747.01(A)(10) Adds failing cases for PolicyBench scenario_025 and for the line 1/line 3 split: line 3 takes premiums of anyone eligible for Medicare OR an employer-paid plan, and line 1 reads the premium inputs the data fills. Corrects existing expectations that encoded the AND gate. Co-Authored-By: Claude Opus 5.5 --- .../integration.yaml | 8 +- ...eimbursed_medical_care_expense_amount.yaml | 77 +++++++- ...loyer_subsidized_health_plan_eligible.yaml | 102 +++++++++++ .../oh_medical_care_insurance_premiums.yaml | 53 ++++++ ...ed_unreimbursed_medical_care_expenses.yaml | 171 ++++++++++++++++++ ...bursed_medical_care_expense_deduction.yaml | 66 +++++++ 6 files changed, 471 insertions(+), 6 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_employer_subsidized_health_plan_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/integration.yaml index 738c898e2d0..2ac9114478a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/integration.yaml @@ -20,7 +20,9 @@ other_medical_expenses: 600 employer_contribution_to_health_insurance_premiums_category: "SOME" output: - oh_insured_unreimbursed_medical_care_expenses: 0 + # Medicare eligible, so the premiums are on line 3 whatever the employer + # paid: 500 + 600 - 10,000 * 7.5% = 350. + oh_insured_unreimbursed_medical_care_expenses: 350 - name: moop > agi * rate period: 2022 @@ -106,5 +108,7 @@ members: [head, spouse] state_code: OH output: - oh_insured_unreimbursed_medical_care_expenses: 950 + # Both are Medicare eligible, so both spouses' premiums are on line 3: + # 300 + 500 + 200 + 300 - 2,000 * 7.5% = 1,150. + oh_insured_unreimbursed_medical_care_expenses: 1_150 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.yaml index 3ddeb38614e..e8ea897e5fd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.yaml @@ -9,7 +9,9 @@ output: oh_insured_unreimbursed_medical_care_expense_amount: 1_100 -- name: Medicare ineligible with moop +- name: Medicare eligible with an employer-paid plan and moop + # Line 3 takes premiums of anyone eligible for Medicare or an employer-paid + # plan, so an employer paying part of the premium does not remove them. period: 2022 input: state_code: OH @@ -18,9 +20,9 @@ other_medical_expenses: 600 employer_contribution_to_health_insurance_premiums_category: "SOME" output: - oh_insured_unreimbursed_medical_care_expense_amount: 600 + oh_insured_unreimbursed_medical_care_expense_amount: 1_100 -- name: Medicare ineligible without moop +- name: Medicare eligible with an employer-paid plan and no moop period: 2022 input: state_code: OH @@ -29,4 +31,71 @@ other_medical_expenses: 0 employer_contribution_to_health_insurance_premiums_category: "SOME" output: - oh_insured_unreimbursed_medical_care_expense_amount: 0 + oh_insured_unreimbursed_medical_care_expense_amount: 500 + +# Worksheet line 3 (2025 instructions, p. 41): premiums paid while "eligible +# for Medicare or an employer-paid health care plan through your or your +# spouse's employer", added to line 4 medical care, R.C. 5747.01(A)(10)(b). + +- name: After-tax premiums of a person with employer coverage count on line 3 + period: 2025 + input: + people: + person1: + age: 45 + has_esi: true + employment_income: 60_000 + health_insurance_premiums_without_medicare_part_b: 6_500 + other_medical_expenses: 800 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + oh_insured_unreimbursed_medical_care_expense_amount: 7_300 + +- name: Premiums with no Medicare or employer plan go to line 1, not line 3 + period: 2025 + input: + people: + person1: + age: 45 + employment_income: 60_000 + health_insurance_premiums_without_medicare_part_b: 6_000 + other_medical_expenses: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + oh_insured_unreimbursed_medical_care_expense_amount: 500 + oh_uninsured_unreimbursed_medical_care_expenses: 6_000 + +- name: Premiums paid through pre-tax payroll deductions are not deducted + # Line 44 instructions (p. 25) and FAQ Q2: no deduction for amounts paid + # with pre-tax dollars. They are excluded from federal wages instead. + period: 2025 + input: + people: + person1: + age: 45 + has_esi: true + employment_income: 60_000 + pre_tax_health_insurance_premiums: 2_000 + other_medical_expenses: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + irs_employment_income: 58_000 + oh_insured_unreimbursed_medical_care_expense_amount: 500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_employer_subsidized_health_plan_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_employer_subsidized_health_plan_eligible.yaml new file mode 100644 index 00000000000..bf0205f82c4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_employer_subsidized_health_plan_eligible.yaml @@ -0,0 +1,102 @@ +# R.C. 5747.01(A)(10)(a) bars the full premium deduction for "any taxpayer who +# is eligible to participate in any subsidized health plan maintained by any +# employer of the taxpayer or of the taxpayer's spouse". The 2025 worksheet +# (p. 41) asks whether "you" were eligible for "an employer-paid health care +# plan through your or your spouse's employer". + +- name: Head with employer coverage + period: 2025 + input: + people: + head: + has_esi: true + spouse: + has_esi: false + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: OH + output: + oh_employer_subsidized_health_plan_eligible: true + +- name: Spouse with employer coverage makes the return eligible + period: 2025 + input: + people: + head: + has_esi: false + spouse: + has_esi: true + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: OH + output: + oh_employer_subsidized_health_plan_eligible: true + +- name: Offer of employer coverage the filer declined + period: 2025 + input: + state_code: OH + offered_aca_disqualifying_esi: true + output: + oh_employer_subsidized_health_plan_eligible: true + +- name: Employer pays some of the filer's premiums + period: 2025 + input: + state_code: OH + employer_contribution_to_health_insurance_premiums_category: SOME + output: + oh_employer_subsidized_health_plan_eligible: true + +- name: Employer pays all of the filer's premiums + period: 2025 + input: + state_code: OH + employer_contribution_to_health_insurance_premiums_category: ALL + output: + oh_employer_subsidized_health_plan_eligible: true + +- name: Employer pays none of the premiums and no employer coverage + period: 2025 + input: + state_code: OH + employer_contribution_to_health_insurance_premiums_category: NONE + output: + oh_employer_subsidized_health_plan_eligible: false + +- name: Employer contribution not applicable + period: 2025 + input: + state_code: OH + employer_contribution_to_health_insurance_premiums_category: NA + output: + oh_employer_subsidized_health_plan_eligible: false + +- name: Only a dependent has employer coverage + period: 2025 + input: + people: + head: + age: 50 + child: + age: 20 + is_tax_unit_dependent: true + has_esi: true + employer_contribution_to_health_insurance_premiums_category: SOME + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: OH + output: + oh_employer_subsidized_health_plan_eligible: false 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 new file mode 100644 index 00000000000..36c1c46fd9c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_medical_care_insurance_premiums.yaml @@ -0,0 +1,53 @@ +# Dental, vision and health insurance premiums the taxpayer paid, for +# worksheet lines 1 and 3 (2025 instructions, p. 41), to the extent not +# otherwise deducted or excluded in computing federal AGI +# (R.C. 5747.01(A)(10)(a)-(b)). + +- name: Direct premium input + period: 2025 + input: + state_code: OH + health_insurance_premiums: 3_000 + output: + oh_medical_care_insurance_premiums: 3_000 + +- name: Non-Medicare premiums are counted once, not with the SPM component + period: 2025 + input: + state_code: OH + age: 40 + health_insurance_premiums_without_medicare_part_b: 6_500 + other_health_insurance_premiums: 6_500 + employer_sponsored_insurance_premiums: 20_000 + output: + oh_medical_care_insurance_premiums: 6_500 + +- name: Premiums paid through pre-tax payroll deductions are left out + period: 2025 + input: + state_code: OH + employment_income: 50_000 + pre_tax_health_insurance_premiums: 2_000 + output: + oh_medical_care_insurance_premiums: 0 + +- name: Premiums deducted as self-employed health insurance are left out + period: 2025 + input: + state_code: OH + is_self_employed: true + self_employment_income: 3_000 + health_insurance_premiums: 5_000 + output: + # 5,000 - min(3,000, 5,000). + oh_medical_care_insurance_premiums: 2_000 + +- name: The self-employed deduction cannot make premiums negative + period: 2025 + input: + state_code: OH + is_self_employed: true + self_employment_income: 80_000 + health_insurance_premiums: 5_000 + output: + oh_medical_care_insurance_premiums: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_uninsured_unreimbursed_medical_care_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_uninsured_unreimbursed_medical_care_expenses.yaml index 413b070a4fe..d9eac668dff 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_uninsured_unreimbursed_medical_care_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_uninsured_unreimbursed_medical_care_expenses.yaml @@ -27,3 +27,174 @@ employer_contribution_to_health_insurance_premiums_category: "ALL" output: oh_uninsured_unreimbursed_medical_care_expenses: 0 + +# Worksheet line 1 (2025 instructions, p. 41): premiums paid while "not +# eligible for Medicare or an employer-paid health care plan through your or +# your spouse's employer", R.C. 5747.01(A)(10)(a). + +- name: Premiums reported as non-Medicare premiums count on line 1 + # Microdata and most household inputs report premiums through + # health_insurance_premiums_without_medicare_part_b, not the direct input. + period: 2025 + input: + people: + person1: + age: 40 + employment_income: 50_000 + health_insurance_premiums_without_medicare_part_b: 6_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + oh_uninsured_unreimbursed_medical_care_expenses: 6_000 + +- name: The spouse's employer plan moves the head's private premiums off line 1 + # Ohio FAQ Q9: a taxpayer who declines the spouse's employer plan and buys + # private insurance enters those premiums on line 3. + period: 2025 + input: + people: + head: + age: 40 + health_insurance_premiums_without_medicare_part_b: 4_000 + spouse: + age: 38 + has_esi: true + employment_income: 50_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: OH + output: + oh_uninsured_unreimbursed_medical_care_expenses: [0, 0] + oh_insured_unreimbursed_medical_care_expense_amount: [4_000, 0] + # Line 8 = 4,000 - 0.075 * 50,000 = 250. + oh_unreimbursed_medical_care_expense_deduction: 250 + +- name: A dependent's premiums follow the filer's employer plan + period: 2025 + input: + people: + head: + age: 40 + has_esi: true + employment_income: 50_000 + child: + age: 10 + is_tax_unit_dependent: true + health_insurance_premiums_without_medicare_part_b: 1_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: OH + output: + oh_uninsured_unreimbursed_medical_care_expenses: [0, 0] + oh_insured_unreimbursed_medical_care_expense_amount: [0, 1_000] + +- name: A dependent's own employer coverage does not bar the filer's line 1 + # The bar is a plan "maintained by any employer of the taxpayer or of the + # taxpayer's spouse"; a dependent's employer is neither. + period: 2025 + input: + people: + head: + age: 50 + employment_income: 50_000 + health_insurance_premiums_without_medicare_part_b: 3_000 + child: + age: 20 + is_tax_unit_dependent: true + is_full_time_student: true + has_esi: true + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: OH + output: + oh_uninsured_unreimbursed_medical_care_expenses: [3_000, 0] + +- name: An offer of employer coverage the filer declined still bars line 1 + period: 2025 + input: + people: + person1: + age: 40 + employment_income: 50_000 + offered_aca_disqualifying_esi: true + health_insurance_premiums_without_medicare_part_b: 5_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + oh_uninsured_unreimbursed_medical_care_expenses: 0 + oh_insured_unreimbursed_medical_care_expense_amount: 5_000 + +- name: Only the Medicare-eligible spouse's premiums leave line 1 + period: 2025 + input: + people: + head: + age: 66 + is_medicare_eligible: true + health_insurance_premiums: 2_000 + spouse: + age: 60 + is_medicare_eligible: false + health_insurance_premiums_without_medicare_part_b: 3_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: OH + output: + oh_uninsured_unreimbursed_medical_care_expenses: [0, 3_000] + oh_insured_unreimbursed_medical_care_expense_amount: [2_000, 0] + +- name: Line 1 is reduced by the federal self-employed health insurance deduction + # Worksheet line 1 note: "You must reduce the amount you enter on this line + # by your federal self-employed health insurance deduction". + period: 2025 + input: + people: + person1: + age: 40 + is_self_employed: true + self_employment_income: 3_000 + health_insurance_premiums: 5_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: OH + output: + # min(3,000 of earnings, 5,000 of premiums). + self_employed_health_insurance_ald: 3_000 + # 5,000 - 3,000. + oh_uninsured_unreimbursed_medical_care_expenses: 2_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml index 5691fa861dc..e0016b82f27 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml @@ -143,3 +143,69 @@ # tax 342 + 2.75% * (52,350 - 26,050) = 1,065.25 (2025 instructions, p. 18). # MAGI less exemptions is 52,350, so the credit is 10% = 106.53. oh_joint_filing_credit: 106.53 + +- name: PolicyBench scenario_025, after-tax premiums of a spouse with employer coverage count on line 3 + # R.C. 5747.01(A)(10)(a) bars the full premium deduction (worksheet line 1) + # for a taxpayer eligible for a subsidized plan of the taxpayer's or the + # spouse's employer. Division (b) still counts premiums the taxpayer paid, + # not otherwise deducted or excluded, toward medical care above 7.5% of + # federal AGI. The 2025 worksheet (p. 41) puts them on line 3, for anyone + # "eligible for Medicare or an employer-paid health care plan", and Ohio's + # FAQ (Q8) says the same of after-tax premiums for an employer plan. The + # inputs are PolicyBench scenario_025's; it previously deducted nothing. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 61 + has_esi: true + hours_worked_last_week: 40 + health_insurance_premiums_without_medicare_part_b: 6_500 + # The same 6,500 under the SPM premium decomposition; not added again. + other_health_insurance_premiums: 6_500 + # Employer-paid, excluded from income under IRC 106, so not paid by + # the taxpayer. + employer_sponsored_insurance_premiums: 21_207.525390625 + other_medical_expenses: 800 + # Not prescribed drugs, so not medical care under IRC 213(b). + over_the_counter_health_expenses: 50 + spouse: + age: 57 + has_esi: true + employment_income: 62_725.29296875 + taxable_private_pension_income: 32_200 + hourly_wage: 19.450000762939453 + hours_worked_last_week: 40 + is_paid_hourly: true + bank_account_assets: 54_394.1171875 + tax_units: + tax_unit: + members: [head, spouse] + first_home_mortgage_balance: 366_000 + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: OH + household_vehicles_value: 9_510 + output: + # 62,725.29 + 32,200. + adjusted_gross_income: 94_925.29 + # Line 1: barred by the employer plan. + oh_uninsured_unreimbursed_medical_care_expenses: [0, 0] + # Line 3 (6,500) + line 4 (800). + oh_insured_unreimbursed_medical_care_expense_amount: [7_300, 0] + # Line 8 = 7,300 - 0.075 * 94,925.29 = 7,300 - 7,119.40 = 180.60. + oh_insured_unreimbursed_medical_care_expenses: 180.60 + oh_unreimbursed_medical_care_expense_deduction: 180.60 + # 94,925.29 - 180.60. + oh_agi: 94_744.69 + # Less 2 * 1,900 exemptions (MAGI above 80,000; R.C. 5747.025). + oh_taxable_income: 90_944.69 + # R.C. 5747.02(A) for 2026: 332 + 2.75% * (90,944.69 - 26,050) = 2,116.60, + # less the 200 retirement income credit (R.C. 5747.055(B)). Was 1,921.57. + oh_income_tax_before_refundable_credits: 1_916.60 + state_income_tax_before_refundable_credits: 1_916.60 From 38b67fafdc4cf70461cb3512c25e2793fcbc2a5f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 19:37:53 -0400 Subject: [PATCH 4/5] Fix Ohio worksheet lines 1 and 3 premium routing (R.C. 5747.01(A)(10)) Line 3 took premiums only of a person who was Medicare-eligible AND whose employer paid none of them; the statute and worksheet say eligible for Medicare OR an employer-paid plan. Line 1 read health_insurance_premiums, an input nothing fills, so premiums reported through the decomposed inputs (the data path) reached neither line. Together these zeroed the premium deduction for PolicyBench scenario_025. - oh_employer_subsidized_health_plan_eligible: the return is eligible when the head or spouse has employer coverage, an offer of it, or an employer paying some or all of their premiums. - oh_medical_care_insurance_premiums: the IRC 213 premium base the federal deduction uses, less the federal self-employed health insurance deduction (worksheet line 1 note). - Lines 1 and 3 split those premiums on Medicare or employer-plan eligibility; the property test checks the partition. - Cite the live 2025 booklet in place of dead tax.ohio.gov/static links. Co-Authored-By: Claude Opus 5.5 --- .../fix-oh-medical-premium-lines.fixed.md | 1 + ...reimbursed_medical_deduction_invariants.py | 99 +++++++++++++++---- ...nreimbursed_medical_care_expense_amount.py | 24 ++--- ...reimbursed_medical_care_expenses_person.py | 6 +- ...mployer_subsidized_health_plan_eligible.py | 30 ++++++ .../oh_medical_care_insurance_premiums.py | 27 +++++ ...ured_unreimbursed_medical_care_expenses.py | 24 ++--- ...imbursed_medical_care_expense_deduction.py | 6 +- 8 files changed, 166 insertions(+), 51 deletions(-) create mode 100644 changelog.d/fix-oh-medical-premium-lines.fixed.md create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_employer_subsidized_health_plan_eligible.py create mode 100644 policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_medical_care_insurance_premiums.py diff --git a/changelog.d/fix-oh-medical-premium-lines.fixed.md b/changelog.d/fix-oh-medical-premium-lines.fixed.md new file mode 100644 index 00000000000..02e7706f9f9 --- /dev/null +++ b/changelog.d/fix-oh-medical-premium-lines.fixed.md @@ -0,0 +1 @@ +Count Ohio health insurance premiums on the unreimbursed medical care worksheet: line 3 now takes premiums of anyone eligible for Medicare or an employer-paid plan (R.C. 5747.01(A)(10)), and line 1 reads the premium inputs the data fills, net of the federal self-employed health insurance deduction. diff --git a/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py index e1be0da4318..f14becb8240 100644 --- a/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py +++ b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py @@ -6,14 +6,18 @@ (premiums with no Medicare or employer plan, and long-term care premiums) are deducted in full. Each person carries their own lines 1 and 2 and a share of line 8 in proportion to their line 5 expenses, and Ohio AGI is the sum over -the members. +the members. Premiums go on line 1 if the person is eligible for neither +Medicare nor a subsidized plan of the taxpayer's or spouse's employer +(R.C. 5747.01(A)(10)(a)), and on line 3, part of line 5, otherwise. Hypothesis draws batches of tax units (single, joint, head of household or -joint with dependents), each person with wages, medical expenses, uninsured -premiums and long-term care premiums, and some heads with a business loss -that makes federal AGI negative. A seeded population of 200 units adds -breadth. Each batch runs as one vectorized simulation, and again with every -medical amount set to zero. For every tax unit: +joint with dependents), each person with wages, medical expenses, health +insurance premiums, long-term care premiums, Medicare eligibility, employer +coverage, an employer coverage offer and an employer contribution category, +and some heads with a business loss that makes federal AGI negative. A seeded +population of 200 units adds breadth. Each batch runs as one vectorized +simulation, and again with every medical amount set to zero. For every tax +unit: 1. Conservation: the members' line 8 shares sum to the return's line 8. 2. Differential: line 8 and each share equal an independent numpy worksheet, @@ -25,6 +29,11 @@ deduction, and raises each person's Ohio AGI (and joint filing credit qualifying income) by exactly their own share, so a spouse never loses qualifying income for the other spouse's expenses. +5. Partition: each person's premiums are on exactly one of lines 1 and 3, so + line 1 plus the premiums on line 3 equals the premiums paid. Line 1 is + zero for everyone on a return where the head or spouse has employer + coverage, an offer of it, or an employer paying some or all of their + premiums, and for every Medicare-eligible person. """ import numpy as np @@ -48,6 +57,12 @@ "health_insurance_premiums", "long_term_health_insurance_premiums", ] +COVERAGE = [ + "is_medicare_eligible", + "has_esi", + "offered_aca_disqualifying_esi", + "employer_contribution_to_health_insurance_premiums_category", +] amounts = st.one_of( st.just(0.0), @@ -56,6 +71,10 @@ ) wages = st.one_of(st.just(0.0), st.integers(1, 200_000).map(float)) losses = st.one_of(st.just(0.0), st.integers(-100_000, -1).map(float)) +# Weighted toward the defaults: no Medicare, no employer coverage and the NONE +# category, which says nothing about whether an employer plan exists. +CATEGORIES = ["NONE", "NONE", "NONE", "SOME", "ALL", "NA"] +rare = st.sampled_from([False, False, False, True]) @st.composite @@ -65,6 +84,12 @@ def people(draw): "other_medical_expenses": draw(amounts), "health_insurance_premiums": draw(amounts), "long_term_health_insurance_premiums": draw(amounts), + "is_medicare_eligible": draw(rare), + "has_esi": draw(rare), + "offered_aca_disqualifying_esi": draw(rare), + "employer_contribution_to_health_insurance_premiums_category": draw( + st.sampled_from(CATEGORIES) + ), } @@ -97,6 +122,12 @@ def person(): "other_medical_expenses": amount(), "health_insurance_premiums": amount(), "long_term_health_insurance_premiums": amount(), + "is_medicare_eligible": bool(rng.random() < 0.25), + "has_esi": bool(rng.random() < 0.25), + "offered_aca_disqualifying_esi": bool(rng.random() < 0.25), + "employer_contribution_to_health_insurance_premiums_category": str( + rng.choice(CATEGORIES) + ), } units = [] @@ -121,14 +152,15 @@ def _situation(units, year, *, medical): def add(name, values, role): people[name] = { - # Under 65 and not disabled, so no one is eligible for Medicare - # and worksheet line 3 is zero. "age": {"head": 45, "spouse": 43, "dependent": 10}[role], "is_tax_unit_head": role == "head", "is_tax_unit_spouse": role == "spouse", "is_tax_unit_dependent": role == "dependent", "employment_income": values["employment_income"], - "employer_contribution_to_health_insurance_premiums_category": "NONE", + **{name: values[name] for name in COVERAGE}, + # No modeled Medicare Part B premium, so the premiums are only + # the drawn ones and zeroing them removes every medical amount. + "takes_up_medicare_if_eligible": False, **{name: values[name] * medical for name in MEDICAL}, } @@ -162,6 +194,11 @@ def add(name, values, role): PERSON = [ "other_medical_expenses", + "health_insurance_premiums", + "oh_medical_care_insurance_premiums", + "is_medicare_eligible", + "has_esi", + "offered_aca_disqualifying_esi", "oh_insured_unreimbursed_medical_care_expense_amount", "oh_insured_unreimbursed_medical_care_expenses_person", "oh_uninsured_unreimbursed_medical_care_expenses", @@ -172,6 +209,7 @@ def add(name, values, role): ] TAX_UNIT = [ "adjusted_gross_income", + "oh_employer_subsidized_health_plan_eligible", "oh_insured_unreimbursed_medical_care_expenses", "oh_unreimbursed_medical_care_expense_deduction", "oh_agi", @@ -185,6 +223,9 @@ def _run(units, year, *, medical=True): for name in PERSON + TAX_UNIT } out["filer"] = np.asarray(sim.calculate("is_tax_unit_head_or_spouse", year)) + out["category"] = sim.calculate( + "employer_contribution_to_health_insurance_premiums_category", year + ).decode_to_str() out["unit"] = sim.populations["tax_unit"].members_entity_id return out @@ -199,11 +240,28 @@ def _check(units, year): line_5_person = base["oh_insured_unreimbursed_medical_care_expense_amount"] line_8 = base["oh_insured_unreimbursed_medical_care_expenses"] share = base["oh_insured_unreimbursed_medical_care_expenses_person"] + line_1 = base["oh_uninsured_unreimbursed_medical_care_expenses"] - # No one is Medicare eligible, so line 5 is each person's medical care. + # 5. Partition, against an independent reading of the worksheet. No one + # is self-employed, so the premiums are the drawn ones. + premiums = base["health_insurance_premiums"] np.testing.assert_allclose( - line_5_person, base["other_medical_expenses"], atol=TOLERANCE + base["oh_medical_care_insurance_premiums"], premiums, atol=TOLERANCE + ) + employer_plan = ( + (base["has_esi"] > 0) + | (base["offered_aca_disqualifying_esi"] > 0) + | np.isin(base["category"], ["SOME", "ALL"]) ) + return_eligible = _unit_sum(base, base["filer"] * employer_plan) > 0 + np.testing.assert_array_equal( + base["oh_employer_subsidized_health_plan_eligible"] > 0, return_eligible + ) + on_line_3 = (base["is_medicare_eligible"] > 0) | return_eligible[unit] + line_3 = line_5_person - base["other_medical_expenses"] + np.testing.assert_allclose(line_1, np.where(on_line_3, 0, premiums), atol=TOLERANCE) + np.testing.assert_allclose(line_3, np.where(on_line_3, premiums, 0), atol=TOLERANCE) + np.testing.assert_allclose(line_1 + line_3, premiums, atol=TOLERANCE) line_5 = _unit_sum(base, line_5_person) # 1. Conservation. @@ -250,13 +308,20 @@ def _check(units, year): np.testing.assert_allclose( without["oh_unreimbursed_medical_care_expense_deduction"], 0, atol=TOLERANCE ) - np.testing.assert_allclose( - without["oh_agi"] - base["oh_agi"], deduction, atol=TOLERANCE - ) + _assert_difference(without["oh_agi"], base["oh_agi"], deduction, "oh_agi") for name in ["oh_agi_person", "oh_joint_filing_credit_qualifying_income"]: - np.testing.assert_allclose( - without[name] - base[name], person_deduction, atol=TOLERANCE, err_msg=name - ) + _assert_difference(without[name], base[name], person_deduction, name) + + +def _assert_difference(minuend, subtrahend, expected, name): + # Incomes are float32, so above 131,072 the spacing between values is + # 0.016 or more, and a difference of two incomes is only exact to about + # one spacing of the larger. + spacing = np.spacing( + np.maximum(np.abs(minuend), np.abs(subtrahend)).astype(np.float32) + ) + error = np.abs(minuend - subtrahend - expected) + assert (error <= TOLERANCE + 2 * spacing).all(), name # A batch's cost is mostly per-variable overhead, so each example is a large diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.py index b7adead891b..2d7c8a538aa 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.py @@ -8,25 +8,21 @@ class oh_insured_unreimbursed_medical_care_expense_amount(Variable): unit = USD definition_period = YEAR reference = ( - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=27", - "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(10) + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10)(b) + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet lines 3 and 4 ) defined_for = StateCode.OH def formula(person, period, parameters): - employer_premium_contribution = person( - "employer_contribution_to_health_insurance_premiums_category", - period, - ) - status = employer_premium_contribution.possible_values + premiums = person("oh_medical_care_insurance_premiums", period) medicare_eligible = person("is_medicare_eligible", period) - # Line 3 - eligible_premiums = ( - person("medical_expense_health_insurance_premiums", period) - * medicare_eligible - ) * (employer_premium_contribution == status.NONE) - # Premiums only count if the employer paid none. + employer_plan_eligible = person.tax_unit( + "oh_employer_subsidized_health_plan_eligible", period + ) + # Line 3: premiums paid while eligible for Medicare or an + # employer-paid plan. + eligible_premiums = premiums * (medicare_eligible | employer_plan_eligible) # Line 4 other_medical_expenses = person("other_medical_expenses", period) return eligible_premiums + other_medical_expenses diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses_person.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses_person.py index 03d07755a80..28e9e72fa0b 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses_person.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses_person.py @@ -8,9 +8,9 @@ class oh_insured_unreimbursed_medical_care_expenses_person(Variable): unit = USD definition_period = YEAR reference = ( - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=27", - "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(10) + "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=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet ) defined_for = StateCode.OH diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_employer_subsidized_health_plan_eligible.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_employer_subsidized_health_plan_eligible.py new file mode 100644 index 00000000000..fa6ae208be7 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_employer_subsidized_health_plan_eligible.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class oh_employer_subsidized_health_plan_eligible(Variable): + value_type = bool + entity = TaxUnit + label = "Ohio filer eligible for an employer-subsidized health plan" + definition_period = YEAR + reference = ( + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10)(a) + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet lines 1 and 3 + ) + defined_for = StateCode.OH + + def formula(tax_unit, period, parameters): + person = tax_unit.members + # Enrolled in, or offered, employer coverage. + has_employer_coverage = person("has_esi", period) | person( + "offered_aca_disqualifying_esi", period + ) + contribution = person( + "employer_contribution_to_health_insurance_premiums_category", + period, + ) + status = contribution.possible_values + employer_pays = (contribution == status.SOME) | (contribution == status.ALL) + # The plan must be maintained by the taxpayer's or the spouse's + # employer, so a dependent's own coverage does not count. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + return tax_unit.any(head_or_spouse & (has_employer_coverage | employer_pays)) 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 new file mode 100644 index 00000000000..ce0c6d7c816 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_medical_care_insurance_premiums.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class oh_medical_care_insurance_premiums(Variable): + value_type = float + entity = Person + label = "Ohio medical 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)(a)-(b) + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet lines 1 and 3 + ) + defined_for = StateCode.OH + + def formula(person, period, parameters): + # Premiums counted as IRC 213 medical care, as in the federal medical + # expense deduction. Premiums paid through pre-tax payroll deductions + # are a separate input, excluded from wages, so they are not included. + 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 + ) + return max_(premiums - self_employed_deduction, 0) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_uninsured_unreimbursed_medical_care_expenses.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_uninsured_unreimbursed_medical_care_expenses.py index bd9befac63b..54dada758e1 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_uninsured_unreimbursed_medical_care_expenses.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_uninsured_unreimbursed_medical_care_expenses.py @@ -8,22 +8,18 @@ class oh_uninsured_unreimbursed_medical_care_expenses(Variable): unit = USD definition_period = YEAR reference = ( - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=27", - "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(10) + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10)(a) + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet line 1 ) defined_for = StateCode.OH def formula(person, period, parameters): - not_medicare_eligible = ~person("is_medicare_eligible", period) - employer_premium_contribution = person( - "employer_contribution_to_health_insurance_premiums_category", - period, + premiums = person("oh_medical_care_insurance_premiums", period) + medicare_eligible = person("is_medicare_eligible", period) + employer_plan_eligible = person.tax_unit( + "oh_employer_subsidized_health_plan_eligible", period ) - status = employer_premium_contribution.possible_values - health_insurance_premiums = person("health_insurance_premiums", period) - # Premiums only count if the employer paid none. - eligible_health_insurance_premiums = health_insurance_premiums * ( - employer_premium_contribution == status.NONE - ) - return not_medicare_eligible * eligible_health_insurance_premiums + # Line 1: premiums paid while eligible for neither Medicare nor an + # employer-paid plan. Otherwise they go on line 3. + return premiums * ~(medicare_eligible | employer_plan_eligible) diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction.py index 2c9bbe3ffd9..384aaa071c4 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction.py @@ -8,9 +8,9 @@ class oh_unreimbursed_medical_care_expense_deduction(Variable): unit = USD definition_period = YEAR reference = ( - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=27", - "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(10) + "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=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet ) defined_for = StateCode.OH From 9bb7314d57fb56421abe0265c7d1d15224b49b48 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 20:54:32 -0400 Subject: [PATCH 5/5] Cite Ohio Line 44 instructions on pp. 25-26 and the medical FAQ The Line 44 heading starts at the foot of PDF p. 25; the operative text, including the pre-tax exclusion, is on p. 26. Adds the Department's FAQ (Q8 and Q9: after-tax premiums for an employer plan go on line 3). Co-Authored-By: Claude Opus 5.5 --- .../oh_insured_unreimbursed_medical_care_expense_amount.yaml | 2 +- .../oh_insured_unreimbursed_medical_care_expense_amount.py | 3 ++- .../oh_insured_unreimbursed_medical_care_expenses_person.py | 2 +- .../medical_exepenses/oh_medical_care_insurance_premiums.py | 2 +- .../oh_uninsured_unreimbursed_medical_care_expenses.py | 2 +- .../oh_unreimbursed_medical_care_expense_deduction.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.yaml index e8ea897e5fd..928bbe7e150 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.yaml @@ -78,7 +78,7 @@ oh_uninsured_unreimbursed_medical_care_expenses: 6_000 - name: Premiums paid through pre-tax payroll deductions are not deducted - # Line 44 instructions (p. 25) and FAQ Q2: no deduction for amounts paid + # Line 44 instructions (p. 26) and FAQ Q2: no deduction for amounts paid # with pre-tax dollars. They are excluded from federal wages instead. period: 2025 input: diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.py index 2d7c8a538aa..fac1790799c 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expense_amount.py @@ -9,8 +9,9 @@ class oh_insured_unreimbursed_medical_care_expense_amount(Variable): definition_period = YEAR reference = ( "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10)(b) - "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44, pp. 25-26 "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet lines 3 and 4 + "https://tax.ohio.gov/help-center/faqs/income-medical-and-health-care-expenses/income-medical-and-health-care-expenses", # Q8 and Q9: after-tax premiums go on line 3 ) defined_for = StateCode.OH diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses_person.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses_person.py index 28e9e72fa0b..6386719ef97 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses_person.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses_person.py @@ -9,7 +9,7 @@ class oh_insured_unreimbursed_medical_care_expenses_person(Variable): 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=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44, pp. 25-26 "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet ) defined_for = StateCode.OH 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 ce0c6d7c816..3094d5705a6 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 @@ -9,7 +9,7 @@ class oh_medical_care_insurance_premiums(Variable): definition_period = YEAR reference = ( "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10)(a)-(b) - "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44, pp. 25-26 "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet lines 1 and 3 ) defined_for = StateCode.OH diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_uninsured_unreimbursed_medical_care_expenses.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_uninsured_unreimbursed_medical_care_expenses.py index 54dada758e1..166bd2ab754 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_uninsured_unreimbursed_medical_care_expenses.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_uninsured_unreimbursed_medical_care_expenses.py @@ -9,7 +9,7 @@ class oh_uninsured_unreimbursed_medical_care_expenses(Variable): definition_period = YEAR reference = ( "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10)(a) - "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44, pp. 25-26 "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet line 1 ) defined_for = StateCode.OH diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction.py index 384aaa071c4..ee0cc284978 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction.py @@ -9,7 +9,7 @@ class oh_unreimbursed_medical_care_expense_deduction(Variable): 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=25", # Line 44 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=25", # Line 44, pp. 25-26 "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet ) defined_for = StateCode.OH