diff --git a/changelog.d/nm-ctc-152c-medical-exemption.fixed.md b/changelog.d/nm-ctc-152c-medical-exemption.fixed.md new file mode 100644 index 00000000000..5efe5966da7 --- /dev/null +++ b/changelog.d/nm-ctc-152c-medical-exemption.fixed.md @@ -0,0 +1 @@ +Count New Mexico child income tax credit children under IRC 152(c), so a child with an ITIN qualifies, and stop halving New Mexico's 65+ medical care expense exemption for married filing separately. diff --git a/policyengine_us/tests/core/test_nm_ctc_children_medical_exemption_invariants.py b/policyengine_us/tests/core/test_nm_ctc_children_medical_exemption_invariants.py new file mode 100644 index 00000000000..96154868af8 --- /dev/null +++ b/policyengine_us/tests/core/test_nm_ctc_children_medical_exemption_invariants.py @@ -0,0 +1,377 @@ +"""New Mexico child income tax credit children and the 65+ medical exemption. + +NMSA 7-2-18.34(J)(2) defines the child income tax credit's "qualifying child" +by IRC 152(c), which has no taxpayer identification number requirement, so +`nm_ctc_qualifying_children` counts 152(c) children whatever their Social +Security number status. NMSA 7-2-5.9 gives $3,000 to a filer or spouse 65 or +older with at least $28,000 of medical care expenses (the PIT-ADJ reading of +the threshold), with no halving for married filing separately. + +For households drawn by Hypothesis and a seeded population, in 2023-2026: + +1. Reference count: `nm_ctc_qualifying_children` equals an independent count + of the 152(c) tests the model records: a dependent who is the filer's + child (not a parent or grandparent) and is either permanently and totally + disabled, or younger than the older filer and under 19 or a full-time + student under 24. Outside New Mexico it is 0. +2. Superset: where every dependent is younger than the older filer, it is + never below the federal `eitc_child_count`, which adds the Social Security + number requirement. (The federal helper does not apply the relative-age + test of IRC 152(c)(3)(A), so the two counts are not ordered otherwise.) +3. Identification invariance: giving every dependent a Social Security + number changes neither the count nor `nm_ctc`. +4. Label invariance: exchanging the head and spouse labels changes neither + the count, `nm_ctc` nor `nm_medical_expense_exemption`. +5. Separate filing: a single adult filing separately gets half of `nm_ctc` + (7-2-18.34(G)) and the whole `nm_medical_expense_exemption`. +6. Reference exemption: `nm_medical_expense_exemption` is $3,000 exactly when + a filer (not a dependent) is 65 or older and medical expenses are at least + $28,000, and 0 otherwise. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEARS = [2023, 2024, 2025, 2026] +STATES = ["NM", "NM", "NM", "TX"] +SSN_CARD_TYPES = ["CITIZEN", "NON_CITIZEN_VALID_EAD", "OTHER_NON_CITIZEN", "NONE"] +RELATIONS = ["child", "child", "child", "parent", "grandparent"] +EXEMPTION = 3_000 +MIN_EXPENSES = 28_000 +TOLERANCE = 0.01 +# Each drawn unit is computed four times: as drawn; with every dependent +# given a Social Security number; with the head and spouse labels exchanged; +# and filing separately (a single adult only). +DRAWN, CITIZEN, SWAPPED, SEPARATE = range(4) + + +def money(high): + return st.one_of(st.just(0), st.integers(1, high)) + + +@st.composite +def adults(draw): + return { + "age": draw(st.integers(18, 90)), + "employment_income": draw(money(120_000)), + "claimed_as_dependent_on_another_return": draw(st.booleans()), + } + + +@st.composite +def dependents(draw): + relation = draw(st.sampled_from(RELATIONS)) + if relation == "child": + age = draw(st.integers(0, 30)) + else: + age = draw(st.integers(40, 95)) + return { + "relation": relation, + "age": age, + "is_full_time_student": draw(st.booleans()) if 18 <= age <= 25 else False, + "is_permanently_and_totally_disabled": draw( + st.booleans() if relation == "child" else st.just(False) + ), + "ssn_card_type": draw(st.sampled_from(SSN_CARD_TYPES)), + } + + +@st.composite +def units(draw): + return { + "state": draw(st.sampled_from(STATES)), + "adults": draw(st.lists(adults(), min_size=1, max_size=2)), + "dependents": draw(st.lists(dependents(), max_size=3)), + "medical_expenses": draw( + st.one_of( + st.sampled_from([27_999, 28_000, 28_001]), + money(60_000), + ) + ), + } + + +def _seeded_units(n, seed=20261009): + rng = np.random.default_rng(seed) + out = [] + for i in range(n): + drawn_adults = [] + for _ in range(int(rng.integers(1, 3))): + drawn_adults.append( + { + "age": int(rng.integers(18, 91)), + "employment_income": float(round(rng.uniform(0, 120_000))), + "claimed_as_dependent_on_another_return": bool(rng.random() < 0.2), + } + ) + drawn_dependents = [] + for _ in range(int(rng.integers(0, 4))): + relation = RELATIONS[int(rng.integers(len(RELATIONS)))] + age = int( + rng.integers(0, 31) if relation == "child" else rng.integers(40, 96) + ) + drawn_dependents.append( + { + "relation": relation, + "age": age, + "is_full_time_student": bool( + 18 <= age <= 25 and rng.random() < 0.5 + ), + "is_permanently_and_totally_disabled": bool( + relation == "child" and rng.random() < 0.1 + ), + "ssn_card_type": SSN_CARD_TYPES[ + int(rng.integers(len(SSN_CARD_TYPES))) + ], + } + ) + out.append( + { + "state": STATES[i % len(STATES)], + "adults": drawn_adults, + "dependents": drawn_dependents, + "medical_expenses": float( + [27_999, 28_000, 28_001][i % 3] + if i % 4 == 0 + else round(rng.uniform(0, 60_000)) + ), + } + ) + return out + + +def _crafted_units(): + """Edge cases the seeded draw rarely reaches.""" + + def adult(age, income=20_000.0): + return { + "age": age, + "employment_income": income, + "claimed_as_dependent_on_another_return": False, + } + + def dependent(age, student=False, disabled=False, ssn="OTHER_NON_CITIZEN"): + return { + "relation": "child", + "age": age, + "is_full_time_student": student, + "is_permanently_and_totally_disabled": disabled, + "ssn_card_type": ssn, + } + + def unit(adults, dependents, expenses=0.0): + return { + "state": "NM", + "adults": adults, + "dependents": dependents, + "medical_expenses": expenses, + } + + elderly_parent = {**dependent(70, ssn="CITIZEN"), "relation": "parent"} + return [ + # A student sibling older than, or as old as, the filer. + unit([adult(20)], [dependent(22, student=True)]), + unit([adult(20)], [dependent(20, student=True)]), + # A disabled sibling older than the filer. + unit([adult(20)], [dependent(30, disabled=True)]), + # Younger than the older spouse only. + unit([adult(25), adult(20)], [dependent(22, student=True)]), + # The only 65+ person is a dependent parent. + unit([adult(40)], [elderly_parent], expenses=30_000.0), + # A 65+ filer with exactly $28,000 of expenses. + unit([adult(70)], [], expenses=28_000.0), + ] + + +def _situation(drawn_units, year): + people, tax_units, households = {}, {}, {} + for i, unit in enumerate(drawn_units): + for copy in (DRAWN, CITIZEN, SWAPPED, SEPARATE): + members = [] + adult_list = list(unit["adults"]) + if copy == SWAPPED: + adult_list = adult_list[::-1] + for j, adult in enumerate(adult_list): + name = f"adult_{i}_{copy}_{j}" + people[name] = { + "age": adult["age"], + "employment_income": adult["employment_income"], + "claimed_as_dependent_on_another_return": adult[ + "claimed_as_dependent_on_another_return" + ], + "is_tax_unit_head": j == 0, + "is_tax_unit_spouse": j == 1, + "is_tax_unit_dependent": False, + "medicare_enrolled": False, + } + members.append(name) + people[members[0]]["other_medical_expenses"] = unit["medical_expenses"] + for j, dependent in enumerate(unit["dependents"]): + name = f"dependent_{i}_{copy}_{j}" + people[name] = { + "age": dependent["age"], + "is_full_time_student": dependent["is_full_time_student"], + "is_permanently_and_totally_disabled": dependent[ + "is_permanently_and_totally_disabled" + ], + "is_parent_of_filer_or_spouse": dependent["relation"] == "parent", + "is_grandparent_of_filer_or_spouse": dependent["relation"] + == "grandparent", + "ssn_card_type": ( + "CITIZEN" if copy == CITIZEN else dependent["ssn_card_type"] + ), + "is_tax_unit_head": False, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": True, + "medicare_enrolled": False, + } + members.append(name) + tax_unit = {"members": members} + if len(unit["adults"]) == 2: + tax_unit["filing_status"] = {year: "JOINT"} + elif copy == SEPARATE: + tax_unit["filing_status"] = {year: "SEPARATE"} + tax_units[f"tax_unit_{i}_{copy}"] = tax_unit + households[f"household_{i}_{copy}"] = { + "members": members, + "state_code": {year: unit["state"]}, + } + people = { + name: {k: {year: v} for k, v in values.items()} + for name, values in people.items() + } + return {"people": people, "tax_units": tax_units, "households": households} + + +def _reference_children(unit): + if unit["state"] != "NM": + return 0 + oldest_filer = max(a["age"] for a in unit["adults"]) + return sum( + d["relation"] == "child" + and ( + d["is_permanently_and_totally_disabled"] + or ( + d["age"] < oldest_filer + and (d["age"] < 19 or (d["is_full_time_student"] and d["age"] < 24)) + ) + ) + for d in unit["dependents"] + ) + + +def _reference_exemption(unit): + if unit["state"] != "NM": + return 0 + filer_65 = any(a["age"] >= 65 for a in unit["adults"]) + return EXEMPTION * (filer_65 and unit["medical_expenses"] >= MIN_EXPENSES) + + +def _describe(unit): + return ( + f"{unit['state']} adults={[(a['age'], a['claimed_as_dependent_on_another_return']) for a in unit['adults']]} " + f"dependents={[(d['relation'], d['age'], d['ssn_card_type']) for d in unit['dependents']]} " + f"expenses={unit['medical_expenses']}" + ) + + +def check_invariants(drawn_units, year): + sim = Simulation(situation=_situation(drawn_units, year)) + + def calc(name): + return np.asarray(sim.calculate(name, year), dtype=float).reshape(-1, 4) + + children = calc("nm_ctc_qualifying_children") + eitc_children = calc("eitc_child_count") + credit = calc("nm_ctc") + exemption = calc("nm_medical_expense_exemption") + for i, unit in enumerate(drawn_units): + where = f"{year} unit {i}: {_describe(unit)}" + single = len(unit["adults"]) == 1 + # 1. Reference count. + assert children[i, DRAWN] == _reference_children(unit), where + # 2. Superset of the EITC count where every dependent is younger than + # the older filer (the New Mexico count is 0 elsewhere). + oldest_filer = max(a["age"] for a in unit["adults"]) + if unit["state"] == "NM" and all( + d["age"] < oldest_filer for d in unit["dependents"] + ): + assert children[i, DRAWN] >= eitc_children[i, DRAWN], where + # 3. Identification invariance. + assert children[i, CITIZEN] == children[i, DRAWN], where + assert abs(credit[i, CITIZEN] - credit[i, DRAWN]) <= TOLERANCE, where + # 4. Label invariance. + assert children[i, SWAPPED] == children[i, DRAWN], where + assert abs(credit[i, SWAPPED] - credit[i, DRAWN]) <= TOLERANCE, where + assert abs(exemption[i, SWAPPED] - exemption[i, DRAWN]) <= TOLERANCE, where + # 5. Separate filing. + if single: + assert abs(credit[i, SEPARATE] - credit[i, DRAWN] / 2) <= TOLERANCE, where + assert abs(exemption[i, SEPARATE] - exemption[i, DRAWN]) <= TOLERANCE, where + # 6. Reference exemption. + assert abs(exemption[i, DRAWN] - _reference_exemption(unit)) <= TOLERANCE, where + return credit[:, DRAWN], exemption[:, DRAWN] + + +def _assert_coverage(drawn_units, credit, exemption): + """The seeded population reaches the cases the invariants are about.""" + single = np.array([len(u["adults"]) == 1 for u in drawn_units]) + child_without_ssn = np.array( + [ + any( + d["relation"] == "child" + and d["age"] < 19 + and d["ssn_card_type"] in ("OTHER_NON_CITIZEN", "NONE") + for d in u["dependents"] + ) + for u in drawn_units + ] + ) + elderly_dependent_only = np.array( + [ + u["state"] == "NM" + and all(a["age"] < 65 for a in u["adults"]) + and any(d["age"] >= 65 for d in u["dependents"]) + and u["medical_expenses"] >= MIN_EXPENSES + for u in drawn_units + ] + ) + not_younger_than_filers = np.array( + [ + any( + d["relation"] == "child" + and not d["is_permanently_and_totally_disabled"] + and d["age"] >= max(a["age"] for a in u["adults"]) + and (d["age"] < 19 or (d["is_full_time_student"] and d["age"] < 24)) + for d in u["dependents"] + ) + for u in drawn_units + ] + ) + assert (child_without_ssn & (credit > 0)).any() + assert not_younger_than_filers.any() + assert (single & (credit > 0)).any() + assert (single & (exemption > 0)).any() + assert elderly_dependent_only.any() + + +@settings( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.sampled_from(YEARS), st.lists(units(), min_size=8, max_size=20)) +def test_nm_ctc_children_and_medical_exemption_hypothesis(year, drawn_units): + check_invariants(drawn_units, year) + + +@pytest.mark.parametrize("year", YEARS) +def test_nm_ctc_children_and_medical_exemption_seeded_population(year): + drawn_units = _seeded_units(40) + _crafted_units() + credit, exemption = check_invariants(drawn_units, year) + _assert_coverage(drawn_units, credit, exemption) diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_ctc.yaml index e0145ad405e..0b02490d17b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_ctc.yaml @@ -2,7 +2,7 @@ period: 2023 input: state_code: NM - eitc_child_count: 0 + nm_ctc_qualifying_children: 0 adjusted_gross_income: 10_000 filing_status: JOINT output: @@ -12,7 +12,7 @@ period: 2023 input: state_code: NM - eitc_child_count: 2 + nm_ctc_qualifying_children: 2 adjusted_gross_income: 50_000 filing_status: JOINT output: @@ -22,7 +22,7 @@ period: 2023 input: state_code: NM - eitc_child_count: 2 + nm_ctc_qualifying_children: 2 adjusted_gross_income: 50_000 filing_status: SEPARATE output: @@ -32,7 +32,7 @@ period: 2023 input: state_code: NM - eitc_child_count: 3 + nm_ctc_qualifying_children: 3 adjusted_gross_income: 350_001 filing_status: SINGLE output: @@ -141,3 +141,74 @@ state_code: NM output: nm_ctc: 0 + +- name: 2025 independent filer with an otherwise qualifying child who has an ITIN gets the child income tax credit + period: 2025 + input: + people: + parent: + age: 30 + employment_income: 20_000 + child: + age: 5 + # An ITIN holder has no valid Social Security number. + ssn_card_type: OTHER_NON_CITIZEN + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NM + output: + # The federal EITC needs a Social Security number (IRC 32(c)(3)(D)). + eitc_child_count: 0 + # NMSA 7-2-18.34(J)(2) adopts the IRC 152(c) definition, which has no + # identification number requirement. + nm_ctc_qualifying_children: 1 + # AGI $20,000 is in the first 2025 row (PIT-RC Table 4): $637 per child. + nm_ctc: 637 + +- name: 2025 independent filer with a qualifying child who has a Social Security number gets the same credit + period: 2025 + input: + people: + parent: + age: 30 + employment_income: 20_000 + child: + age: 5 + ssn_card_type: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NM + output: + eitc_child_count: 1 + nm_ctc_qualifying_children: 1 + nm_ctc: 637 + +- name: 2025 married couple filing separately with an ITIN child gets half the credit + period: 2025 + input: + people: + parent: + age: 30 + employment_income: 20_000 + child: + age: 5 + ssn_card_type: OTHER_NON_CITIZEN + tax_units: + tax_unit: + members: [parent, child] + filing_status: SEPARATE + households: + household: + members: [parent, child] + state_code: NM + output: + # NMSA 7-2-18.34(G): each may claim "only one-half"; 637 / 2. + nm_ctc: 318.5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_ctc_qualifying_children.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_ctc_qualifying_children.yaml new file mode 100644 index 00000000000..88f708e73a8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/nm_ctc_qualifying_children.yaml @@ -0,0 +1,308 @@ +- name: 2025 a child with an ITIN counts although the federal EITC does not count them + period: 2025 + input: + people: + parent: + age: 30 + child: + age: 5 + ssn_card_type: OTHER_NON_CITIZEN + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NM + output: + eitc_child_count: 0 + # NMSA 7-2-18.34(J)(2): "qualifying child" as defined by IRC 152(c). + nm_ctc_qualifying_children: 1 + +- name: 2025 a child without a Social Security number card counts + period: 2025 + input: + people: + parent: + age: 30 + child: + age: 5 + ssn_card_type: NONE + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NM + output: + eitc_child_count: 0 + # IRC 152(c) has no identification number requirement. + nm_ctc_qualifying_children: 1 + +- name: 2025 age test of IRC 152(c)(3) - under 19, a full-time student under 24, or permanently and totally disabled + period: 2025 + input: + people: + parent: + age: 50 + child_18: + age: 18 + is_tax_unit_dependent: true + child_19: + age: 19 + is_tax_unit_dependent: true + student_23: + age: 23 + is_full_time_student: true + is_tax_unit_dependent: true + student_24: + age: 24 + is_full_time_student: true + is_tax_unit_dependent: true + disabled_30: + age: 30 + is_permanently_and_totally_disabled: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent, child_18, child_19, student_23, student_24, disabled_30] + households: + household: + members: [parent, child_18, child_19, student_23, student_24, disabled_30] + state_code: NM + output: + # The 18-year-old, the 23-year-old student and the disabled 30-year-old. + nm_ctc_qualifying_children: 3 + +- name: 2025 a dependent parent is not a qualifying child (IRC 152(c)(2) relationship test) + period: 2025 + input: + people: + filer: + age: 40 + is_tax_unit_dependent: false + grandmother: + age: 70 + is_tax_unit_dependent: true + is_parent_of_filer_or_spouse: true + child: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [filer, grandmother, child] + households: + household: + members: [filer, grandmother, child] + state_code: NM + output: + nm_ctc_qualifying_children: 1 + +- name: 2025 children still count on a joint return with one claimable spouse (the claimable spouse is the head) + period: 2025 + input: + people: + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: true + is_tax_unit_spouse: false + person_b: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 2 + ssn_card_type: OTHER_NON_CITIZEN + tax_units: + tax_unit: + members: [person_a, person_b, child] + filing_status: JOINT + households: + household: + members: [person_a, person_b, child] + state_code: NM + output: + # 7-2-18.34 counts IRC 152(c) qualifying children, not dependents, so + # the IRC 152(b)(1) rule that a claimable filer has no dependents does + # not remove the child. + nm_ctc_qualifying_children: 1 + +- name: 2025 children still count on a joint return with one claimable spouse (the claimable spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + person_a: + age: 20 + claimed_as_dependent_on_another_return: true + is_tax_unit_head: false + is_tax_unit_spouse: true + child: + age: 2 + ssn_card_type: OTHER_NON_CITIZEN + tax_units: + tax_unit: + members: [person_b, person_a, child] + filing_status: JOINT + households: + household: + members: [person_b, person_a, child] + state_code: NM + output: + nm_ctc_qualifying_children: 1 + +- name: 2025 no New Mexico count outside New Mexico + period: 2025 + input: + people: + parent: + age: 30 + child: + age: 5 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + nm_ctc_qualifying_children: 0 + +- name: 2025 a student sibling older than the filer is not a qualifying child (IRC 152(c)(3)(A)) + period: 2025 + input: + people: + filer: + age: 20 + employment_income: 20_000 + is_tax_unit_dependent: false + sibling: + age: 22 + is_full_time_student: true + ssn_card_type: OTHER_NON_CITIZEN + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [filer, sibling] + households: + household: + members: [filer, sibling] + state_code: NM + output: + # The child must be "younger than the taxpayer claiming such individual + # as a qualifying child". + nm_ctc_qualifying_children: 0 + nm_ctc: 0 + +- name: 2025 a student sibling the same age as the filer is not a qualifying child + period: 2025 + input: + people: + filer: + age: 20 + is_tax_unit_dependent: false + sibling: + age: 20 + is_full_time_student: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [filer, sibling] + households: + household: + members: [filer, sibling] + state_code: NM + output: + nm_ctc_qualifying_children: 0 + +- name: 2025 a permanently and totally disabled sibling older than the filer is a qualifying child (IRC 152(c)(3)(B)) + period: 2025 + input: + people: + filer: + age: 20 + is_tax_unit_dependent: false + sibling: + age: 30 + is_permanently_and_totally_disabled: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [filer, sibling] + households: + household: + members: [filer, sibling] + state_code: NM + output: + # 152(c)(3)(B): "the requirements of subparagraph (A) shall be treated as + # met". + nm_ctc_qualifying_children: 1 + +- name: 2025 on a joint return a child younger than either spouse qualifies (the older spouse is the head) + period: 2025 + input: + people: + person_a: + age: 25 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + person_b: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + sibling: + age: 22 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person_a, person_b, sibling] + filing_status: JOINT + households: + household: + members: [person_a, person_b, sibling] + state_code: NM + output: + # Publication 596: "younger than you (or your spouse if filing jointly)". + nm_ctc_qualifying_children: 1 + +- name: 2025 on a joint return a child younger than either spouse qualifies (the older spouse is the spouse) + period: 2025 + input: + people: + person_b: + age: 20 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + person_a: + age: 25 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + sibling: + age: 22 + is_full_time_student: true + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person_b, person_a, sibling] + filing_status: JOINT + households: + household: + members: [person_b, person_a, sibling] + state_code: NM + output: + nm_ctc_qualifying_children: 1 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 0f3d7ef4e76..ca13295aa32 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 @@ -63,7 +63,7 @@ output: nm_medical_expense_exemption: 3_000 -- name: Eligible - halved for separate filers +- name: Eligible - not halved for separate filers period: 2023 input: state_code: NM @@ -71,7 +71,45 @@ other_medical_expenses: 28_000 filing_status: SEPARATE output: - nm_medical_expense_exemption: 1_500 + # Neither NMSA 7-2-5.9 nor the PIT-ADJ line 18 instructions halve the + # exemption for married filing separately. + nm_medical_expense_exemption: 3_000 + +- name: 2025 married filing separately gets the full exemption + period: 2025 + input: + state_code: NM + age: 70 + medicare_enrolled: false + other_medical_expenses: 30_000 + filing_status: SEPARATE + output: + # PIT-ADJ line 18: "enter $3,000 on line 18 to claim the exemption". + nm_medical_expense_exemption: 3_000 + +- name: 2025 expenses of exactly $28,000 qualify under the PIT-ADJ instructions + period: 2025 + input: + state_code: NM + age: 70 + medicare_enrolled: false + other_medical_expenses: 28_000 + filing_status: SINGLE + output: + # PIT-ADJ line 18: expenses "of $28,000 or more". NMSA 7-2-5.9(A) says + # expenses that "exceed" $28,000, which would give 0 here. + nm_medical_expense_exemption: 3_000 + +- name: 2025 expenses one dollar under $28,000 do not qualify + period: 2025 + input: + state_code: NM + age: 70 + medicare_enrolled: false + other_medical_expenses: 27_999 + filing_status: SINGLE + output: + nm_medical_expense_exemption: 0 - name: Output always $3,000 period: 2023 diff --git a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_ctc.py b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_ctc.py index df105c86537..672fc48c5ac 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_ctc.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_ctc.py @@ -17,10 +17,9 @@ def formula(tax_unit, period, parameters): # Legal code refers to "adjusted gross income", # Tax form does not specify if federal or state AGI agi = tax_unit("adjusted_gross_income", period) - # The law 7-2-18.34(J)(2) defines qualifying children as those from IRC 152(c). - # IRC 152(c) refers to the EITC qualifying children. - # https://www.law.cornell.edu/uscode/text/26/152#c - children = tax_unit("eitc_child_count", period) + # NMSA 7-2-18.34(J)(2) defines qualifying children by IRC 152(c), + # without the Social Security number requirement of the federal EITC. + children = tax_unit("nm_ctc_qualifying_children", period) p = parameters(period).gov.states.nm.tax.income.credits.ctc amount_per_child = p.amount.calc(agi, right=True) amount = amount_per_child * children diff --git a/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_ctc_qualifying_children.py b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_ctc_qualifying_children.py new file mode 100644 index 00000000000..86d3b8ff90e --- /dev/null +++ b/policyengine_us/variables/gov/states/nm/tax/income/credits/nm_ctc_qualifying_children.py @@ -0,0 +1,56 @@ +from policyengine_us.model_api import * + + +class nm_ctc_qualifying_children(Variable): + value_type = int + entity = TaxUnit + label = "New Mexico child income tax credit qualifying children" + documentation = """ + Number of qualifying children for the New Mexico child income tax credit. + NMSA 7-2-18.34(J)(2) adopts the IRC 152(c) qualifying-child definition, + which has no taxpayer identification number requirement, so a child with + an ITIN (or no Social Security number) counts here although the federal + EITC (IRC 32(c)(3)(D)) does not count them. A child who is not disabled + must also be younger than the filer, or than either spouse on a joint + return (IRC 152(c)(3)(A)). + """ + definition_period = YEAR + reference = ( + # NMSA 7-2-18.34(J)(2) defines "qualifying child" by IRC 152(c). + "https://nmonesource.com/nmos/nmsa/en/item/4340/index.do#!fragment/zoupio-_Toc140503818/BQCwhgziBcwMYgK4DsDWszIQewE4BUBTADwBdoAvbRABwEtsBaAfX2zgEYAWABgFYeAZgAcHYQEoANMmylCEAIqJCuAJ7QA5BskRCYXAiUr1WnXoMgAynlIAhdQCUAogBknANQCCAOQDCTyVIwACNoUnZxcSA", + "https://www.law.cornell.edu/uscode/text/26/152#c", + "https://realfile.tax.newmexico.gov/2025pit-rc-ins.pdf#page=10", + # Publication 596, qualifying child age test: "younger than you (or + # your spouse if filing jointly)". PDF pages 10, 12. + "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=10", + ) + defined_for = StateCode.NM + + def formula(tax_unit, period, parameters): + person = tax_unit.members + # NMSA 7-2-18.34(J)(2): "qualifying child" means "qualifying child" as + # defined by IRC 152(c), and also a minor child or stepchild who would + # qualify if public assistance toward their support were counted as + # the taxpayer's. is_eitc_qualifying_child applies the 152(c) tests + # the model records (relationship, and age or permanent and total + # disability) and leaves out the EITC identification requirement of + # IRC 32(c)(3)(D), which 152(c) does not contain. The model does not + # record the 152(c)(1)(D) own-support test, so every dependent who + # passes the other tests is treated as meeting it. + # A filer who can be claimed elsewhere is treated as having no + # dependents (IRC 152(b)(1)), but 7-2-18.34 counts 152(c) qualifying + # children rather than dependents, so the count does not drop to zero + # here; nm_ctc applies the claimant test of 7-2-18.34(A). + qualifying_child = person("is_eitc_qualifying_child", period) + # IRC 152(c)(3)(A) also requires the child to be "younger than the + # taxpayer claiming such individual as a qualifying child"; on a joint + # return, younger than either spouse (Publication 596). 152(c)(3)(B) + # treats the requirement as met for a permanently and totally + # disabled child. + age = person("age", period) + filer = person("is_tax_unit_head_or_spouse", period) + oldest_filer_age = tax_unit.max(where(filer, age, 0)) + younger_than_a_filer = age < tax_unit.project(oldest_filer_age) + disabled = person("is_permanently_and_totally_disabled", period) + meets_relative_age_test = younger_than_a_filer | disabled + return tax_unit.sum(qualifying_child & meets_relative_age_test) diff --git a/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.py b/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.py index b88e65879a3..81b4829aee8 100644 --- a/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.py +++ b/policyengine_us/variables/gov/states/nm/tax/income/exemptions/nm_medical_expense_exemption.py @@ -46,11 +46,14 @@ def formula(tax_unit, period, parameters): # dependent aged 65 or older does not qualify the return. filer = person("is_tax_unit_head_or_spouse", period) age_eligible = tax_unit.any(filer & (age >= p.age_eligibility)) + # NMSA 7-2-5.9(A) requires expenses that "exceed twenty-eight thousand + # dollars ($28,000)"; PIT-ADJ line 18 instructions allow expenses "of + # $28,000 or more". This keeps the instructions' reading, under which + # expenses of exactly $28,000 qualify, until the conflict is resolved. expense_eligible = medical_expense >= p.min_expenses eligible = age_eligible & expense_eligible - # Exemption is halved for married filing separately - filing_status = tax_unit("filing_status", period) - separate = filing_status == filing_status.possible_values.SEPARATE - denominator = where(separate, 2, 1) - numerator = eligible * p.amount - return numerator / denominator + # Neither 7-2-5.9 nor the PIT-ADJ line 18 instructions halve the + # exemption for married individuals filing separately. Sections that + # do, such as the 65+ medical care credit (7-2-18.13(B)) and the organ + # donation deduction (7-2-36(B), the next PIT-ADJ line), say so. + return eligible * p.amount