Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fix-oh-medical-premium-lines.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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)
),
}


Expand Down Expand Up @@ -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 = []
Expand All @@ -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},
}

Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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. 26) 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
Loading
Loading