From ea9f7696a218fc28faad728e8e5dd6e6cef44891 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 08:26:33 -0400 Subject: [PATCH 1/4] Count only filer-paid premiums in Schedule A medical expenses IRC 213(a) allows medical expenses the taxpayer paid. Premium inputs sit on the person who paid (#10055), so a dependent's own premium payments are not the filers' expenses. The federal subtotal now sums premiums over the head and spouse only and keeps every member's other medical expenses, which the CPS ASEC records on the person cared for. - New Jersey drops its per-dependent exclusion, which the federal base no longer needs, and counts only the filers' payroll premiums. - North Dakota's renter refund adds the dependents' premiums back: NDCC 57-02-08.1(5)(c) counts medical expenses the applicant's dependent paid. - Montana's 2021-2023 line 1 takes out only the filers' premiums. Co-Authored-By: Claude Opus 5.5 --- ...chedule-a-dependent-paid-premiums.fixed.md | 1 + .../itemizing/itemized_medical_expenses.py | 26 ++++++++++-- .../mt_medical_expense_deduction_joint.py | 20 +++++----- .../nd_renters_refund_income.py | 19 ++++++++- .../nj_medical_expense_deduction.py | 40 +++++++++---------- .../expense/health/other_medical_expenses.py | 11 +++++ 6 files changed, 80 insertions(+), 37 deletions(-) create mode 100644 changelog.d/fix-schedule-a-dependent-paid-premiums.fixed.md 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..f2e67b11403 --- /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 medical expense deduction. 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/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..0e14694c04b 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 @@ -21,7 +21,12 @@ class nd_renters_refund_income(Variable): "caller-supplied subtotal without that amount is used unchanged, " "including 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." + ) From b837849e9cfa8570ca0547e6648b0a93cdb902c1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 08:51:50 -0400 Subject: [PATCH 2/4] Test that a dependent's own premiums stay off the filers' medical bases Add YAML cases for the federal subtotal, New Mexico's deduction, credit and exemption, New Jersey, North Dakota and Montana, each with the head and spouse labels both ways, plus Hypothesis properties: the filers' amounts do not depend on a dependent's own premiums, moving premiums from a filer to a dependent never raises them and moving back restores them, labels do not matter, and North Dakota keeps both payers' premiums. Update cases that expected a dependent's own premiums in Schedule A: two federal, nine Idaho federal expectations (Idaho's own outputs are unchanged), two New Jersey, and a Montana supplied subtotal that included the dependent's payment. Co-Authored-By: Claude Opus 5.5 --- ...st_medical_expense_premium_conservation.py | 139 ++++++----- ...schedule_a_payer_attribution_properties.py | 225 ++++++++++++++++++ .../itemizing/itemized_medical_expenses.yaml | 193 +++++++++++++++ .../itemizing/medical_expense_deduction.yaml | 26 +- .../states/id/tax/income/id_deductions.yaml | 6 +- .../id/tax/income/id_itemized_deductions.yaml | 22 +- ...health_insurance_premiums_subtraction.yaml | 16 +- .../mt_medical_expense_deduction_joint.yaml | 76 +++++- .../nd_renters_refund_income.yaml | 77 ++++++ .../nj_medical_expense_deduction.yaml | 77 +++++- .../credits/nm_medical_expense_credit.yaml | 92 +++++++ .../nm_medical_care_expense_deduction.yaml | 54 +++++ .../nm_medical_expense_exemption.yaml | 88 +++++++ 13 files changed, 988 insertions(+), 103 deletions(-) create mode 100644 policyengine_us/tests/core/test_schedule_a_payer_attribution_properties.py 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..0bcf4123292 --- /dev/null +++ b/policyengine_us/tests/core/test_schedule_a_payer_attribution_properties.py @@ -0,0 +1,225 @@ +"""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; +- that return with the premiums moved back; +- 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, and moving them back restores each 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 and every reader at once. +""" + +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, + # and moving them back restores it. + 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..2f962b78fbd 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,57 @@ 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] 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..5426ae32258 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 page 5). 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] From b3c256be8d2947e4821168e4d16f6e6536594d3a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 08:58:18 -0400 Subject: [PATCH 3/4] Update the Idaho and Montana invariants for the filer-paid subtotal Idaho's federal-first provenance test expected the dependent's own premiums in the federal deduction. It now gives the dependent other costs instead, which the federal subtotal counts and Idaho's claimant worksheet does not, so it still separates the two amounts. Montana's line 1 invariant subtracts only the filers' premiums, which are all the federal subtotal holds. Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_id_health_insurance_premiums.py | 8 ++++++-- .../unit/test_mt_medical_premium_deduction_invariants.py | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) 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/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, ) From 2e5fccfd1f2624887be6437149ad94fd4fe6ef17 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 10:43:42 -0400 Subject: [PATCH 4/4] Address review r1: NJ scope, ND wording, NM control and citations - Changelog: a dependent's own premiums leave New Jersey's general medical base (Worksheet F line 1); line 5 still carries a dependent's derived SE deduction. - North Dakota documentation: a supplied subtotal infers no exclusion, but the dependents' premiums are still added. - New Mexico: a parent-paid control for the general deduction (25% of 2,200 = 550), and the PIT-ADJ quote cited at PDF pages 5-6. - Property test docstring: the moved-back row checks determinism, and the file covers the named readers, not every reader. Co-Authored-By: Claude Opus 5.5 --- ...chedule-a-dependent-paid-premiums.fixed.md | 2 +- ...schedule_a_payer_attribution_properties.py | 13 +++-- .../nm_medical_care_expense_deduction.yaml | 48 +++++++++++++++++++ .../nm_medical_expense_exemption.yaml | 4 +- .../nd_renters_refund_income.py | 6 +-- 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/changelog.d/fix-schedule-a-dependent-paid-premiums.fixed.md b/changelog.d/fix-schedule-a-dependent-paid-premiums.fixed.md index f2e67b11403..fa21e118f6d 100644 --- a/changelog.d/fix-schedule-a-dependent-paid-premiums.fixed.md +++ b/changelog.d/fix-schedule-a-dependent-paid-premiums.fixed.md @@ -1 +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 medical expense deduction. 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. +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_schedule_a_payer_attribution_properties.py b/policyengine_us/tests/core/test_schedule_a_payer_attribution_properties.py index 0bcf4123292..4f3bd259e64 100644 --- a/policyengine_us/tests/core/test_schedule_a_payer_attribution_properties.py +++ b/policyengine_us/tests/core/test_schedule_a_payer_attribution_properties.py @@ -14,7 +14,7 @@ - the drawn return; - the same return with part of one filer's premiums moved to a dependent; -- that return with the premiums moved back; +- 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. @@ -29,7 +29,9 @@ 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, and moving them back restores each to the cent. + 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 @@ -38,7 +40,8 @@ income less every member's premiums and other medical expenses. YAML covers the legal examples; these properties check the payer rule across -generated returns and every reader at once. +generated returns for the readers named above, not every reader of the +subtotal. """ import numpy as np @@ -183,8 +186,8 @@ def _assert_filer_amounts(cases, year): np.testing.assert_allclose( values[(*key, "no_dependent_premiums")], drawn, atol=CENT ) - # 3. Moving premiums to a dependent never raises the amount, - # and moving them back restores it. + # 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 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 2f962b78fbd..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 @@ -451,3 +451,51 @@ # 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 5426ae32258..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 @@ -232,8 +232,8 @@ # 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 page 5). Two tax -# units with the same people label the senior head and then spouse. +# 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: 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 0e14694c04b..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,9 +17,9 @@ 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. The federal subtotal " "counts only the head's and spouse's premiums, while NDCC "