From 0d1da0602298a2a6ee770ec69ed1e5bde0418489 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 8 Nov 2026 15:05:41 -0500 Subject: [PATCH 1/6] Count SSI and TANF in North Dakota renter's refund income N.D.C.C. 57-02-08.1(5)(c) counts "any county, state, or federal public assistance benefits" and excludes only amounts "excluded from income by federal or state law". No federal or North Dakota law excludes SSI or TANF: the IRS leaves them out of gross income under its general welfare exclusion, which no statute or regulation enacts. Federal law does exclude SNAP (7 U.S.C. 2017(b)) and LIHEAP (42 U.S.C. 8624(f)(1)) for any purpose under state law, so those stay out. SFN 24777 line 7 already asks for S.S.I. The test cases take their expected values from the statute. The medical premium conservation test now gives its households no public assistance, so its North Dakota expectation still tests the medical base alone. Co-Authored-By: Claude Opus 5.5 --- ...-renters-refund-public-assistance.fixed.md | 1 + .../renters_refund/income_sources.yaml | 8 +++ ...st_medical_expense_premium_conservation.py | 8 ++- .../renters_refund/nd_renters_refund.yaml | 26 +++++++ .../nd_renters_refund_income.yaml | 67 +++++++++++++++++-- 5 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 changelog.d/nd-renters-refund-public-assistance.fixed.md diff --git a/changelog.d/nd-renters-refund-public-assistance.fixed.md b/changelog.d/nd-renters-refund-public-assistance.fixed.md new file mode 100644 index 00000000000..64ff7d00477 --- /dev/null +++ b/changelog.d/nd-renters-refund-public-assistance.fixed.md @@ -0,0 +1 @@ +North Dakota renter's refund income counts Supplemental Security Income and TANF, which N.D.C.C. 57-02-08.1(5)(c) includes as public assistance benefits. diff --git a/policyengine_us/parameters/gov/states/nd/tax/property/renters_refund/income_sources.yaml b/policyengine_us/parameters/gov/states/nd/tax/property/renters_refund/income_sources.yaml index 31642f83af3..40dcf8dc35d 100644 --- a/policyengine_us/parameters/gov/states/nd/tax/property/renters_refund/income_sources.yaml +++ b/policyengine_us/parameters/gov/states/nd/tax/property/renters_refund/income_sources.yaml @@ -4,7 +4,13 @@ values: - market_income - social_security - total_unemployment_compensation + # NDCC 57-02-08.1(5)(c) counts "any county, state, or federal public + # assistance benefits". The IRS leaves these out of gross income under its + # general welfare exclusion, which no statute or regulation enacts, so no + # federal or state law excludes them from income. - general_assistance + - ssi + - tanf metadata: unit: list period: year @@ -14,3 +20,5 @@ metadata: href: https://ndlegis.gov/cencode/t57c02.pdf#page=16 - title: North Dakota Office of State Tax Commissioner | Property Tax Credits for North Dakota Homeowners and Renters href: https://www.tax.nd.gov/sites/www/files/documents/guidelines/homestead-veterans-renters/credits-for-nd-homeowners-renters-guideline.pdf#page=2 + - title: North Dakota Office of State Tax Commissioner | SFN 24777 (10-2025), Application for Senior Citizens or Permanently and Totally Disabled Renter's Property Tax Refund, line 7 + href: https://www.tax.nd.gov/sites/www/files/documents/forms/homestead-disabled-renters/renters-refund-application.pdf#page=1 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 a6f6721a641..12bb5623f1c 100644 --- a/policyengine_us/tests/core/test_medical_expense_premium_conservation.py +++ b/policyengine_us/tests/core/test_medical_expense_premium_conservation.py @@ -370,7 +370,7 @@ def _state_case(**values): def _state_simulation(cases, reference_system, with_se_deduction): - people, tax_units, households, marital_units = {}, {}, {}, {} + people, tax_units, spm_units, households, marital_units = {}, {}, {}, {}, {} expectations = { "nd_renters_refund_income": [], "nm_medical_expense_credit": [], @@ -429,6 +429,10 @@ def _state_simulation(cases, reference_system, with_se_deduction): ), "employment_income": case["income"] if person_index == 0 else 0, "medicare_enrolled": False, + # NDCC 57-02-08.1(5)(c) also counts public assistance. These + # households receive none, so the ND expectation tests the + # medical base alone. + "ssi": 0, premium_variable: premiums[position], "other_medical_expenses": other[position], } @@ -448,6 +452,7 @@ def _state_simulation(cases, reference_system, with_se_deduction): if supplied: tax_unit["itemized_medical_expenses"] = {PERIOD: case["subtotal"]} tax_units[unit_name] = tax_unit + spm_units[unit_name] = {"members": names, "tanf": {PERIOD: 0}} households[unit_name] = {"members": names, "state_code": {PERIOD: state}} accounting["main_base"].append(main_base) accounting["gross"].append(gross) @@ -475,6 +480,7 @@ def _state_simulation(cases, reference_system, with_se_deduction): situation={ "people": people, "tax_units": tax_units, + "spm_units": spm_units, "households": households, "marital_units": marital_units, }, diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund.yaml index c2abaee3396..1b8c5b5bdeb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund.yaml @@ -134,3 +134,29 @@ state_code: ND output: nd_renters_refund: 400 + +- name: Case 8, SSI counts in income and lowers the refund. + absolute_error_margin: 0.01 + period: 2026 + input: + people: + person1: + age: 70 + social_security_retirement: 6_000 + ssi: 9_000 + rent: 4_800 + tax_units: + tax_unit: + members: [person1] + itemized_medical_expenses: 0 + households: + household: + members: [person1] + state_code: ND + output: + # NDCC 57-02-08.1(5)(c) counts social security and federal public + # assistance benefits: 6,000 + 9,000. + nd_renters_refund_income: 15_000 + # NDCC 57-02-08.1(2)(b): 20% of the rent less 4% of income, + # 960 - 600. + nd_renters_refund: 360 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 92a0bc58635..55d4a781189 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 @@ -40,7 +40,7 @@ output: nd_renters_refund_income: 0 -- name: Case 3, income includes unemployment and excludes nonincome sources. +- name: Case 3, income includes unemployment and public assistance and excludes nonincome sources. absolute_error_margin: 0.01 period: 2025 input: @@ -61,7 +61,10 @@ members: [person1] state_code: ND output: - nd_renters_refund_income: 12_000 + # NDCC 57-02-08.1(5)(c) counts "any county, state, or federal public + # assistance benefits", so general assistance and SSI count with + # unemployment compensation: 8,000 + 4,000 + 6,000. + nd_renters_refund_income: 18_000 - name: Case 4, after-tax premiums reduce renter refund income as medical expenses. absolute_error_margin: 0.01 @@ -346,7 +349,9 @@ people: parent: age: 40 - employment_income: 30_000 + # Wages go in before any labor supply response, where TANF reads + # them, so the family has no TANF. + employment_income_before_lsr: 30_000 child: age: 15 is_tax_unit_dependent: true @@ -411,7 +416,9 @@ people: parent: age: 40 - employment_income: 45_000 + # Wages go in before any labor supply response, where TANF reads + # them, so the family has no TANF. + employment_income_before_lsr: 45_000 child: age: 3 is_tax_unit_dependent: true @@ -480,3 +487,55 @@ output: # 30,000 + 5,000 + 1,000, with no state-law subtraction. nd_renters_refund_income: 36_000 + +- name: North Dakota counts TANF, which no law excludes from income, and leaves out SNAP, which federal law excludes. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 10_000 + child: + age: 8 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + tanf: 3_600 + snap: 2_400 + households: + household: + members: [parent, child] + state_code: ND + output: + # NDCC 57-02-08.1(5)(c) counts "any county, state, or federal public + # assistance benefits" and excludes "any amount excluded from income by + # federal or state law". 7 U.S.C. 2017(b) keeps SNAP out of income "for + # any purpose under any Federal, State, or local laws": 10,000 + 3,600. + nd_renters_refund_income: 13_600 + +- name: North Dakota counts a dependent's SSI with the applicant's social security. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandparent: + age: 70 + social_security_retirement: 18_000 + grandchild: + age: 10 + ssi: 9_000 + tax_units: + tax_unit: + members: [grandparent, grandchild] + households: + household: + members: [grandparent, grandchild] + state_code: ND + output: + # The statute counts "the income of any dependent of the applicant" and + # federal public assistance benefits: 18,000 + 9,000. + nd_renters_refund_income: 27_000 From 0aa7a651f4dbb10436d7d60db6428c4d2ed4d5d4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 8 Nov 2026 15:18:32 -0500 Subject: [PATCH 2/6] Log the North Dakota renter's refund TANF reading as a candidate The Tax Commissioner's guideline lists TANF among items that are not income; N.D.C.C. 57-02-08.1(5)(c) counts public assistance benefits and no law excludes TANF. The May 2013 guideline counted TANF as income. Co-Authored-By: Claude Opus 5.5 --- source_conflicts/candidates.yaml | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index aa64b2e1811..95f0e026dc4 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -6831,6 +6831,43 @@ candidates: - https://github.com/PolicyEngine/policyengine-us/pull/10663 state: ruled note: The statute's text settles the case, so it governs over the guideline's general list. +- id: us-nd:renters-refund-tanf + program: renter-credit + years: + from: 2025 + type: tax + question: Does the renter's refund count TANF, when the statute counts public assistance benefits and the Tax + Commissioner's guideline lists TANF among items that are not income? + sources: + - kind: statute + citation: N.D.C.C. 57-02-08.1(5)(c) + url: https://ndlegis.gov/cencode/t57c02.pdf#page=16 + - kind: guidance + citation: Property Tax Credits for North Dakota Homeowners and Renters, item 11 + url: https://www.tax.nd.gov/sites/www/files/documents/guidelines/homestead-veterans-renters/credits-for-nd-homeowners-renters-guideline.pdf#page=2 + readings: + statute: Income includes "any county, state, or federal public assistance benefits" and leaves out "any amount + excluded from income by federal or state law". No federal or North Dakota law excludes TANF from income. The + IRS leaves need-based public assistance out of gross income under its general welfare exclusion, which no + statute or regulation enacts. Federal law does exclude SNAP (7 U.S.C. 2017(b)) and LIHEAP (42 U.S.C. + 8624(f)(1)) from income for any purpose under state law. + other: Item 11 lists TANF among items that are not income, next to food stamps and federal fuel assistance. + example: A renter with $10,000 of wages and $3,600 of TANF has income of $13,600 under the statute and $10,000 + under item 11. + pe_us: + follows: statute + code: + - policyengine_us/parameters/gov/states/nd/tax/property/renters_refund/income_sources.yaml + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10724 + state: ruled + note: The statute names public assistance benefits as income, so it governs over item 11's general list, as in + us-nd:renters-refund-insurance-proceeds. Reading "excluded from income by federal or state law" to take in the + general welfare exclusion would leave the clause that counts public assistance almost nothing to count, and the + particular inclusion controls the general exclusion (N.D.C.C. 1-02-07, 1-02-38(2)). The May 2013 edition of the + guideline counted TANF as income under item 10, and Engrossed House Bill 1147 (2015) shows the clause as existing + text; the June 2023 and July 2025 editions list TANF under item 11. SSI raises no conflict, since SFN 24777 line 7 + asks for it. - id: us-nd:stillborn-subtraction-dependency-condition program: income-tax years: From 4a7f7ba045a45e22f5a7fa057b07a5a79d23e2ab Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 8 Nov 2026 15:35:45 -0500 Subject: [PATCH 3/6] Test public assistance in North Dakota renter's refund income as invariants Hypothesis checks four properties over random SPM units: a dollar of SSI, TANF or general assistance adds at most a dollar of income, and exactly a dollar above the zero floor; SNAP changes neither income nor the refund; public assistance never raises the refund; and TANF counts once in total across the tax units of an SPM unit, split per member. Dropping ssi and tanf from income_sources fails both tests, and adding snap fails the SNAP check. Co-Authored-By: Claude Opus 5.5 --- ...est_nd_renters_refund_public_assistance.py | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 policyengine_us/tests/test_nd_renters_refund_public_assistance.py diff --git a/policyengine_us/tests/test_nd_renters_refund_public_assistance.py b/policyengine_us/tests/test_nd_renters_refund_public_assistance.py new file mode 100644 index 00000000000..ae489931a18 --- /dev/null +++ b/policyengine_us/tests/test_nd_renters_refund_public_assistance.py @@ -0,0 +1,278 @@ +"""Invariants for public assistance in North Dakota's renter's refund income. + +N.D.C.C. 57-02-08.1(5)(c) (https://ndlegis.gov/cencode/t57c02.pdf#page=16) +counts income "including any county, state, or federal public assistance +benefits" and excludes "any amount excluded from income by federal or state +law". No federal or North Dakota law excludes SSI, TANF or general assistance +from income. 7 U.S.C. 2017(b) excludes SNAP from income "for any purpose under +any Federal, State, or local laws". SSI and general assistance belong to a +person; TANF belongs to an SPM unit, and add() gives each member of the unit an +equal share, which counts in that member's tax unit. + +Four invariants hold for every input. + +1. A dollar of SSI, TANF or general assistance raises renter's refund income + by at most a dollar and never lowers it. It raises it by exactly a dollar + when the income without the benefits is above zero, since only the floor + at zero can absorb any of it. +2. SNAP never changes renter's refund income or the refund. +3. Public assistance never raises the refund, since the refund falls as + income rises. +4. TANF counts once in total across the tax units of an SPM unit: the shares + the tax units count sum to the unit's benefit, each in proportion to the + tax unit's members. This per-person split is the model's attribution of an + SPM unit's benefit, which this test pins. + +YAML pins one household per case. These need random amounts, medical costs +that push income to the floor, and SPM units that hold more than one tax unit. +""" + +from collections import Counter + +import numpy as np +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2026 +# Amounts are float32 in the model. +TOLERANCE = 0.5 + + +@st.composite +def spm_units(draw, positive_income=False): + """A renter aged 65 or more in their own tax unit, sometimes sharing the + SPM unit with a parent and child in a second tax unit.""" + wages = st.integers(min_value=1 if positive_income else 0, max_value=40_000) + benefit = st.integers(min_value=0, max_value=12_000) + unit = { + "renter": { + "age": draw(st.integers(min_value=65, max_value=90)), + "employment_income": draw(wages), + "ssi": draw(benefit), + "general_assistance": draw(benefit), + # Medical costs above income push income to the zero floor. + "other_medical_expenses": ( + 0 if positive_income else draw(st.integers(0, 60_000)) + ), + "rent": draw(st.integers(min_value=0, max_value=15_000)), + }, + "tanf": draw(st.integers(min_value=0, max_value=10_000)), + "snap": draw(st.integers(min_value=0, max_value=8_000)), + } + if positive_income or draw(st.booleans()): + unit["family"] = { + "parent_wages": draw(wages), + "child_ssi": draw(benefit), + } + return unit + + +def without_public_assistance(unit): + renter = {**unit["renter"], "ssi": 0, "general_assistance": 0} + family = unit.get("family") + if family is not None: + family = {**family, "child_ssi": 0} + return {**unit, "renter": renter, "family": family, "tanf": 0} + + +def without(unit, benefit): + return {**unit, benefit: 0} + + +def situation(units): + """One household per SPM unit, all in North Dakota. Every benefit is an + input, so no formula adds any other.""" + people, tax_units, spm, households, marital_units = {}, {}, {}, {}, {} + for index, unit in enumerate(units): + renter = f"renter_{index}" + people[renter] = { + "age": {YEAR: unit["renter"]["age"]}, + # No computed Medicare premium adds to the medical costs. + "medicare_enrolled": {YEAR: False}, + "is_tax_unit_head": {YEAR: True}, + "is_tax_unit_dependent": {YEAR: False}, + **{ + name: {YEAR: unit["renter"][name]} + for name in ( + "employment_income", + "ssi", + "general_assistance", + "other_medical_expenses", + "rent", + ) + }, + } + members = [renter] + tax_units[f"renter_tax_unit_{index}"] = {"members": [renter]} + marital_units[f"renter_marital_unit_{index}"] = {"members": [renter]} + family = unit.get("family") + if family is not None: + parent, child = f"parent_{index}", f"child_{index}" + people[parent] = { + "age": {YEAR: 30}, + "medicare_enrolled": {YEAR: False}, + "is_tax_unit_head": {YEAR: True}, + "is_tax_unit_dependent": {YEAR: False}, + "employment_income": {YEAR: family["parent_wages"]}, + "ssi": {YEAR: 0}, + "general_assistance": {YEAR: 0}, + } + people[child] = { + "age": {YEAR: 8}, + "medicare_enrolled": {YEAR: False}, + "is_tax_unit_head": {YEAR: False}, + "is_tax_unit_dependent": {YEAR: True}, + "ssi": {YEAR: family["child_ssi"]}, + "general_assistance": {YEAR: 0}, + } + members += [parent, child] + tax_units[f"family_tax_unit_{index}"] = {"members": [parent, child]} + marital_units[f"parent_marital_unit_{index}"] = {"members": [parent]} + marital_units[f"child_marital_unit_{index}"] = {"members": [child]} + spm[f"spm_unit_{index}"] = { + "members": members, + "tanf": {YEAR: unit["tanf"]}, + "snap": {YEAR: unit["snap"]}, + } + households[f"household_{index}"] = { + "members": members, + "state_code": {YEAR: "ND"}, + } + return { + "people": people, + "tax_units": tax_units, + "spm_units": spm, + "households": households, + "marital_units": marital_units, + } + + +def tax_unit_rows(units): + """For each tax unit in situation order: its SPM unit's index, its member + count, and the SSI and general assistance its members receive.""" + rows = [] + for index, unit in enumerate(units): + renter = unit["renter"] + rows.append((index, 1, renter["ssi"] + renter["general_assistance"])) + family = unit.get("family") + if family is not None: + rows.append((index, 2, family["child_ssi"])) + return rows + + +def calculate(units, *names): + simulation = Simulation(situation=situation(units)) + return [simulation.calculate(name, YEAR).astype(float) for name in names] + + +def tanf_shares(units): + """Each tax unit's members' equal shares of their SPM unit's TANF.""" + rows = tax_unit_rows(units) + sizes = Counter() + for index, members, _ in rows: + sizes[index] += members + return np.array( + [units[index]["tanf"] * members / sizes[index] for index, members, _ in rows] + ) + + +def expected_public_assistance(units): + """Each tax unit's SSI and general assistance, plus its TANF share.""" + person_benefits = np.array([benefits for _, _, benefits in tax_unit_rows(units)]) + return person_benefits + tanf_shares(units) + + +@settings(max_examples=12, deadline=None) +@given(units=st.lists(spm_units(), min_size=1, max_size=8)) +@example( + units=[ + # The renter's medical costs exceed wages and benefits together, so + # the floor absorbs all of them. + { + "renter": { + "age": 70, + "employment_income": 5_000, + "ssi": 6_000, + "general_assistance": 1_000, + "other_medical_expenses": 30_000, + "rent": 6_000, + }, + "tanf": 0, + "snap": 2_000, + }, + # The floor absorbs part of the SSI. + { + "renter": { + "age": 70, + "employment_income": 0, + "ssi": 9_000, + "general_assistance": 0, + "other_medical_expenses": 4_000, + "rent": 6_000, + }, + "tanf": 0, + "snap": 0, + }, + ] +) +def test_public_assistance_counts_dollar_for_dollar_above_the_floor(units): + stripped = [without_public_assistance(unit) for unit in units] + no_snap = [without(unit, "snap") for unit in units] + income, refund = calculate( + units + stripped + no_snap, "nd_renters_refund_income", "nd_renters_refund" + ) + count = len(income) // 3 + with_benefits, without_benefits, without_snap = ( + income[:count], + income[count : 2 * count], + income[2 * count :], + ) + benefits = expected_public_assistance(units) + added = with_benefits - without_benefits + assert (added >= -TOLERANCE).all() + assert (added <= benefits + TOLERANCE).all() + above_floor = without_benefits > 0 + np.testing.assert_allclose( + added[above_floor], benefits[above_floor], atol=TOLERANCE + ) + # SNAP stays out of income and the refund. + np.testing.assert_allclose(without_snap, with_benefits, atol=TOLERANCE) + np.testing.assert_allclose(refund[2 * count :], refund[:count], atol=TOLERANCE) + # More income never raises the refund. + assert (refund[:count] <= refund[count : 2 * count] + TOLERANCE).all() + + +@settings(max_examples=12, deadline=None) +@given(units=st.lists(spm_units(positive_income=True), min_size=1, max_size=8)) +@example( + units=[ + { + "renter": { + "age": 72, + "employment_income": 12_000, + "ssi": 0, + "general_assistance": 0, + "other_medical_expenses": 0, + "rent": 7_200, + }, + "tanf": 3_600, + "snap": 0, + "family": {"parent_wages": 9_000, "child_ssi": 0}, + } + ] +) +def test_tanf_counts_once_across_the_tax_units_of_an_spm_unit(units): + no_tanf = [without(unit, "tanf") for unit in units] + (income,) = calculate(units + no_tanf, "nd_renters_refund_income") + count = len(income) // 2 + added = income[:count] - income[count:] + # Every tax unit has wages, so the floor absorbs no share. + np.testing.assert_allclose(added, tanf_shares(units), atol=TOLERANCE) + totals = Counter() + for (index, _, _), share in zip(tax_unit_rows(units), added): + totals[index] += share + for index, total in totals.items(): + assert total == pytest.approx(units[index]["tanf"], abs=TOLERANCE) From 54840127196b4510478e360da0384f100bbece7b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 8 Nov 2026 16:52:56 -0500 Subject: [PATCH 4/6] Guard LIHEAP and test a shared SPM unit above the floor in the North Dakota public assistance invariants Answers the independent review of 7c476bc687: - Invariant 1 speaks of a tax unit's share of TANF, so a tax unit in a shared SPM unit fits it, and says "exceeds zero". - Invariant 2 covers North Dakota LIHEAP, which 42 U.S.C. 8624(f)(1) excludes from income, and the test checks that the SNAP and LIHEAP inputs reach the model, so the guard is not vacuous. - situation() gives each SPM unit its own family. Without explicit families, the model puts every person in one. - A new example sits above the floor in a shared SPM unit. - The parameter comment and the candidate say the general welfare exclusion rests on IRS rulings, not a statute or regulation. The candidate adds that the Tax Commissioner's forms count SSI, which the IRS leaves out only under that doctrine: SFN 24777 line 7 and the 2026 homestead credit application. Co-Authored-By: Claude Opus 5.5 --- .../renters_refund/income_sources.yaml | 9 +- ...est_nd_renters_refund_public_assistance.py | 86 ++++++++++++++----- source_conflicts/candidates.yaml | 14 +-- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/policyengine_us/parameters/gov/states/nd/tax/property/renters_refund/income_sources.yaml b/policyengine_us/parameters/gov/states/nd/tax/property/renters_refund/income_sources.yaml index 40dcf8dc35d..27f742af193 100644 --- a/policyengine_us/parameters/gov/states/nd/tax/property/renters_refund/income_sources.yaml +++ b/policyengine_us/parameters/gov/states/nd/tax/property/renters_refund/income_sources.yaml @@ -5,9 +5,12 @@ values: - social_security - total_unemployment_compensation # NDCC 57-02-08.1(5)(c) counts "any county, state, or federal public - # assistance benefits". The IRS leaves these out of gross income under its - # general welfare exclusion, which no statute or regulation enacts, so no - # federal or state law excludes them from income. + # assistance benefits". The IRS leaves general assistance, SSI and TANF out + # of gross income under its general welfare exclusion, which rests on IRS + # rulings, not a statute or regulation, so no federal or state law + # excludes them from income. SNAP (7 U.S.C. 2017(b)) and LIHEAP, including + # nd_liheap (42 U.S.C. 8624(f)(1)), stay out because federal law excludes + # them from income for any purpose under state law. - general_assistance - ssi - tanf diff --git a/policyengine_us/tests/test_nd_renters_refund_public_assistance.py b/policyengine_us/tests/test_nd_renters_refund_public_assistance.py index ae489931a18..58b6a197dd0 100644 --- a/policyengine_us/tests/test_nd_renters_refund_public_assistance.py +++ b/policyengine_us/tests/test_nd_renters_refund_public_assistance.py @@ -5,17 +5,20 @@ benefits" and excludes "any amount excluded from income by federal or state law". No federal or North Dakota law excludes SSI, TANF or general assistance from income. 7 U.S.C. 2017(b) excludes SNAP from income "for any purpose under -any Federal, State, or local laws". SSI and general assistance belong to a -person; TANF belongs to an SPM unit, and add() gives each member of the unit an -equal share, which counts in that member's tax unit. +any Federal, State, or local laws", and 42 U.S.C. 8624(f)(1) excludes LIHEAP, +including North Dakota's, "for any purpose under any Federal or State law". +SSI and general assistance belong to a person; TANF belongs to an SPM unit, +and add() gives each member of the unit an equal share, which counts in that +member's tax unit. Four invariants hold for every input. -1. A dollar of SSI, TANF or general assistance raises renter's refund income - by at most a dollar and never lowers it. It raises it by exactly a dollar - when the income without the benefits is above zero, since only the floor - at zero can absorb any of it. -2. SNAP never changes renter's refund income or the refund. +1. A dollar of a tax unit's SSI, general assistance or TANF share raises its + renter's refund income by at most a dollar and never lowers it. It raises + it by exactly a dollar when the income without the benefits exceeds zero, + since only the floor at zero can absorb any of it. +2. SNAP and North Dakota LIHEAP never change renter's refund income or the + refund. 3. Public assistance never raises the refund, since the refund falls as income rises. 4. TANF counts once in total across the tax units of an SPM unit: the shares @@ -61,6 +64,7 @@ def spm_units(draw, positive_income=False): }, "tanf": draw(st.integers(min_value=0, max_value=10_000)), "snap": draw(st.integers(min_value=0, max_value=8_000)), + "nd_liheap": draw(st.integers(min_value=0, max_value=3_000)), } if positive_income or draw(st.booleans()): unit["family"] = { @@ -78,14 +82,15 @@ def without_public_assistance(unit): return {**unit, "renter": renter, "family": family, "tanf": 0} -def without(unit, benefit): - return {**unit, benefit: 0} +def without(unit, *benefits): + return {**unit, **{benefit: 0 for benefit in benefits}} def situation(units): - """One household per SPM unit, all in North Dakota. Every benefit is an - input, so no formula adds any other.""" - people, tax_units, spm, households, marital_units = {}, {}, {}, {}, {} + """One household, family and SPM unit per unit, all in North Dakota. + Every benefit is an input, so no formula adds any other.""" + people, tax_units, marital_units = {}, {}, {} + spm, families, households = {}, {}, {} for index, unit in enumerate(units): renter = f"renter_{index}" people[renter] = { @@ -136,7 +141,10 @@ def situation(units): "members": members, "tanf": {YEAR: unit["tanf"]}, "snap": {YEAR: unit["snap"]}, + "nd_liheap": {YEAR: unit["nd_liheap"]}, } + # Without explicit families, the model puts every person in one. + families[f"family_{index}"] = {"members": members} households[f"household_{index}"] = { "members": members, "state_code": {YEAR: "ND"}, @@ -144,9 +152,10 @@ def situation(units): return { "people": people, "tax_units": tax_units, + "marital_units": marital_units, "spm_units": spm, + "families": families, "households": households, - "marital_units": marital_units, } @@ -163,8 +172,7 @@ def tax_unit_rows(units): return rows -def calculate(units, *names): - simulation = Simulation(situation=situation(units)) +def calculate(simulation, *names): return [simulation.calculate(name, YEAR).astype(float) for name in names] @@ -202,6 +210,7 @@ def expected_public_assistance(units): }, "tanf": 0, "snap": 2_000, + "nd_liheap": 600, }, # The floor absorbs part of the SSI. { @@ -215,17 +224,47 @@ def expected_public_assistance(units): }, "tanf": 0, "snap": 0, + "nd_liheap": 0, }, ] ) +@example( + units=[ + # Above the floor in a shared SPM unit, the renter's tax unit counts + # 3,000 of SSI, 500 of general assistance and a third of the TANF + # (4,700), and the family's counts the child's 2,400 of SSI and two + # thirds of the TANF (4,800). + { + "renter": { + "age": 72, + "employment_income": 12_000, + "ssi": 3_000, + "general_assistance": 500, + "other_medical_expenses": 0, + "rent": 7_200, + }, + "tanf": 3_600, + "snap": 1_500, + "nd_liheap": 900, + "family": {"parent_wages": 9_000, "child_ssi": 2_400}, + } + ] +) def test_public_assistance_counts_dollar_for_dollar_above_the_floor(units): stripped = [without_public_assistance(unit) for unit in units] - no_snap = [without(unit, "snap") for unit in units] + no_snap_or_liheap = [without(unit, "snap", "nd_liheap") for unit in units] + simulation = Simulation(situation=situation(units + stripped + no_snap_or_liheap)) + # The SNAP and LIHEAP inputs reach the model, so the guard below tests them. + for name in ("snap", "nd_liheap"): + (amounts,) = calculate(simulation, name) + np.testing.assert_allclose( + amounts[: len(units)], [unit[name] for unit in units], atol=TOLERANCE + ) income, refund = calculate( - units + stripped + no_snap, "nd_renters_refund_income", "nd_renters_refund" + simulation, "nd_renters_refund_income", "nd_renters_refund" ) count = len(income) // 3 - with_benefits, without_benefits, without_snap = ( + with_benefits, without_benefits, without_snap_or_liheap = ( income[:count], income[count : 2 * count], income[2 * count :], @@ -238,8 +277,8 @@ def test_public_assistance_counts_dollar_for_dollar_above_the_floor(units): np.testing.assert_allclose( added[above_floor], benefits[above_floor], atol=TOLERANCE ) - # SNAP stays out of income and the refund. - np.testing.assert_allclose(without_snap, with_benefits, atol=TOLERANCE) + # SNAP and LIHEAP stay out of income and the refund. + np.testing.assert_allclose(without_snap_or_liheap, with_benefits, atol=TOLERANCE) np.testing.assert_allclose(refund[2 * count :], refund[:count], atol=TOLERANCE) # More income never raises the refund. assert (refund[:count] <= refund[count : 2 * count] + TOLERANCE).all() @@ -260,13 +299,16 @@ def test_public_assistance_counts_dollar_for_dollar_above_the_floor(units): }, "tanf": 3_600, "snap": 0, + "nd_liheap": 0, "family": {"parent_wages": 9_000, "child_ssi": 0}, } ] ) def test_tanf_counts_once_across_the_tax_units_of_an_spm_unit(units): no_tanf = [without(unit, "tanf") for unit in units] - (income,) = calculate(units + no_tanf, "nd_renters_refund_income") + (income,) = calculate( + Simulation(situation=situation(units + no_tanf)), "nd_renters_refund_income" + ) count = len(income) // 2 added = income[:count] - income[count:] # Every tax unit has wages, so the floor absorbs no share. diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 95f0e026dc4..8a84d501f71 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -6848,10 +6848,11 @@ candidates: readings: statute: Income includes "any county, state, or federal public assistance benefits" and leaves out "any amount excluded from income by federal or state law". No federal or North Dakota law excludes TANF from income. The - IRS leaves need-based public assistance out of gross income under its general welfare exclusion, which no - statute or regulation enacts. Federal law does exclude SNAP (7 U.S.C. 2017(b)) and LIHEAP (42 U.S.C. + IRS leaves need-based public assistance out of gross income under its general welfare exclusion, which rests on + IRS rulings, not a statute or regulation. Federal law does exclude SNAP (7 U.S.C. 2017(b)) and LIHEAP (42 U.S.C. 8624(f)(1)) from income for any purpose under state law. - other: Item 11 lists TANF among items that are not income, next to food stamps and federal fuel assistance. + other: Item 11 lists TANF among items that are not income, in the same list as food stamps and federal fuel + assistance. example: A renter with $10,000 of wages and $3,600 of TANF has income of $13,600 under the statute and $10,000 under item 11. pe_us: @@ -6864,10 +6865,11 @@ candidates: note: The statute names public assistance benefits as income, so it governs over item 11's general list, as in us-nd:renters-refund-insurance-proceeds. Reading "excluded from income by federal or state law" to take in the general welfare exclusion would leave the clause that counts public assistance almost nothing to count, and the - particular inclusion controls the general exclusion (N.D.C.C. 1-02-07, 1-02-38(2)). The May 2013 edition of the + particular inclusion controls the general exclusion (N.D.C.C. 1-02-07, 1-02-38(2)). The Tax Commissioner's own + forms count SSI, which the IRS also leaves out of gross income only under that doctrine. SFN 24777 line 7 asks for + "S.S.I.", and the 2026 homestead credit application lists "SSI benefits" as income. The May 2013 edition of the guideline counted TANF as income under item 10, and Engrossed House Bill 1147 (2015) shows the clause as existing - text; the June 2023 and July 2025 editions list TANF under item 11. SSI raises no conflict, since SFN 24777 line 7 - asks for it. + text; the June 2023 and July 2025 editions list TANF under item 11. - id: us-nd:stillborn-subtraction-dependency-condition program: income-tax years: From c0da46ca64a89ab2b8f917237cae51a4e16fd170 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 04:39:37 -0500 Subject: [PATCH 5/6] Quote the Social Security exception in the North Dakota TANF candidate N.D.C.C. 57-02-08.1(5)(c) leaves out amounts excluded by federal or state law "with the exception of income from social security benefits". The clause excepts nothing else, so public assistance that a statute does exclude, SNAP and LIHEAP, stays out. The candidate and the test docstring now quote the whole clause. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_nd_renters_refund_public_assistance.py | 4 ++-- source_conflicts/candidates.yaml | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/test_nd_renters_refund_public_assistance.py b/policyengine_us/tests/test_nd_renters_refund_public_assistance.py index 58b6a197dd0..18ab49afb2f 100644 --- a/policyengine_us/tests/test_nd_renters_refund_public_assistance.py +++ b/policyengine_us/tests/test_nd_renters_refund_public_assistance.py @@ -3,8 +3,8 @@ N.D.C.C. 57-02-08.1(5)(c) (https://ndlegis.gov/cencode/t57c02.pdf#page=16) counts income "including any county, state, or federal public assistance benefits" and excludes "any amount excluded from income by federal or state -law". No federal or North Dakota law excludes SSI, TANF or general assistance -from income. 7 U.S.C. 2017(b) excludes SNAP from income "for any purpose under +law with the exception of income from social security benefits". No federal or +North Dakota law excludes SSI, TANF or general assistance from income. 7 U.S.C. 2017(b) excludes SNAP from income "for any purpose under any Federal, State, or local laws", and 42 U.S.C. 8624(f)(1) excludes LIHEAP, including North Dakota's, "for any purpose under any Federal or State law". SSI and general assistance belong to a person; TANF belongs to an SPM unit, diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 8a84d501f71..62a1232c8ba 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -6847,10 +6847,11 @@ candidates: url: https://www.tax.nd.gov/sites/www/files/documents/guidelines/homestead-veterans-renters/credits-for-nd-homeowners-renters-guideline.pdf#page=2 readings: statute: Income includes "any county, state, or federal public assistance benefits" and leaves out "any amount - excluded from income by federal or state law". No federal or North Dakota law excludes TANF from income. The - IRS leaves need-based public assistance out of gross income under its general welfare exclusion, which rests on - IRS rulings, not a statute or regulation. Federal law does exclude SNAP (7 U.S.C. 2017(b)) and LIHEAP (42 U.S.C. - 8624(f)(1)) from income for any purpose under state law. + excluded from income by federal or state law with the exception of income from social security benefits". No + federal or North Dakota law excludes TANF from income. The IRS leaves need-based public assistance out of gross + income under its general welfare exclusion, which rests on IRS rulings, not a statute or regulation. Federal law + does exclude SNAP (7 U.S.C. 2017(b)) and LIHEAP (42 U.S.C. 8624(f)(1)) from income for any purpose under state + law, and the clause excepts only social security, so those two stay out. other: Item 11 lists TANF among items that are not income, in the same list as food stamps and federal fuel assistance. example: A renter with $10,000 of wages and $3,600 of TANF has income of $13,600 under the statute and $10,000 From d3668276d9a5512654189becc3f2c0186a50d3e8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 05:31:20 -0500 Subject: [PATCH 6/6] State the North Dakota TANF canon correctly and record the 2019 Act The candidate said the particular inclusion controls the general exclusion. N.D.C.C. 1-02-07 first asks for a reading that gives both provisions effect, and reaches the special provision only on an irreconcilable conflict. The note now says that, and says where the social security exception came from: House Bill 1174 (2019) added it in the Act that created the state income tax deduction for social security, so it says nothing about benefits no statute excludes. It also cites the two forms and the Act as sources, and rewraps the property test's docstring. Co-Authored-By: Claude Opus 5.5 --- ...est_nd_renters_refund_public_assistance.py | 13 ++++----- source_conflicts/candidates.yaml | 27 +++++++++++++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/policyengine_us/tests/test_nd_renters_refund_public_assistance.py b/policyengine_us/tests/test_nd_renters_refund_public_assistance.py index 18ab49afb2f..4b92e625756 100644 --- a/policyengine_us/tests/test_nd_renters_refund_public_assistance.py +++ b/policyengine_us/tests/test_nd_renters_refund_public_assistance.py @@ -4,12 +4,13 @@ counts income "including any county, state, or federal public assistance benefits" and excludes "any amount excluded from income by federal or state law with the exception of income from social security benefits". No federal or -North Dakota law excludes SSI, TANF or general assistance from income. 7 U.S.C. 2017(b) excludes SNAP from income "for any purpose under -any Federal, State, or local laws", and 42 U.S.C. 8624(f)(1) excludes LIHEAP, -including North Dakota's, "for any purpose under any Federal or State law". -SSI and general assistance belong to a person; TANF belongs to an SPM unit, -and add() gives each member of the unit an equal share, which counts in that -member's tax unit. +North Dakota law excludes SSI, TANF or general assistance from income. +7 U.S.C. 2017(b) excludes SNAP from income "for any purpose under any Federal, +State, or local laws", and 42 U.S.C. 8624(f)(1) excludes LIHEAP, including +North Dakota's, "for any purpose under any Federal or State law". SSI and +general assistance belong to a person; TANF belongs to an SPM unit, and add() +gives each member of the unit an equal share, which counts in that member's +tax unit. Four invariants hold for every input. diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 62a1232c8ba..5586cf5c8c3 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -6845,6 +6845,18 @@ candidates: - kind: guidance citation: Property Tax Credits for North Dakota Homeowners and Renters, item 11 url: https://www.tax.nd.gov/sites/www/files/documents/guidelines/homestead-veterans-renters/credits-for-nd-homeowners-renters-guideline.pdf#page=2 + - kind: form + citation: SFN 24777 (10-2025), line 7 + url: https://www.tax.nd.gov/sites/www/files/documents/forms/homestead-disabled-renters/renters-refund-application.pdf#page=1 + - kind: form + citation: SFN 24757 (10-2025), 2026 homestead credit application, list of income, p. 2 + url: https://www.tax.nd.gov/sites/www/files/documents/property-tax/2026-homestead-credit-application.pdf#page=2 + - kind: statute + citation: House Bill 1174 (2019), enrolled, sections 1 and 3 + url: https://ndlegis.gov/assembly/66-2019/regular/documents/19-0115-05000.pdf#page=1 + - kind: agency-practice + citation: Office of Tax Commissioner, fiscal note on House Bill 1174 (2019), on section 1 + url: https://ndlegis.gov/files/resource/66-2019/library/hb1174.pdf#page=1 readings: statute: Income includes "any county, state, or federal public assistance benefits" and leaves out "any amount excluded from income by federal or state law with the exception of income from social security benefits". No @@ -6865,12 +6877,17 @@ candidates: state: ruled note: The statute names public assistance benefits as income, so it governs over item 11's general list, as in us-nd:renters-refund-insurance-proceeds. Reading "excluded from income by federal or state law" to take in the - general welfare exclusion would leave the clause that counts public assistance almost nothing to count, and the - particular inclusion controls the general exclusion (N.D.C.C. 1-02-07, 1-02-38(2)). The Tax Commissioner's own - forms count SSI, which the IRS also leaves out of gross income only under that doctrine. SFN 24777 line 7 asks for + general welfare exclusion would leave the clause that counts public assistance almost nothing to count. N.D.C.C. + 1-02-07 and 1-02-38(2) call for a reading that gives both clauses effect. The inclusion counts public assistance, + and the exclusion removes what a statute or regulation excludes. The exception for social security says which + excluded amounts come back. The Legislative Assembly added it in House Bill 1174 (2019), the Act that created a + state income tax deduction for social security (section 3, now N.D.C.C. 57-38-30.3(2)(s)). In its fiscal note, the + Tax Commissioner's office wrote that section 1 of that Act keeps the full benefit in the homestead income test, so + the exception says nothing about benefits no statute excludes. The Tax Commissioner's own forms count SSI, which + the IRS also leaves out of gross income only under the general welfare exclusion. SFN 24777 line 7 asks for "S.S.I.", and the 2026 homestead credit application lists "SSI benefits" as income. The May 2013 edition of the - guideline counted TANF as income under item 10, and Engrossed House Bill 1147 (2015) shows the clause as existing - text; the June 2023 and July 2025 editions list TANF under item 11. + guideline counted TANF as income under item 10, and Engrossed House Bill 1147 (2015) shows the public assistance + words as existing text; the June 2023 and July 2025 editions list TANF under item 11. - id: us-nd:stillborn-subtraction-dependency-condition program: income-tax years: