diff --git a/changelog.d/fix-schedule-a-dependent-paid-premiums.fixed.md b/changelog.d/fix-schedule-a-dependent-paid-premiums.fixed.md new file mode 100644 index 00000000000..fa21e118f6d --- /dev/null +++ b/changelog.d/fix-schedule-a-dependent-paid-premiums.fixed.md @@ -0,0 +1 @@ +Count only the head's and spouse's own health insurance premium payments in federal Schedule A medical expenses (IRC 213(a)). A dependent's own payments no longer enter the filers' subtotal, New Mexico's medical deduction, credit and exemption, or New Jersey's general medical base (Worksheet F line 1). North Dakota's renter refund keeps them (NDCC 57-02-08.1(5)(c)), and Montana's 2021-2023 line 1 takes out only the filers' premiums. diff --git a/policyengine_us/tests/core/test_id_health_insurance_premiums.py b/policyengine_us/tests/core/test_id_health_insurance_premiums.py index b27b2be36ac..7fa88266780 100644 --- a/policyengine_us/tests/core/test_id_health_insurance_premiums.py +++ b/policyengine_us/tests/core/test_id_health_insurance_premiums.py @@ -249,7 +249,11 @@ def test_deleted_medical_input_recalculated_federal_first(): "dependent": { "age": {year: 20}, "is_tax_unit_dependent": {year: True}, - "health_insurance_premiums": {year: 500}, + # Other costs, which the federal subtotal counts for every + # member and Idaho's claimant worksheet leaves out, keep + # the two amounts apart. A dependent's own premiums would + # not: neither counts them. + "other_medical_expenses": {year: 500}, }, }, "tax_units": { @@ -273,7 +277,7 @@ def test_deleted_medical_input_recalculated_federal_first(): # Federal first: 3,000 + 500 - 30,000 * 7.5% = 1,250. assert simulation.calculate("medical_expense_deduction", year).tolist() == [1_250] - # Idaho excludes the dependent's payment: medical overlap is + # Idaho excludes the dependent's costs: medical overlap is # 3,000 - 30,000 * 7.5% = 750, leaving 3,000 - 750 = 2,250. # Core's provenance defect before 3.32.27 uses 1,250 and returns 1,750. assert simulation.calculate( diff --git a/policyengine_us/tests/core/test_medical_expense_premium_conservation.py b/policyengine_us/tests/core/test_medical_expense_premium_conservation.py index 5734523889c..89df0d2d1e9 100644 --- a/policyengine_us/tests/core/test_medical_expense_premium_conservation.py +++ b/policyengine_us/tests/core/test_medical_expense_premium_conservation.py @@ -6,11 +6,14 @@ tax units. Premium inputs use payer attribution: a person's inputs include all premiums that person paid, including family coverage regardless of who is covered. +Schedule A counts what the head and spouse paid (IRC 213(a)), so a +dependent's own premiums never enter it. The federal conservation properties generate deductions within the filers' paid premiums; beneficiary-assigned inputs cannot establish that invariant. The independent accounting invariant is that Schedule A's premium base plus -the self-employed health insurance deduction never exceeds premiums paid. -Non-premium medical expenses must remain available in full before the floor. +the self-employed health insurance deduction never exceeds the premiums the +filers paid. Every member's non-premium medical expenses must remain +available in full before the floor. Examples use small vectorized simulations and a shared read-only reference system. Supplied and computed subtotal scenarios use separate simulations to @@ -129,11 +132,11 @@ def _simulation(members, reference_system, deductions=None): def _assert_conservation(members, simulation): - paid = np.array([sum(person["premiums"] for person in unit) for unit in members]) - other = np.array([sum(person["other"] for person in unit) for unit in members]) - dependent_premiums = np.array( - [sum(person["premiums"] for person in unit[2:]) for unit in members] + # Positions 0 and 1 are the head and spouse; position 2 is a dependent. + filer_paid = np.array( + [sum(person["premiums"] for person in unit[:2]) for unit in members] ) + other = np.array([sum(person["other"] for person in unit) for unit in members]) medical_base = simulation.calculate("itemized_medical_expenses", PERIOD) se_deduction = simulation.calculate("self_employed_health_insurance_ald", PERIOD) schedule_a_premium_use = medical_base - other @@ -141,11 +144,11 @@ def _assert_conservation(members, simulation): # Neither the SE deduction nor a different filing unit can consume the # non-premium expenses. Check the bound separately from conservation. assert (schedule_a_premium_use >= 0).all() - assert (schedule_a_premium_use >= dependent_premiums).all() - assert (schedule_a_premium_use + se_deduction <= paid).all() - # All premiums here are otherwise eligible: unused premiums remain in - # Schedule A's base, even when earnings cap the SE deduction. - np.testing.assert_array_equal(schedule_a_premium_use + se_deduction, paid) + assert (schedule_a_premium_use + se_deduction <= filer_paid).all() + # All the filers' premiums here are otherwise eligible: unused premiums + # remain in Schedule A's base, even when earnings cap the SE deduction. + # A dependent's own premiums are in neither. + np.testing.assert_array_equal(schedule_a_premium_use + se_deduction, filer_paid) EXAMPLE_UNITS = [ @@ -219,7 +222,11 @@ def nj_dependent_expenses(draw): nj_units = st.lists( - st.lists(nj_dependent_expenses(), min_size=2, max_size=4), + st.tuples( + # The parent's own premiums, then the dependents. + st.integers(0, 25_000), + st.lists(nj_dependent_expenses(), min_size=2, max_size=4), + ), min_size=2, max_size=4, ) @@ -227,10 +234,14 @@ def nj_dependent_expenses(draw): def _nj_simulation(members, reference_system): people, tax_units, households, marital_units = {}, {}, {}, {} - for unit_index, unit in enumerate(members): + for unit_index, (parent_premiums, unit) in enumerate(members): parent = f"parent_{unit_index}" names = [parent] - people[parent] = {"age": {PERIOD: 40}} + people[parent] = { + "age": {PERIOD: 40}, + "medicare_enrolled": {PERIOD: False}, + "health_insurance_premiums": {PERIOD: parent_premiums}, + } marital_units[f"parent_marital_unit_{unit_index}"] = {"members": [parent]} for person_index, person in enumerate(unit): name = f"dependent_{unit_index}_{person_index}" @@ -272,47 +283,43 @@ def _nj_simulation(members, reference_system): @settings(max_examples=12, deadline=None, derandomize=True) @example( members=[ - [ - dict(premiums=0, other=0, deduction=1_000), - dict(premiums=1_000, other=0, deduction=0), - ], - [ - dict(premiums=300, other=200, deduction=800), - dict(premiums=500, other=100, deduction=0), - ], + ( + 0, + [ + dict(premiums=0, other=0, deduction=1_000), + dict(premiums=1_000, other=0, deduction=0), + ], + ), + ( + 300, + [ + dict(premiums=300, other=200, deduction=800), + dict(premiums=500, other=100, deduction=0), + ], + ), ] ) @given(members=nj_units) -def test_nj_excludes_each_dependents_deduction_only_from_their_own_premiums( +def test_nj_counts_the_filers_premiums_and_each_dependents_own_deduction( reference_system, members ): simulation = _nj_simulation(members, reference_system) - paid = np.array([sum(person["premiums"] for person in unit) for unit in members]) - other = np.array([sum(person["other"] for person in unit) for unit in members]) + parent_paid = np.array([parent_premiums for parent_premiums, _ in members]) + other = np.array([sum(person["other"] for person in unit) for _, unit in members]) deductions = np.array( - [sum(person["deduction"] for person in unit) for unit in members] + [sum(person["deduction"] for person in unit) for _, unit in members] ) - person_exclusions = [ - [min(person["deduction"], person["premiums"]) for person in unit] - for unit in members - ] - for unit, exclusions in zip(members, person_exclusions): - assert all( - 0 <= exclusion <= person["premiums"] - for person, exclusion in zip(unit, exclusions) - ) - excluded = np.array([sum(exclusions) for exclusions in person_exclusions]) nj_deduction = simulation.calculate("nj_medical_expense_deduction", PERIOD) - actual_exclusion = paid + other + deductions - nj_deduction - # C.54A:3-3(c)(3) excludes only amounts separately deducted. Each person's - # exclusion is bounded before summation, so no ALD can consume a different - # person's premiums. This property also covers ALDs above own paid costs. + # Worksheet F line 1 counts expenses the filers paid for themselves and + # their dependents: the parent's premiums and every member's other costs. + # A dependent's own premiums never enter it, whatever that dependent + # deducted, so no deduction can consume another person's premiums. Line 5 + # keeps each dependent's separate SE deduction, since New Jersey counts + # the dependent's income (C.54A:3-3(c)(3); 2025 NJ-1040 instructions, + # line 31). # https://pub.njleg.gov/bills/9899/PL99/222_.HTM - assert (actual_exclusion >= 0).all() - assert (actual_exclusion <= excluded).all() - np.testing.assert_array_equal(actual_exclusion, excluded) - np.testing.assert_array_equal(nj_deduction, paid + other + deductions - excluded) + np.testing.assert_array_equal(nj_deduction, parent_paid + other + deductions) # Flat strategies keep generation small while varying independent caller @@ -373,8 +380,8 @@ def _state_simulation(cases, reference_system, with_se_deduction): "nm_medical_expense_exemption": [], } accounting = { - "main_base": [], - "gross": [], + "filer_base": [], + "filer_gross": [], "other": [], "filer_premiums": [], "excluded": [], @@ -388,14 +395,20 @@ def _state_simulation(cases, reference_system, with_se_deduction): other = [0 if subtotal_only else case["other"][i] for i in person_indices] deduction = case["deduction"] if with_se_deduction else 0 filer_premiums = premiums[0] + (premiums[1] if joint else 0) + # Person 2, always present, is the dependent. + dependent_premiums = premiums[-1] excluded = min(filer_premiums, max(0, deduction)) - gross = sum(premiums) + sum(other) + # What the filers paid: their premiums and every member's other costs. + filer_gross = filer_premiums + sum(other) supplied = case["subtotal_mode"] != "derived" # Caller subtotals are independent of their component inputs and SE - # deduction. The oracle preserves the supplied amount exactly as main - # does; only the computed path restores an applied federal exclusion. - main_base = case["subtotal"] if supplied else gross - senior_eligible = case["age"] >= 65 and main_base >= 28_000 + # deduction. The oracle uses a supplied amount exactly; only the + # computed path restores an applied federal exclusion. + filer_base = case["subtotal"] if supplied else filer_gross + # NDCC 57-02-08.1(5)(c) also takes out what the dependent paid, which + # a Schedule A amount never includes. + nd_base = filer_base + dependent_premiums + senior_eligible = case["age"] >= 65 and filer_base >= 28_000 denominator = 2 if case["filing_status"] == "SEPARATE" else 1 for state in ("ND", "NM"): @@ -445,17 +458,17 @@ def _state_simulation(cases, reference_system, with_se_deduction): tax_unit["itemized_medical_expenses"] = {PERIOD: case["subtotal"]} tax_units[unit_name] = tax_unit households[unit_name] = {"members": names, "state_code": {PERIOD: state}} - accounting["main_base"].append(main_base) - accounting["gross"].append(gross) + accounting["filer_base"].append(filer_base) + accounting["filer_gross"].append(filer_gross) accounting["other"].append(sum(other)) accounting["filer_premiums"].append(filer_premiums) accounting["excluded"].append(excluded) accounting["applied_exclusion"].append(0 if supplied else excluded) - # This independent accounting oracle reproduces main's state - # outputs from its paid-cost base rather than reading this head's - # medical subtotal or excluded-premium calculations as expectations. + # This independent accounting oracle derives the state outputs + # from the paid-cost bases rather than reading the model's + # medical subtotal or excluded-premium calculations. expectations["nd_renters_refund_income"].append( - max(0, case["income"] - main_base) if state == "ND" else 0 + max(0, case["income"] - nd_base) if state == "ND" else 0 ) expectations["nm_medical_expense_credit"].append( 2_800 / denominator @@ -499,7 +512,7 @@ def _assert_state_outputs(cases, reference_system, with_se_deduction): applied_exclusion, accounting["applied_exclusion"] ) np.testing.assert_array_equal( - medical_base + applied_exclusion, accounting["main_base"] + medical_base + applied_exclusion, accounting["filer_base"] ) if not supplied: excluded = simulation.calculate( @@ -509,7 +522,9 @@ def _assert_state_outputs(cases, reference_system, with_se_deduction): # capped premium exclusion is restored. These bounds deliberately # do not constrain an independent caller-supplied subtotal. np.testing.assert_array_equal(excluded, accounting["excluded"]) - np.testing.assert_array_equal(medical_base + excluded, accounting["gross"]) + np.testing.assert_array_equal( + medical_base + excluded, accounting["filer_gross"] + ) assert (excluded >= 0).all() assert (excluded <= np.array(accounting["filer_premiums"])).all() assert (medical_base >= np.array(accounting["other"])).all() @@ -536,7 +551,7 @@ def _assert_state_outputs(cases, reference_system, with_se_deduction): ] ) @given(cases=state_units) -def test_state_paid_cost_bases_match_main_without_se_deduction(reference_system, cases): +def test_state_paid_cost_bases_without_se_deduction(reference_system, cases): _assert_state_outputs(cases, reference_system, with_se_deduction=False) @@ -565,7 +580,5 @@ def test_state_paid_cost_bases_match_main_without_se_deduction(reference_system, ] ) @given(cases=state_units) -def test_state_paid_cost_bases_preserve_main_inputs_with_se_deduction( - reference_system, cases -): +def test_state_paid_cost_bases_with_se_deduction(reference_system, cases): _assert_state_outputs(cases, reference_system, with_se_deduction=True) diff --git a/policyengine_us/tests/core/test_schedule_a_payer_attribution_properties.py b/policyengine_us/tests/core/test_schedule_a_payer_attribution_properties.py new file mode 100644 index 00000000000..4f3bd259e64 --- /dev/null +++ b/policyengine_us/tests/core/test_schedule_a_payer_attribution_properties.py @@ -0,0 +1,228 @@ +"""Schedule A counts the premiums the filers paid, not a dependent's own. + +IRC 213(a) allows medical expenses the taxpayer paid for the taxpayer, the +spouse and dependents. Premium inputs sit on the person who paid them, so a +parent-paid policy covering a dependent is the parent's input, and a +dependent's premium input is the dependent's own payment. Other medical +expenses sit on the person cared for, including amounts the parents paid on a +dependent's behalf, so every member's count. + +Hypothesis draws batches of single and joint returns with one or two +dependents, premiums and other medical expenses on every member, and a +supplied self-employed health insurance deduction. For each return it builds +these variants in one vectorized simulation per year: + +- the drawn return; +- the same return with part of one filer's premiums moved to a dependent; +- the premiums moved back, which repeats the drawn inputs in another row; +- the drawn return with the head and spouse labels exchanged; +- the drawn return with every dependent's premiums set to 0. + +Invariants, for every drawn return: + +1. Differential: `itemized_medical_expenses` equals an independent oracle, + max(0, filer premiums - deduction) + every member's other medical + expenses. +2. Dependent invariance: the filers' subtotal, federal medical deduction, + New Mexico's medical care expense deduction (PIT-1 line 16) and senior + medical credit and exemption, New Jersey's medical expense deduction and + Montana's 2021-2023 joint medical deduction do not depend on a + dependent's own premiums. +3. Payer move: moving premiums from a filer to a dependent never raises any of + those amounts. The moved-back row has the drawn inputs, so it checks only + that each return's result depends on its own inputs (determinism, no + leakage between rows): it restores each amount to the cent. +4. Labels: exchanging the head and spouse labels changes none of them, nor + North Dakota's renter refund income. +5. North Dakota: NDCC 57-02-08.1(5)(c) takes out medical expenses paid by the + applicant or the applicant's dependent, so renter refund income does not + change when premiums move between a filer and a dependent, and it equals + income less every member's premiums and other medical expenses. + +YAML covers the legal examples; these properties check the payer rule across +generated returns for the readers named above, not every reader of the +subtotal. +""" + +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 + +CENT = 0.01 +# Variables read on the filers' return, by the year in which they apply. +FILER_AMOUNTS = { + # Montana's itemized schedule applies 2021-2023. + 2022: ["mt_medical_expense_deduction_joint"], + # New Mexico's medical care expense deduction ends after 2024. + 2024: [ + "itemized_medical_expenses", + "medical_expense_deduction", + "nm_medical_care_expense_deduction", + "nm_medical_expense_credit", + "nm_medical_expense_exemption", + "nj_medical_expense_deduction", + ], +} +STATES = {2022: ["MT"], 2024: ["NM", "NJ"], 2025: ["ND"]} +VARIANTS = ["drawn", "moved", "moved_back", "swapped", "no_dependent_premiums"] + + +@st.composite +def returns(draw): + joint = draw(st.booleans()) + dependents = draw(st.integers(1, 2)) + filers = 2 if joint else 1 + premiums = [draw(st.integers(0, 20_000)) for _ in range(filers + dependents)] + other = [draw(st.integers(0, 15_000)) for _ in range(filers + dependents)] + filer_premiums = sum(premiums[:filers]) + mover = draw(st.integers(0, filers - 1)) + return { + "joint": joint, + "filers": filers, + "premiums": premiums, + "other": other, + "head_age": draw(st.sampled_from([45, 66])), + "income": draw(st.integers(0, 120_000)), + # Within the filers' premiums, as the payer contract requires, or + # above them. + "deduction": draw(st.integers(0, filer_premiums + 5_000)), + "mover": mover, + "moved": draw(st.integers(0, premiums[mover])), + "receiver": filers + draw(st.integers(0, dependents - 1)), + } + + +def _variant(case, variant): + premiums = list(case["premiums"]) + head, spouse = 0, 1 if case["joint"] else None + if variant == "moved": + premiums[case["mover"]] -= case["moved"] + premiums[case["receiver"]] += case["moved"] + elif variant == "no_dependent_premiums": + premiums[case["filers"] :] = [0] * (len(premiums) - case["filers"]) + elif variant == "swapped" and case["joint"]: + head, spouse = 1, 0 + # "moved_back" undoes "moved": its inputs are the drawn ones. + return premiums, head, spouse + + +def _simulation(cases, year): + people, tax_units, households, marital_units = {}, {}, {}, {} + rows = [] + for state in STATES[year]: + for case_index, case in enumerate(cases): + for variant in VARIANTS: + unit = f"{state}_{case_index}_{variant}" + premiums, head, spouse = _variant(case, variant) + names = [] + for index, paid in enumerate(premiums): + name = f"{unit}_{index}" + names.append(name) + filer = index < case["filers"] + people[name] = { + variable: {year: value} + for variable, value in { + "age": (case["head_age"] if index == 0 else 40) + if filer + else 17, + "is_tax_unit_head": index == head, + "is_tax_unit_spouse": index == spouse, + "is_tax_unit_dependent": not filer, + "employment_income": case["income"] if index == 0 else 0, + "medicare_enrolled": False, + "health_insurance_premiums": paid, + "other_medical_expenses": case["other"][index], + }.items() + } + if not filer: + marital_units[f"{name}_marital_unit"] = {"members": [name]} + marital_units[f"{unit}_marital_unit"] = { + "members": names[: case["filers"]] + } + tax_units[unit] = { + "members": names, + "self_employed_health_insurance_ald": {year: case["deduction"]}, + } + households[unit] = {"members": names, "state_code": {year: state}} + rows.append((state, case_index, variant)) + simulation = Simulation( + tax_benefit_system=system, + situation={ + "people": people, + "tax_units": tax_units, + "households": households, + "marital_units": marital_units, + }, + ) + return simulation, rows + + +def _by_row(values, rows): + return {row: value for row, value in zip(rows, values)} + + +def _tax_unit_amount(simulation, variable, year): + entity = simulation.tax_benefit_system.variables[variable].entity.key + values = simulation.calculate(variable, year) + if entity == "person": + # Montana reports the joint deduction on the head; sum over the unit. + return simulation.map_result(values, "person", "tax_unit") + return values + + +def _assert_filer_amounts(cases, year): + simulation, rows = _simulation(cases, year) + for variable in FILER_AMOUNTS[year]: + values = _by_row(_tax_unit_amount(simulation, variable, year), rows) + for state in STATES[year]: + for case_index, case in enumerate(cases): + key = (state, case_index) + drawn = values[(*key, "drawn")] + # 2. A dependent's own premiums do not reach the filers. + np.testing.assert_allclose( + values[(*key, "no_dependent_premiums")], drawn, atol=CENT + ) + # 3. Moving premiums to a dependent never raises the amount; + # the moved-back row repeats the drawn inputs (determinism). + assert values[(*key, "moved")] <= drawn + CENT, (variable, case) + np.testing.assert_allclose( + values[(*key, "moved_back")], drawn, atol=CENT + ) + # 4. Either spouse may be labelled head. + np.testing.assert_allclose(values[(*key, "swapped")], drawn, atol=CENT) + return simulation, rows + + +@settings(max_examples=8, deadline=None, derandomize=True) +@given(cases=st.lists(returns(), min_size=2, max_size=3)) +def test_filers_count_only_their_own_premium_payments(cases): + simulation, rows = _assert_filer_amounts(cases, 2024) + # 1. Differential against an independent oracle. + subtotal = _by_row(simulation.calculate("itemized_medical_expenses", 2024), rows) + for (state, case_index, variant), value in subtotal.items(): + case = cases[case_index] + premiums, _, _ = _variant(case, variant) + filer_premiums = sum(premiums[: case["filers"]]) + expected = max(0, filer_premiums - case["deduction"]) + sum(case["other"]) + assert value == pytest.approx(expected, abs=CENT), (variant, case) + _assert_filer_amounts(cases, 2022) + + +@settings(max_examples=8, deadline=None, derandomize=True) +@given(cases=st.lists(returns(), min_size=2, max_size=3)) +def test_north_dakota_counts_premiums_either_payer_made(cases): + simulation, rows = _simulation(cases, 2025) + income = _by_row(simulation.calculate("nd_renters_refund_income", 2025), rows) + for case_index, case in enumerate(cases): + drawn = income[("ND", case_index, "drawn")] + # 5. Every member's premiums and other medical expenses come off. + expected = max(0, case["income"] - sum(case["premiums"]) - sum(case["other"])) + assert drawn == pytest.approx(expected, abs=CENT), case + for variant in ("moved", "moved_back", "swapped"): + np.testing.assert_allclose( + income[("ND", case_index, variant)], drawn, atol=CENT + ) 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..69075f8bff7 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,196 @@ standard_deduction: 15_750 # 58,000 - 15,750. taxable_income: 42_250 + +# Premiums a dependent pays. +# +# IRC 213(a) allows "the expenses paid during the taxable year, not +# compensated for by insurance or otherwise, for medical care of the +# taxpayer, his spouse, or a dependent". Premium inputs sit on the person who +# paid them: a parent-paid policy covering a dependent is the parent's input +# and counts, while premiums a dependent paid are the dependent's own and do +# not. Other medical expenses sit on the person cared for, including amounts +# the parents paid on the dependent's behalf (the CPS ASEC medical care +# question), so the dependent's count. +# +# Each case has two tax units with the same people and inputs: the first +# labels the earner as head, the second as spouse. The results must match. + +- name: A dependent's own premium payments are not the filers' medical expenses. + period: 2025 + input: + people: + a_earner: + age: 45 + is_tax_unit_head: true + employment_income: 8_000 + a_partner: + age: 45 + is_tax_unit_spouse: true + a_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + other_medical_expenses: 800 + b_earner: + age: 45 + is_tax_unit_spouse: true + employment_income: 8_000 + b_partner: + age: 45 + is_tax_unit_head: true + b_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + other_medical_expenses: 800 + tax_units: + a: + members: [a_earner, a_partner, a_student] + b: + members: [b_partner, b_earner, b_student] + households: + a: + members: [a_earner, a_partner, a_student] + b: + members: [b_partner, b_earner, b_student] + output: + medical_expense_health_insurance_premiums: [0, 0, 1_200, 0, 0, 1_200] + adjusted_gross_income: [8_000, 8_000] + # The student's 800 of medical bills; counting the student's own 1,200 + # of premiums would give 2,000. + itemized_medical_expenses: [800, 800] + # 800 - 7.5% of 8,000. + medical_expense_deduction: [200, 200] + +- name: Coverage a filer buys for a dependent counts in full. + period: 2025 + input: + people: + a_earner: + age: 45 + is_tax_unit_head: true + employment_income: 8_000 + a_partner: + age: 45 + is_tax_unit_spouse: true + # The partner pays the 1,200 policy that covers the student. + health_insurance_premiums: 1_200 + a_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + other_medical_expenses: 800 + b_earner: + age: 45 + is_tax_unit_spouse: true + employment_income: 8_000 + b_partner: + age: 45 + is_tax_unit_head: true + health_insurance_premiums: 1_200 + b_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + other_medical_expenses: 800 + tax_units: + a: + members: [a_earner, a_partner, a_student] + b: + members: [b_partner, b_earner, b_student] + households: + a: + members: [a_earner, a_partner, a_student] + b: + members: [b_partner, b_earner, b_student] + output: + # 1,200 + 800. + itemized_medical_expenses: [2_000, 2_000] + # 2,000 - 7.5% of 8,000. + medical_expense_deduction: [1_400, 1_400] + +- name: The filers' self-employed deduction never reaches a dependent's own premiums. + period: 2025 + input: + people: + a_earner: + age: 45 + is_tax_unit_head: true + health_insurance_premiums: 3_000 + other_medical_expenses: 2_500 + a_partner: + age: 45 + is_tax_unit_spouse: true + a_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + other_medical_expenses: 500 + b_earner: + age: 45 + is_tax_unit_spouse: true + health_insurance_premiums: 3_000 + other_medical_expenses: 2_500 + b_partner: + age: 45 + is_tax_unit_head: true + b_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + other_medical_expenses: 500 + tax_units: + a: + members: [a_earner, a_partner, a_student] + self_employed_health_insurance_ald: 2_000 + adjusted_gross_income: 40_000 + b: + members: [b_partner, b_earner, b_student] + self_employed_health_insurance_ald: 2_000 + adjusted_gross_income: 40_000 + households: + a: + members: [a_earner, a_partner, a_student] + b: + members: [b_partner, b_earner, b_student] + output: + itemized_medical_expenses_excluded_premiums: [2_000, 2_000] + # (3,000 - 2,000) + 2,500 + 500; the student's 1,200 is not counted. + itemized_medical_expenses: [4_000, 4_000] + # 4,000 - 7.5% of 40,000. + medical_expense_deduction: [1_000, 1_000] + +- name: A single parent does not count a dependent's own Medicare premiums. + period: 2025 + input: + people: + parent: + age: 50 + employment_income: 30_000 + other_medical_expenses: 1_000 + relative: + # A disabled adult relative claimed as a dependent, whose Part B + # premiums are withheld from their own benefits. + age: 30 + is_tax_unit_dependent: true + medicare_enrolled: true + medicare_part_b_premium: 2_220 + other_medical_expenses: 3_000 + tax_units: + tax_unit: + members: [parent, relative] + households: + household: + members: [parent, relative] + output: + medical_expense_health_insurance_premiums: [0, 2_220] + # 1,000 + 3,000 of bills; the relative's 2,220 of Part B premiums are not + # the parent's payment. + itemized_medical_expenses: 4_000 + # 4,000 - 7.5% of 30,000. + medical_expense_deduction: 1_750 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/medical_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/medical_expense_deduction.yaml index 1228ba0acf8..990da13dbc0 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/medical_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/medical_expense_deduction.yaml @@ -120,7 +120,7 @@ itemized_medical_expenses: [5_500, 5_500, 6_500] medical_expense_deduction: [3_250, 3_250, 4_250] -- name: A filer SE deduction cannot consume premiums attributed to a dependent. +- name: A filer SE deduction above the filer's premiums leaves the other medical expenses. period: 2025 input: people: @@ -139,8 +139,12 @@ self_employed_health_insurance_ald: 1_000 adjusted_gross_income: 0 output: - itemized_medical_expenses: 800 - medical_expense_deduction: 800 + # The parent's 300 of premiums were deducted on Schedule 1 (IRC + # 162(l)(3)), and the child's 500 are the child's own payment, not an + # expense the parent paid (IRC 213(a)). The 100 + 200 of other medical + # expenses remain. + itemized_medical_expenses: 300 + medical_expense_deduction: 300 - name: Parent-paid family coverage is attributed entirely to the parent. period: 2025 @@ -181,11 +185,11 @@ self_employed_health_insurance_ald: 800 adjusted_gross_income: 0 output: - # The exclusion is min(800 ALD, 300 filer-paid premiums) = 300, leaving - # the dependent's 500. Attributed premium uses are 300 + 500 = 800. - # Supplying an 800 parent ALD with only 300 parent-paid premiums violates - # the payer contract; adding the full supplied ALD to Schedule A gives - # 1,300 and is not a conservation guarantee for these inconsistent inputs. - # A parent who actually paid all 800 must use the preceding input pattern. - itemized_medical_expenses: 500 - medical_expense_deduction: 500 + # The exclusion is min(800 ALD, 300 filer-paid premiums) = 300. Under the + # payer contract the child's 500 is the child's own payment, which is not + # the parent's medical expense (IRC 213(a)), so nothing is left. Supplying + # an 800 parent ALD with only 300 parent-paid premiums violates that + # contract; a parent who actually paid all 800 must use the preceding + # input pattern, which gives the same 0. + itemized_medical_expenses: 0 + medical_expense_deduction: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_deductions.yaml index e70f41c30d1..5cc54450575 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_deductions.yaml @@ -64,7 +64,7 @@ id_deductions: 1_000 - name: Claimant medical overlap determines the combined deduction election - # Form 39R worksheet: claimant medical deduction is $750, not $1,250. + # Form 39R worksheet: claimant medical deduction is $750. # Itemized route: $16,750 + $2,250 = $19,000. # Standard route: $15,750 + $3,000 = $18,750. period: 2025 @@ -89,7 +89,9 @@ members: [head, dependent] state_code: ID output: - medical_expense_deduction: 1_250 + # The federal deduction also leaves out the dependent's own 500 + # (IRC 213(a)): 3,000 - 7.5% of 30,000. + medical_expense_deduction: 750 id_health_insurance_premiums_medical_deduction: 750 id_itemizes: true id_deductions: 16_750 diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_itemized_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_itemized_deductions.yaml index e931e17dbdd..a4e5bbfee22 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_itemized_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/id_itemized_deductions.yaml @@ -45,9 +45,11 @@ members: [head, dependent] state_code: ID output: - medical_expense_deduction: 1_250 + # The federal deduction also leaves out the dependent's own 500 + # (IRC 213(a)): 3,000 - 7.5% of 30,000. + medical_expense_deduction: 750 id_health_insurance_premiums_medical_deduction: 750 - itemized_taxable_income_deductions: 21_250 + itemized_taxable_income_deductions: 20_750 id_itemized_deductions: 20_750 id_itemizes: true id_health_insurance_premiums_subtraction: 2_250 @@ -81,9 +83,11 @@ members: [head, dependent] state_code: ID output: - medical_expense_deduction: 40_500 + # The federal deduction also leaves out the dependent's own 500 + # (IRC 213(a)), so it equals the claimant deduction. + medical_expense_deduction: 40_000 id_health_insurance_premiums_medical_deduction: 40_000 - itemized_taxable_income_deductions: 66_689.18919 + itemized_taxable_income_deductions: 66_216.21622 id_itemized_deductions: 66_216.21622 id_itemizes: true id_health_insurance_premiums_subtraction: 60_000 @@ -103,7 +107,7 @@ - name: A supplied final federal itemized total already excludes dependent-paid medical costs # The actual $20,000 Schedule A total includes claimant medical expenses; - # subtracting the derived federal medical overstatement again is incorrect. + # Idaho uses it as given, with no further medical adjustment. period: 2025 input: people: @@ -127,7 +131,9 @@ members: [head, dependent] state_code: ID output: - medical_expense_deduction: 1_250 + # The federal deduction also leaves out the dependent's own 500 + # (IRC 213(a)): 3,000 - 7.5% of 30,000. + medical_expense_deduction: 750 id_health_insurance_premiums_medical_deduction: 750 id_itemized_deductions: 20_000 id_itemizes: true @@ -163,7 +169,9 @@ members: [head, dependent] state_code: ID output: - medical_expense_deduction: 40_500 + # The federal deduction also leaves out the dependent's own 500 + # (IRC 213(a)), so it equals the claimant deduction. + medical_expense_deduction: 40_000 id_health_insurance_premiums_medical_deduction: 40_000 itemized_taxable_income_deductions: 66_216.21622 id_itemized_deductions: 66_216.21622 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..fffa2b999bf 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 @@ -244,7 +244,9 @@ members: [head, dependent] state_code: ID output: - medical_expense_deduction: 1_250 + # The federal deduction also leaves out the dependent's own 500 + # (IRC 213(a)): 3,000 - 7.5% of 30,000. + medical_expense_deduction: 750 id_health_insurance_premiums_medical_deduction: 750 id_itemizes: true id_health_insurance_premiums_subtraction: 2_250 @@ -273,7 +275,9 @@ members: [head, dependent] state_code: ID output: - medical_expense_deduction: 250 + # The federal deduction also leaves out the dependent's own 500 + # (IRC 213(a)): 2,000 is below the 2,250 floor. + medical_expense_deduction: 0 id_health_insurance_premiums_medical_deduction: 0 id_itemizes: true id_health_insurance_premiums_subtraction: 2_000 @@ -309,7 +313,9 @@ members: [head, spouse, dependent] state_code: ID output: - medical_expense_deduction: 5_500 + # Federal: the filers' 5,000 of premiums and every member's 6,000 of + # other costs, less the 6,000 floor; not the dependent's own 500. + medical_expense_deduction: 5_000 id_health_insurance_premiums_medical_deduction: 1_000 id_itemizes: true id_health_insurance_premiums_subtraction: 4_000 @@ -424,7 +430,9 @@ members: [head, dependent] state_code: ID output: - medical_expense_deduction: 1_250 + # The federal deduction also leaves out the dependent's own 500 + # (IRC 213(a)): 3,000 - 7.5% of 30,000. + medical_expense_deduction: 750 id_health_insurance_premiums_medical_deduction: 750 id_health_insurance_premiums_subtraction: 2_250 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml index 2bd044b3c36..738df9c9a1a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_medical_expense_deduction_joint.yaml @@ -710,10 +710,10 @@ tax_units: tax_unit: members: [person1, person2] - # Schedule A line 1: 10,000 of other expenses + the dependent's 1,000; - # the head's 6,000 and 400 of long-term care were taken as the - # self-employed deduction. - itemized_medical_expenses: 11_000 + # Schedule A line 1: 10,000 of other expenses. The head's 6,000 and + # 400 of long-term care were taken as the self-employed deduction, + # and the dependent's own 1,000 is not an expense the head paid. + itemized_medical_expenses: 10_000 itemized_medical_expenses_applied_exclusion: 6_400 mt_agi_joint: 40_000 households: @@ -721,9 +721,11 @@ members: [person1, person2] state_code: MT output: - # Medical premiums left in the subtotal: 7,000 - min(6,000, 6,400) = 1,000. - # Line 1: (11,000 - 1,000) - 3,000 = 7,000. The filer's premiums were - # already deducted, and the dependent's own payments do not enter line 2. + # Medical premiums left in the subtotal: 6,000 - min(6,000, 6,400) = 0; + # the 400 above the head's medical premiums is not taken from the + # dependent's 1,000, which is not in the subtotal. + # Line 1: 10,000 - 3,000 = 7,000. The filer's premiums were already + # deducted, and the dependent's own payments do not enter line 2. mt_premium_deduction_joint: 0 mt_medical_expense_deduction_joint: [7_000, 0] @@ -756,3 +758,63 @@ # Line 1: (10,200 - 0 - 200) - 3,000 = 7,000. # Line 3: 400 - (6,200 - 6,000) = 200. mt_medical_expense_deduction_joint: 7_200 + +- name: Line 1 takes out only the filers' premiums, which are all the subtotal holds + # A dependent's own 1,200 is not in the federal subtotal, so line 1 must not + # take it out. Two tax units with the same people label the earner head and + # then spouse; the deduction is reported on whoever is head. + period: 2022 + absolute_error_margin: 0.01 + input: + people: + a_earner: + age: 45 + is_tax_unit_head: true + other_medical_expenses: 5_000 + a_partner: + age: 45 + is_tax_unit_spouse: true + health_insurance_premiums: 3_000 + a_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + other_medical_expenses: 800 + b_earner: + age: 45 + is_tax_unit_spouse: true + other_medical_expenses: 5_000 + b_partner: + age: 45 + is_tax_unit_head: true + health_insurance_premiums: 3_000 + b_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + other_medical_expenses: 800 + tax_units: + a: + members: [a_earner, a_partner, a_student] + self_employed_health_insurance_ald: 1_000 + mt_agi_joint: 50_000 + b: + members: [b_partner, b_earner, b_student] + self_employed_health_insurance_ald: 1_000 + mt_agi_joint: 50_000 + households: + a: + members: [a_earner, a_partner, a_student] + state_code: MT + b: + members: [b_partner, b_earner, b_student] + state_code: MT + output: + # (3,000 - 1,000) + 5,000 + 800. + itemized_medical_expenses: [7_800, 7_800] + # Line 2: 3,000 - 1,000. + mt_premium_deduction_joint: [2_000, 2_000] + # Line 1: 7,800 - 2,000 = 5,800 - 7.5% x 50,000 = 2,050; plus line 2. + mt_medical_expense_deduction_joint: [4_050, 0, 0, 0, 4_050, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml index f7b07d2ac3c..06025b85e63 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml @@ -225,3 +225,80 @@ output: # Medical costs paid: 10,000 + 6,400 = 16,400. nd_renters_refund_income: 13_600 + +# Expenses a dependent paid. NDCC 57-02-08.1(5)(c) excludes from income +# "medical expenses paid during the year by the applicant or the applicant's +# dependent". The federal subtotal counts only the filers' premiums, so the +# dependent's own payments are added back. Two tax units with the same people +# label the earner head and then spouse. +- name: North Dakota takes out premiums a dependent paid as well as the filers' costs + period: 2025 + input: + people: + a_earner: + age: 45 + is_tax_unit_head: true + employment_income: 30_000 + other_medical_expenses: 500 + a_partner: + age: 45 + is_tax_unit_spouse: true + a_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + b_earner: + age: 45 + is_tax_unit_spouse: true + employment_income: 30_000 + other_medical_expenses: 500 + b_partner: + age: 45 + is_tax_unit_head: true + b_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + tax_units: + a: + members: [a_earner, a_partner, a_student] + b: + members: [b_partner, b_earner, b_student] + households: + a: + members: [a_earner, a_partner, a_student] + state_code: ND + b: + members: [b_partner, b_earner, b_student] + state_code: ND + output: + itemized_medical_expenses: [500, 500] + # 30,000 - (500 + the student's 1,200). + nd_renters_refund_income: [28_300, 28_300] + +- name: North Dakota adds a dependent's premiums to a supplied Schedule A subtotal + period: 2025 + input: + people: + parent: + age: 45 + employment_income: 30_000 + student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + tax_units: + tax_unit: + members: [parent, student] + # Schedule A line 1 covers only what the parent paid. + itemized_medical_expenses: 500 + households: + household: + members: [parent, student] + state_code: ND + output: + # 30,000 - (500 + the student's 1,200). + nd_renters_refund_income: 28_300 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..9e969924c4b 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 @@ -20,7 +20,7 @@ output: nj_medical_expense_deduction: 1_000 -- name: New Jersey preserves dependent premiums when the filer's deduction exceeds reported filer premiums +- name: New Jersey leaves out a dependent's own premiums when the filer's deduction exceeds the filer's premiums period: 2025 input: people: @@ -43,8 +43,11 @@ members: [parent, child] state_code: NJ output: - itemized_medical_expenses: 800 - nj_medical_expense_deduction: 1_800 + # The parent's 300 were deducted as self-employed; the child's own 500 is + # not an expense the parent paid. Other expenses: 100 + 200. + itemized_medical_expenses: 300 + # Separate SE deduction 1,000 + 300. + nj_medical_expense_deduction: 1_300 - name: Partial out of pocket expenses deductable period: 2022 @@ -175,7 +178,7 @@ # Only the 500 of other expenses remains subject to the 800 floor. nj_medical_expense_deduction: 1_000 -- name: New Jersey cannot subtract one dependent's deduction from another dependent's premiums +- name: New Jersey keeps a dependent's separate deduction but not another dependent's own premiums period: 2025 input: people: @@ -200,12 +203,13 @@ state_code: NJ output: dependents_self_employed_health_insurance_ald: 1_000 - itemized_medical_expenses: 1_000 - # C.54A:3-3(c)(3) excludes separately deducted amounts, not another - # person's paid premiums: min(1,000, 0) + min(0, 1,000) = 0 excluded. - # Separate SE deduction 1,000 + general medical premiums 1,000 = 2,000. + # Dependent B's own 1,000 is not an expense the parent paid, so line 1 + # is 0 and nothing remains for dependent A's deduction to consume. + itemized_medical_expenses: 0 + # Separate SE deduction 1,000, which New Jersey counts with dependent A's + # income. # https://pub.njleg.gov/bills/9899/PL99/222_.HTM - nj_medical_expense_deduction: 2_000 + nj_medical_expense_deduction: 1_000 - name: New Jersey combines pretax premiums with separately deducted SE premiums period: 2025 @@ -224,3 +228,58 @@ nj_agi: 40_000 # 1,000 + (5,500 + 3,000 - 0.02 * 40,000) = 8,700. nj_medical_expense_deduction: 8_700 + +- name: New Jersey leaves out a dependent's own premiums, after-tax or through payroll + # Line 31: "unreimbursed medical expenses you paid during the year for + # yourself, your spouse or domestic partner, and any dependents you claim" + # (2025 NJ-1040 instructions, PDF page 25). Two tax units with the same + # people label the earner head and then spouse. + period: 2025 + input: + people: + a_earner: + age: 45 + is_tax_unit_head: true + other_medical_expenses: 3_000 + pre_tax_health_insurance_premiums: 400 + a_partner: + age: 45 + is_tax_unit_spouse: true + a_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + pre_tax_health_insurance_premiums: 500 + b_earner: + age: 45 + is_tax_unit_spouse: true + other_medical_expenses: 3_000 + pre_tax_health_insurance_premiums: 400 + b_partner: + age: 45 + is_tax_unit_head: true + b_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + pre_tax_health_insurance_premiums: 500 + tax_units: + a: + members: [a_earner, a_partner, a_student] + nj_agi: 50_000 + b: + members: [b_partner, b_earner, b_student] + nj_agi: 50_000 + households: + a: + members: [a_earner, a_partner, a_student] + state_code: NJ + b: + members: [b_partner, b_earner, b_student] + state_code: NJ + output: + # 3,000 + the filer's 400 of payroll premiums - 2% of 50,000. Counting + # the student's 1,200 and 500 would give 4,100. + nj_medical_expense_deduction: [2_400, 2_400] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml index be256f04348..14ff81ef538 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_medical_expense_credit.yaml @@ -515,3 +515,95 @@ itemized_medical_expenses_applied_exclusion: 6_400 output: nm_medical_expense_credit: 2_800 + +# Expenses a dependent paid. PIT-RC line 23 requires that "you paid +# unreimbursed and uncompensated medical care expenses of $28,000 or more", +# which "can be for the care of any combination of you, your spouse, or +# dependents" (2025 PIT-RC instructions, PDF page 9). A dependent's own +# premium payments are not the filers'. Two tax units with the same people +# label the senior head and then spouse. +- name: New Mexico's credit does not count a dependent's own premiums toward $28,000 + period: 2025 + input: + people: + a_senior: + age: 70 + is_tax_unit_head: true + other_medical_expenses: 27_500 + a_partner: + age: 60 + is_tax_unit_spouse: true + a_son: + age: 35 + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + b_senior: + age: 70 + is_tax_unit_spouse: true + other_medical_expenses: 27_500 + b_partner: + age: 60 + is_tax_unit_head: true + b_son: + age: 35 + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + tax_units: + a: + members: [a_senior, a_partner, a_son] + b: + members: [b_partner, b_senior, b_son] + households: + a: + members: [a_senior, a_partner, a_son] + state_code: NM + b: + members: [b_partner, b_senior, b_son] + state_code: NM + output: + # The filers paid 27,500; with the son's own 1,200 it would be 28,700. + itemized_medical_expenses: [27_500, 27_500] + nm_medical_expense_credit: [0, 0] + +- name: New Mexico's credit counts a policy the filers bought for a dependent + period: 2025 + input: + people: + a_senior: + age: 70 + is_tax_unit_head: true + other_medical_expenses: 27_500 + a_partner: + age: 60 + is_tax_unit_spouse: true + # The partner pays the son's 1,200 policy. + health_insurance_premiums: 1_200 + a_son: + age: 35 + is_tax_unit_dependent: true + b_senior: + age: 70 + is_tax_unit_spouse: true + other_medical_expenses: 27_500 + b_partner: + age: 60 + is_tax_unit_head: true + health_insurance_premiums: 1_200 + b_son: + age: 35 + is_tax_unit_dependent: true + tax_units: + a: + members: [a_senior, a_partner, a_son] + b: + members: [b_partner, b_senior, b_son] + households: + a: + members: [a_senior, a_partner, a_son] + state_code: NM + b: + members: [b_partner, b_senior, b_son] + state_code: NM + output: + itemized_medical_expenses: [28_700, 28_700] + nm_medical_expense_credit: [2_800, 2_800] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/deductions/nm_medical_care_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/deductions/nm_medical_care_expense_deduction.yaml index d5204ffe5e8..82a260be152 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/deductions/nm_medical_care_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/deductions/nm_medical_care_expense_deduction.yaml @@ -397,3 +397,105 @@ filing_status: HEAD_OF_HOUSEHOLD output: nm_medical_care_expense_deduction: 0 + +# A dependent's own premium payments. PIT-1 line 16 allows "medical care +# expenses paid during the tax year for you, your spouse, or a dependent" +# (2022 PIT-1 instructions, PDF page 31); a dependent's own payments are not +# the filers'. Two tax units with the same people label the earner head and +# then spouse; the results must match. +- name: New Mexico's deduction leaves out a dependent's own premium payments + period: 2024 + input: + people: + a_earner: + age: 45 + is_tax_unit_head: true + employment_income: 20_000 + other_medical_expenses: 1_000 + a_partner: + age: 45 + is_tax_unit_spouse: true + a_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + b_earner: + age: 45 + is_tax_unit_spouse: true + employment_income: 20_000 + other_medical_expenses: 1_000 + b_partner: + age: 45 + is_tax_unit_head: true + b_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + tax_units: + a: + members: [a_earner, a_partner, a_student] + b: + members: [b_partner, b_earner, b_student] + households: + a: + members: [a_earner, a_partner, a_student] + state_code: NM + b: + members: [b_partner, b_earner, b_student] + state_code: NM + output: + filing_status: [JOINT, JOINT] + itemized_medical_expenses: [1_000, 1_000] + # 25% of 1,000 for joint AGI not over 30,000. Counting the student's + # 1,200 of premiums would give 25% of 2,200 = 550. + nm_medical_care_expense_deduction: [250, 250] + +- name: New Mexico's deduction counts a policy the filers bought for a dependent + period: 2024 + input: + people: + a_earner: + age: 45 + is_tax_unit_head: true + employment_income: 20_000 + other_medical_expenses: 1_000 + a_partner: + age: 45 + is_tax_unit_spouse: true + # The partner pays the student's 1,200 policy. + health_insurance_premiums: 1_200 + a_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + b_earner: + age: 45 + is_tax_unit_spouse: true + employment_income: 20_000 + other_medical_expenses: 1_000 + b_partner: + age: 45 + is_tax_unit_head: true + health_insurance_premiums: 1_200 + b_student: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + tax_units: + a: + members: [a_earner, a_partner, a_student] + b: + members: [b_partner, b_earner, b_student] + households: + a: + members: [a_earner, a_partner, a_student] + state_code: NM + b: + members: [b_partner, b_earner, b_student] + state_code: NM + output: + itemized_medical_expenses: [2_200, 2_200] + # 25% of 2,200 for joint AGI not over 30,000. + nm_medical_care_expense_deduction: [550, 550] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.yaml index fbc36a3d88e..9c73b863d72 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.yaml @@ -228,3 +228,91 @@ itemized_medical_expenses_applied_exclusion: 6_400 output: nm_medical_expense_exemption: 3_000 + +# Expenses a dependent paid. PIT-ADJ line 18 applies "if you or your spouse +# are 65 years of age or older, and you paid unreimbursed and uncompensated +# medical care expenses of $28,000 or more" for "any combination of you, your +# spouse, or your dependents" (2025 PIT-ADJ instructions, PDF pages 5-6). +# Two tax units with the same people label the senior head and then spouse. +- name: New Mexico's exemption does not count a dependent's own premiums toward $28,000 + period: 2025 + input: + people: + a_senior: + age: 70 + is_tax_unit_head: true + other_medical_expenses: 27_500 + a_partner: + age: 60 + is_tax_unit_spouse: true + a_son: + age: 35 + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + b_senior: + age: 70 + is_tax_unit_spouse: true + other_medical_expenses: 27_500 + b_partner: + age: 60 + is_tax_unit_head: true + b_son: + age: 35 + is_tax_unit_dependent: true + health_insurance_premiums: 1_200 + tax_units: + a: + members: [a_senior, a_partner, a_son] + b: + members: [b_partner, b_senior, b_son] + households: + a: + members: [a_senior, a_partner, a_son] + state_code: NM + b: + members: [b_partner, b_senior, b_son] + state_code: NM + output: + # The filers paid 27,500; with the son's own 1,200 it would be 28,700. + nm_medical_expense_exemption: [0, 0] + +- name: New Mexico's exemption counts a policy the filers bought for a dependent + period: 2025 + input: + people: + a_senior: + age: 70 + is_tax_unit_head: true + other_medical_expenses: 27_500 + a_partner: + age: 60 + is_tax_unit_spouse: true + health_insurance_premiums: 1_200 + a_son: + age: 35 + is_tax_unit_dependent: true + b_senior: + age: 70 + is_tax_unit_spouse: true + other_medical_expenses: 27_500 + b_partner: + age: 60 + is_tax_unit_head: true + health_insurance_premiums: 1_200 + b_son: + age: 35 + is_tax_unit_dependent: true + tax_units: + a: + members: [a_senior, a_partner, a_son] + b: + members: [b_partner, b_senior, b_son] + households: + a: + members: [a_senior, a_partner, a_son] + state_code: NM + b: + members: [b_partner, b_senior, b_son] + state_code: NM + output: + nm_medical_expense_exemption: [3_000, 3_000] 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..708a241e281 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 filers' premiums (the + federal subtotal leaves out a dependent's own). 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 @@ -448,7 +449,7 @@ def _check(units, year, mode): _unit_sum(run, other), run["itemized_medical_expenses"] + run["itemized_medical_expenses_applied_exclusion"] - - _unit_sum(run, premium) + - _unit_sum(run, filer * premium) - run["itemized_medical_expenses_long_term_care_premiums"], atol=TOLERANCE, ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/itemized_medical_expenses.py index f953f779949..1f045b5c243 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,9 @@ 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", + # CPS ASEC wording of the premium and medical care questions, PDF + # pages 2-3. + "https://www.census.gov/content/dam/Census/library/working-papers/2015/demo/Medical-Out-of-pocket-Expenses-CPSASEC-2013-2014.pdf#page=2", ] documentation = ( "Medical expenses counted before applying the itemized medical " @@ -20,12 +23,18 @@ class itemized_medical_expenses(Variable): "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 " - "filer's self-employed health insurance deduction are excluded under " - "IRC Section 162(l)(3) before the medical expense floor is applied. " + "over-the-counter health expenses. IRC Section 213(a) allows expenses " + "the taxpayer paid for the taxpayer, the spouse and dependents. " "Premium inputs use payer attribution: each person reports premiums " "that person paid, including the full cost of family coverage regardless " "of whom it covers. A parent-paid dependent policy belongs on the parent. " + "So only the head's and spouse's premiums count; a dependent's own " + "premium payments are not the filers' expenses. Other medical expenses " + "are recorded on the person cared for, including amounts other " + "household members paid on that person's behalf, so every member's " + "count. 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. " "The exclusion is bounded to filer-paid premiums; beneficiary-attributed " "inputs with an ALD exceeding that pool are inconsistent with this contract " "and cannot establish conservation of the supplied ALD. A supplied value " @@ -43,12 +52,21 @@ class itemized_medical_expenses(Variable): ) def formula(tax_unit, period, parameters): - premiums = add(tax_unit, period, ["medical_expense_health_insurance_premiums"]) + # IRC 213(a) counts what the head and spouse paid. Premium inputs sit + # on the payer, so take only their premiums, which include coverage + # they bought for dependents; a dependent's own payments are not the + # filers' expenses. + premiums = tax_unit_non_dep_sum( + "medical_expense_health_insurance_premiums", tax_unit, period + ) # The federal ALD covers the head and spouse; dependents take their # own SE deductions on their own returns. Bound the exclusion to the # corresponding filer premiums, leaving other medical costs intact. excluded_premiums = tax_unit( "itemized_medical_expenses_excluded_premiums", period ) + # Other medical expenses sit on the person cared for, including + # amounts household members paid on a dependent's behalf, so count + # every member's. other_expenses = add(tax_unit, period, ["other_medical_expenses"]) return premiums - excluded_premiums + other_expenses diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py index 8bf845b16ac..d14ffbbeec8 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/medical/mt_medical_expense_deduction_joint.py @@ -26,20 +26,20 @@ def formula(person, period, parameters): # expenses allowed under the Internal Revenue Code other than the # insurance premiums deducted in full on lines 2 and 3. Start # from the federal subtotal and take out the premiums still in - # it: the members' medical insurance premiums less the part of - # the self-employed exclusion applied to them, which is bounded by - # the head's and spouse's medical premiums (any excess covered - # long-term care premiums, which are not in the subtotal), and the - # long-term care premiums the subtotal includes. For a computed - # subtotal this leaves the members' other medical expenses. - premiums = add( - tax_unit, period, ["medical_expense_health_insurance_premiums"] - ) + # it: the head's and spouse's medical insurance premiums (the + # subtotal leaves out a dependent's own payments) less the part + # of the self-employed exclusion applied to them, which is + # bounded by those premiums (any excess covered long-term care + # premiums, which are not in the subtotal), and the long-term care + # premiums the subtotal includes. For a computed subtotal this + # leaves the members' other medical expenses. filer_premiums = tax_unit_non_dep_sum( "medical_expense_health_insurance_premiums", tax_unit, period ) excluded = tax_unit("itemized_medical_expenses_applied_exclusion", period) - premiums_in_subtotal = premiums - min_(filer_premiums, max_(0, excluded)) + premiums_in_subtotal = filer_premiums - min_( + filer_premiums, max_(0, excluded) + ) expense = max_( 0, tax_unit("itemized_medical_expenses", period) diff --git a/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py b/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py index 2330827d41a..d67f0c8031e 100644 --- a/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py +++ b/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py @@ -17,11 +17,16 @@ class nd_renters_refund_income(Variable): "Renter refund income after unreimbursed medical costs actually paid. " "The paid-cost base restores the self-employed premiums excluded from " "the federal subtotal: by the federal formula, or as supplied with a " - "caller's subtotal in itemized_medical_expenses_applied_exclusion. A " - "caller-supplied subtotal without that amount is used unchanged, " - "including when components are supplied. Premium inputs use payer " + "caller's subtotal in itemized_medical_expenses_applied_exclusion. For " + "a caller-supplied subtotal without that amount no exclusion is " + "inferred, even when components are supplied. Premium inputs use payer " "attribution: each person reports premiums that person paid, including " - "family coverage regardless of whom it covers." + "family coverage regardless of whom it covers. The federal subtotal " + "counts only the head's and spouse's premiums, while NDCC " + "57-02-08.1(5)(c) also takes out medical expenses paid by the " + "applicant's dependents, so their own premium payments are added " + "back, to a supplied subtotal as well, since a Schedule A amount never " + "includes them." ) def formula(tax_unit, period, parameters): @@ -34,7 +39,7 @@ def formula(tax_unit, period, parameters): excluded_premiums = add(tax_unit, period, ["pre_tax_health_insurance_premiums"]) # Restore premiums excluded from the federal subtotal (computed, or # supplied with a caller's subtotal). - medical_expenses = add( + filer_medical_expenses = add( tax_unit, period, [ @@ -42,6 +47,16 @@ def formula(tax_unit, period, parameters): "itemized_medical_expenses_applied_exclusion", ], ) + # NDCC 57-02-08.1(5)(c) takes out medical expenses "paid during the + # year by the applicant or the applicant's dependent". The federal + # subtotal leaves out the dependents' own premium payments, so add + # them back. + person = tax_unit.members + dependent_premiums = tax_unit.sum( + person("medical_expense_health_insurance_premiums", period) + * person("is_tax_unit_dependent", period) + ) + medical_expenses = filer_medical_expenses + dependent_premiums return max_( add(tax_unit, period, p.income_sources) - excluded_premiums 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..add8adf0f6c 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 @@ -11,12 +11,12 @@ class nj_medical_expense_deduction(Variable): documentation = ( "New Jersey's separate self-employed health insurance deduction plus " "medical expenses above the state floor, excluding premiums already " - "deducted separately. Premium inputs follow payer attribution: each " - "person reports the premiums they paid, including family coverage " - "regardless of who is covered. Each dependent's exclusion is limited " - "to that dependent's own medical premiums. A dependent aggregate ALD " - "override must identify the corresponding person ALDs for exclusion; " - "the total alone does not establish which person paid the premiums." + "deducted separately. Worksheet F counts medical expenses the filers " + "paid for themselves and their dependents. Premium inputs follow payer " + "attribution: each person reports the premiums they paid, including " + "family coverage regardless of who is covered. So the head's and " + "spouse's premiums count, and a dependent's own premium payments, " + "after-tax or through payroll, do not." ) reference = ( "https://pub.njleg.gov/bills/9899/PL99/222_.HTM", @@ -37,23 +37,21 @@ def formula(tax_unit, period, parameters): "dependents_self_employed_health_insurance_ald", ], ) - # 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. - person = tax_unit.members - excluded_dependent_premiums = tax_unit.sum( - person("self_employed_health_insurance_ald_excluded_premiums", period) - * person("is_tax_unit_dependent", period) - ) + # Worksheet F line 1 takes medical expenses the filers paid for + # themselves and their dependents. The federal base counts the + # head's and spouse's premiums net of their self-employed deduction + # (C.54A:3-3 excludes premiums deducted separately) and leaves out a + # dependent's own premium payments, including any the dependent + # deducted as self-employed. # New Jersey includes salary-reduction cafeteria-plan premiums in - # taxable wages (TB-39(R)), so these employee-paid premiums also - # qualify for the medical expense deduction. The separate payroll - # input is disjoint from the after-tax premium inputs. + # taxable wages (TB-39(R)), so the filers' own payroll premiums also + # qualify. The separate payroll input is disjoint from the after-tax + # premium inputs and, like them, sits on the person who paid. + filer_payroll_premiums = tax_unit_non_dep_sum( + "pre_tax_health_insurance_premiums", tax_unit, period + ) medical_expenses = ( - tax_unit("itemized_medical_expenses", period) - - excluded_dependent_premiums - + add(tax_unit, period, ["pre_tax_health_insurance_premiums"]) + tax_unit("itemized_medical_expenses", period) + filer_payroll_premiums ) agi = tax_unit("nj_agi", period) floor = p.rate * agi diff --git a/policyengine_us/variables/household/expense/health/other_medical_expenses.py b/policyengine_us/variables/household/expense/health/other_medical_expenses.py index a882a586b7e..315448359d1 100644 --- a/policyengine_us/variables/household/expense/health/other_medical_expenses.py +++ b/policyengine_us/variables/household/expense/health/other_medical_expenses.py @@ -8,3 +8,14 @@ class other_medical_expenses(Variable): unit = USD definition_period = YEAR uprating = "calibration.gov.hhs.cms.moop_per_capita" + # CPS ASEC medical care question (PMED_VAL), PDF pages 2-3. + reference = "https://www.census.gov/content/dam/Census/library/working-papers/2015/demo/Medical-Out-of-pocket-Expenses-CPSASEC-2013-2014.pdf#page=2" + documentation = ( + "Out-of-pocket costs of this person's own medical care other than " + "insurance premiums and over-the-counter products, such as copays for " + "doctor and dentist visits, diagnostic tests, prescription medicine, " + "glasses and contacts, and medical supplies. Unlike premium inputs, " + "which sit on the person who paid, these sit on the person cared for " + "and include amounts other household members paid on that person's " + "behalf, as the CPS ASEC question asks." + )