From 4cdae4f74545e03944ee4a120918a3b2ff682ab3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 09:16:34 -0400 Subject: [PATCH 1/2] Share North Carolina's $20,000 mortgage and property tax cap between spouses filing separately G.S. 105-153.5(a)(2)b caps mortgage interest plus real estate taxes at $20,000 "claimed by both spouses combined" for spouses filing separately, prorated by the share each paid or, for joint obligations paid from joint accounts, by each spouse's income. PE gave each tax unit its own $20,000. Split Form D-400 Schedule A lines 3, 4 and 5 into nc_mortgage_and_property_tax_before_limitation, _limitation and _deduction. Line 4 reaches a separately filing spouse in another tax unit through a two-person marital unit whose members each head a SEPARATE return (the Arkansas medical-floor guard). New MaritalUnit input mortgage_and_real_estate_taxes_paid_from_joint_account switches the shares from line 3 amounts to federal AGI. Co-Authored-By: Claude Opus 5.5 --- .../nc-mortgage-property-cap-mfs.fixed.md | 1 + ..._and_property_tax_limitation_invariants.py | 259 ++++++++++ .../deductions/nc_itemized_deductions.yaml | 37 ++ ...ge_and_property_tax_before_limitation.yaml | 32 ++ ...c_mortgage_and_property_tax_deduction.yaml | 166 +++++++ ..._mortgage_and_property_tax_limitation.yaml | 459 ++++++++++++++++++ .../deductions/nc_itemized_deductions.py | 16 +- ...gage_and_property_tax_before_limitation.py | 28 ++ .../nc_mortgage_and_property_tax_deduction.py | 24 + ...nc_mortgage_and_property_tax_limitation.py | 78 +++ ...al_estate_taxes_paid_from_joint_account.py | 11 + 11 files changed, 1099 insertions(+), 12 deletions(-) create mode 100644 changelog.d/nc-mortgage-property-cap-mfs.fixed.md create mode 100644 policyengine_us/tests/core/test_nc_mortgage_and_property_tax_limitation_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.yaml create mode 100644 policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py create mode 100644 policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.py create mode 100644 policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.py create mode 100644 policyengine_us/variables/household/expense/housing/mortgage_and_real_estate_taxes_paid_from_joint_account.py diff --git a/changelog.d/nc-mortgage-property-cap-mfs.fixed.md b/changelog.d/nc-mortgage-property-cap-mfs.fixed.md new file mode 100644 index 00000000000..484f545a42a --- /dev/null +++ b/changelog.d/nc-mortgage-property-cap-mfs.fixed.md @@ -0,0 +1 @@ +North Carolina's $20,000 cap on mortgage interest and real estate taxes is now shared by spouses filing separately, prorated by the share each paid or, for joint-account payments, by each spouse's income (G.S. 105-153.5(a)(2)b). diff --git a/policyengine_us/tests/core/test_nc_mortgage_and_property_tax_limitation_invariants.py b/policyengine_us/tests/core/test_nc_mortgage_and_property_tax_limitation_invariants.py new file mode 100644 index 00000000000..61fb839fa8a --- /dev/null +++ b/policyengine_us/tests/core/test_nc_mortgage_and_property_tax_limitation_invariants.py @@ -0,0 +1,259 @@ +"""Invariants of North Carolina's $20,000 mortgage and property tax limitation. + +N.C. Gen. Stat. 105-153.5(a)(2)b caps home mortgage interest plus real estate +taxes at $20,000, "claimed by both spouses combined" when spouses file +separately. Above $20,000 the deductions are "prorated based on the percentage +paid by each spouse", or, "for joint obligations paid from joint accounts", +"based on the income reported by each spouse". Form D-400 Schedule A applies +this as line 4 (the limitation) and line 5 (the smaller of line 3 and line 4). + +Spouses filing separately are two tax units linked by a marital unit, so +YAML's single-situation cases cannot cover what this file checks: many mixed +returns in one vectorized simulation (separate couples with and without a +joint account, joint returns, single filers, a lone separate filer, and a +separate return with a dependent), the same returns with the spouses listed +in the other order, and the results under higher payments. Hypothesis draws +batches, and a seeded population of 120 returns adds breadth. Line 3 and AGI +are inputs, so no federal calculation runs. For every return: + +1. Differential: lines 4 and 5 equal an independent restatement of the + statute, with the $20,000 taken from the statute rather than the + parameter files. Separate filers are spouses only when an explicit marital + unit pairs them; the paid share is each spouse's line 3 over the couple's; + the joint-account share is each spouse's federal AGI, not below zero, over + the couple's, or one half when neither spouse has any. +2. Bounds: 0 <= line 5 <= line 3, and a couple's two lines 5 never exceed + $20,000. Without a joint account they add up to the smaller of the + couple's line 3 total and $20,000. +3. Label invariance: listing a couple's spouses in the other order gives each + spouse the same result. +4. Monotone (without a joint account): raising one spouse's payments never + lowers that spouse's deduction or the couple's total, and never raises the + other spouse's deduction. With a joint account the form can lower the + total: just above $20,000 each spouse gets the smaller of their own line 3 + and their income share of $20,000. That is intended under the Schedule A + reading and not checked here. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +CAP = 20_000 # G.S. 105-153.5(a)(2)b +TOLERANCE = 0.01 # dollars +YEARS = ["2024", "2026"] + +amounts = st.one_of( + st.just(0.0), + st.integers(1, 12_000).map(float), + st.integers(12_000, 45_000).map(float), +) +incomes = st.one_of( + st.just(0.0), + st.integers(-30_000, -1).map(float), + st.integers(1, 300_000).map(float), +) +increases = st.integers(0, 20_000).map(float) +KINDS = ["separate", "joint", "single", "lone_separate", "separate_dependent"] + + +@st.composite +def returns(draw): + kind = draw(st.sampled_from(KINDS)) + spouses = 2 if kind in ("separate", "separate_dependent") else 1 + return { + "kind": kind, + "line3": [draw(amounts) for _ in range(spouses)], + "agi": [draw(incomes) for _ in range(spouses)], + "increase": draw(increases), + "joint_account": draw(st.booleans()) and spouses == 2, + } + + +def _seeded_returns(n=120, seed=20261009): + rng = np.random.default_rng(seed) + drawn = [] + for _ in range(n): + kind = KINDS[rng.integers(len(KINDS))] + spouses = 2 if kind in ("separate", "separate_dependent") else 1 + drawn.append( + { + "kind": kind, + "line3": [ + float(rng.choice([0, rng.integers(1, 45_001)])) + for _ in range(spouses) + ], + "agi": [float(rng.integers(-30_000, 300_001)) for _ in range(spouses)], + "increase": float(rng.integers(0, 20_001)), + "joint_account": bool(rng.random() < 0.5) and spouses == 2, + } + ) + return drawn + + +def _situation(drawn, year, *, swap=False, raise_first=False): + """One tax unit per return, or two for spouses filing separately.""" + people, tax_units, marital_units, households = {}, {}, {}, {} + rows = [] # (return index, spouse index) per tax unit, in order + for i, r in enumerate(drawn): + order = [1, 0] if swap and len(r["line3"]) == 2 else [0, 1] + line3 = list(r["line3"]) + if raise_first: + line3[0] += r["increase"] + names = [] + if r["kind"] == "joint": + names = [f"r{i}_head", f"r{i}_spouse"] + people[names[0]] = {"age": {year: 45}} + people[names[1]] = {"age": {year: 43}} + tax_units[f"t{i}"] = { + "members": names, + "filing_status": {year: "JOINT"}, + "nc_mortgage_and_property_tax_before_limitation": {year: line3[0]}, + "adjusted_gross_income": {year: r["agi"][0]}, + } + marital_units[f"m{i}"] = {"members": names} + rows.append((i, 0)) + elif r["kind"] in ("single", "lone_separate"): + names = [f"r{i}_head"] + people[names[0]] = {"age": {year: 45}} + tax_units[f"t{i}"] = { + "members": names, + "filing_status": { + year: "SINGLE" if r["kind"] == "single" else "SEPARATE" + }, + "nc_mortgage_and_property_tax_before_limitation": {year: line3[0]}, + "adjusted_gross_income": {year: r["agi"][0]}, + } + marital_units[f"m{i}"] = {"members": names} + rows.append((i, 0)) + else: + couple = [] + for k in order: + name = f"r{i}_spouse{k}" + couple.append(name) + members = [name] + people[name] = {"age": {year: 45}} + if r["kind"] == "separate_dependent" and k == 0: + child = f"r{i}_child" + people[child] = { + "age": {year: 10}, + "is_tax_unit_dependent": {year: True}, + } + members.append(child) + marital_units[f"m{i}_child"] = {"members": [child]} + names.append(child) + tax_units[f"t{i}_{k}"] = { + "members": members, + "filing_status": {year: "SEPARATE"}, + "nc_mortgage_and_property_tax_before_limitation": {year: line3[k]}, + "adjusted_gross_income": {year: r["agi"][k]}, + } + rows.append((i, k)) + marital_units[f"m{i}"] = { + "members": couple, + "mortgage_and_real_estate_taxes_paid_from_joint_account": { + year: r["joint_account"] + }, + } + names += couple + households[f"h{i}"] = {"members": names, "state_code": {year: "NC"}} + situation = { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "households": households, + } + return Simulation(situation=situation), rows + + +def _expected(drawn, raise_first=False): + """Lines 4 and 5 per (return, spouse), restated from the statute.""" + limitation, deduction = {}, {} + for i, r in enumerate(drawn): + line3 = list(r["line3"]) + if raise_first: + line3[0] += r["increase"] + if len(line3) == 1: + limitation[i, 0] = CAP + deduction[i, 0] = min(line3[0], CAP) + continue + combined = sum(line3) + if combined <= CAP: + shares = None + elif r["joint_account"]: + income = [max(a, 0) for a in r["agi"]] + total = sum(income) + shares = [a / total for a in income] if total > 0 else [0.5, 0.5] + else: + shares = [a / combined for a in line3] + for k in range(2): + limitation[i, k] = CAP if shares is None else CAP * shares[k] + deduction[i, k] = min(line3[k], limitation[i, k]) + return limitation, deduction + + +def _results(drawn, year, **kwargs): + simulation, rows = _situation(drawn, year, **kwargs) + line4 = simulation.calculate("nc_mortgage_and_property_tax_limitation", year) + line5 = simulation.calculate("nc_mortgage_and_property_tax_deduction", year) + return ( + {row: float(v) for row, v in zip(rows, line4)}, + {row: float(v) for row, v in zip(rows, line5)}, + ) + + +def _check(drawn, year): + line3 = {(i, k): a for i, r in enumerate(drawn) for k, a in enumerate(r["line3"])} + expected4, expected5 = _expected(drawn) + line4, line5 = _results(drawn, year) + + # 1. Differential against the restated statute. + for row in expected4: + assert abs(line4[row] - expected4[row]) < TOLERANCE, (row, drawn[row[0]]) + assert abs(line5[row] - expected5[row]) < TOLERANCE, (row, drawn[row[0]]) + + # 2. Bounds, and the couple's total. + for row, value in line5.items(): + assert -TOLERANCE < value < line3[row] + TOLERANCE + for i, r in enumerate(drawn): + if len(r["line3"]) == 2: + total = line5[i, 0] + line5[i, 1] + assert total < CAP + TOLERANCE + if not r["joint_account"]: + assert abs(total - min(sum(r["line3"]), CAP)) < TOLERANCE + + # 3. The order in which the spouses are listed does not matter. + swapped4, swapped5 = _results(drawn, year, swap=True) + for row in line5: + assert abs(swapped4[row] - line4[row]) < TOLERANCE + assert abs(swapped5[row] - line5[row]) < TOLERANCE + + # 4. Paying more never lowers the payer's deduction or the couple's total + # without a joint account. + raised4, raised5 = _results(drawn, year, raise_first=True) + expected_raised4, expected_raised5 = _expected(drawn, raise_first=True) + for row in expected_raised5: + assert abs(raised4[row] - expected_raised4[row]) < TOLERANCE + assert abs(raised5[row] - expected_raised5[row]) < TOLERANCE + for i, r in enumerate(drawn): + if len(r["line3"]) == 2 and not r["joint_account"]: + assert raised5[i, 0] > line5[i, 0] - TOLERANCE + assert raised5[i, 1] < line5[i, 1] + TOLERANCE + assert raised5[i, 0] + raised5[i, 1] > line5[i, 0] + line5[i, 1] - TOLERANCE + + +@settings( + max_examples=6, + deadline=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(drawn=st.lists(returns(), min_size=5, max_size=25), year=st.sampled_from(YEARS)) +def test_nc_mortgage_and_property_tax_limitation_invariants(drawn, year): + _check(drawn, year) + + +def test_nc_mortgage_and_property_tax_limitation_seeded_population(): + for year in YEARS: + _check(_seeded_returns(), year) diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_itemized_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_itemized_deductions.yaml index 31a7a288278..e7de92e5de7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_itemized_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_itemized_deductions.yaml @@ -45,3 +45,40 @@ state_code: NC output: nc_itemized_deductions: 14_000 + +- name: Spouses filing separately share the $20,000 mortgage and property tax limitation + period: 2024 + input: + people: + spouse1: + age: 45 + employment_income: 90_000 + home_mortgage_interest: 15_000 + spouse2: + age: 45 + employment_income: 90_000 + home_mortgage_interest: 15_000 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + charitable_deduction: 5_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + charitable_deduction: 5_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # G.S. 105-153.5(a)(2)b: $20,000 for both spouses combined, prorated by + # the 50% each paid, so $10,000 each, plus $5,000 of charity. + nc_itemized_deductions: [15_000, 15_000] + # $15,000 exceeds the $12,750 standard deduction for a separate return. + nc_taxable_income: [75_000, 75_000] + # 4.5% in 2024. + nc_income_tax: [3_375, 3_375] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.yaml new file mode 100644 index 00000000000..482f949d18b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.yaml @@ -0,0 +1,32 @@ +# Form D-400 Schedule A line 3: home mortgage interest (line 1) plus real +# estate property taxes (line 2), G.S. 105-153.5(a)(2)b. + +- name: Mortgage interest plus real estate taxes within the federal cap + period: 2024 + input: + filing_status: JOINT + mortgage_interest: 9_000 + real_estate_taxes: 4_000 + state_code: NC + output: + nc_mortgage_and_property_tax_before_limitation: 13_000 + +- name: Real estate taxes above the federal cap count only up to the cap + period: 2024 + input: + filing_status: SEPARATE + mortgage_interest: 9_000 + real_estate_taxes: 11_000 + state_code: NC + output: + # Section 164 allows $5,000 of these taxes on a separate return. + nc_mortgage_and_property_tax_before_limitation: 14_000 + +- name: The amount before limitation is not capped at $20,000 + period: 2024 + input: + filing_status: SINGLE + mortgage_interest: 25_000 + state_code: NC + output: + nc_mortgage_and_property_tax_before_limitation: 25_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.yaml new file mode 100644 index 00000000000..e617361d3dc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.yaml @@ -0,0 +1,166 @@ +# Form D-400 Schedule A line 5: "Compare Line 3 with Line 4 and enter the +# smaller amount." Line 3 is home mortgage interest plus real estate property +# taxes; line 4 is the $20,000 limitation, shared by spouses filing separately +# (G.S. 105-153.5(a)(2)b). Spouses filing separately are two tax units linked +# by an explicit marital unit. + +- name: A single filer's deduction is capped at $20,000 + period: 2024 + input: + filing_status: SINGLE + nc_mortgage_and_property_tax_before_limitation: 25_000 + state_code: NC + output: + nc_mortgage_and_property_tax_deduction: 20_000 + +- name: Spouses filing separately who each paid $15,000 of mortgage interest deduct $10,000 each + period: 2024 + input: + people: + spouse1: + age: 45 + home_mortgage_interest: 15_000 + spouse2: + age: 45 + home_mortgage_interest: 15_000 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # $30,000 combined; each paid half of the $20,000 the spouses may claim. + nc_mortgage_and_property_tax_before_limitation: [15_000, 15_000] + nc_mortgage_and_property_tax_deduction: [10_000, 10_000] + +- name: Spouses filing separately with unequal payments deduct their prorated shares of $20,000 + period: 2024 + input: + people: + spouse1: + age: 45 + home_mortgage_interest: 18_000 + spouse2: + age: 45 + home_mortgage_interest: 2_000 + real_estate_taxes: 4_000 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # $18,000 and $6,000 paid, $24,000 combined: 75% and 25% of $20,000. + nc_mortgage_and_property_tax_before_limitation: [18_000, 6_000] + nc_mortgage_and_property_tax_deduction: [15_000, 5_000] + +- name: Spouses filing separately below $20,000 combined each deduct what they paid + period: 2024 + input: + people: + spouse1: + age: 45 + home_mortgage_interest: 8_000 + spouse2: + age: 45 + home_mortgage_interest: 7_000 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + nc_mortgage_and_property_tax_deduction: [8_000, 7_000] + +- name: Joint obligations paid from a joint account are prorated by income + period: 2024 + input: + people: + spouse1: + age: 45 + home_mortgage_interest: 15_000 + spouse2: + age: 45 + home_mortgage_interest: 15_000 + marital_units: + couple: + members: [spouse1, spouse2] + mortgage_and_real_estate_taxes_paid_from_joint_account: true + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + adjusted_gross_income: 120_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + adjusted_gross_income: 40_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # Line 4 is 75% and 25% of $20,000: $15,000 and $5,000. Line 5 is the + # smaller of line 3 ($15,000 each) and line 4. + nc_mortgage_and_property_tax_limitation: [15_000, 5_000] + nc_mortgage_and_property_tax_deduction: [15_000, 5_000] + +- name: A joint-account income share above a spouse's own amount leaves line 5 at line 3 + period: 2024 + input: + people: + spouse1: + age: 45 + home_mortgage_interest: 15_000 + spouse2: + age: 45 + home_mortgage_interest: 15_000 + marital_units: + couple: + members: [spouse1, spouse2] + mortgage_and_real_estate_taxes_paid_from_joint_account: true + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + adjusted_gross_income: 90_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + adjusted_gross_income: 10_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # Line 4 is 90% and 10% of $20,000: $18,000 and $2,000. Line 5 takes the + # smaller of each spouse's line 3 ($15,000) and line 4. + nc_mortgage_and_property_tax_limitation: [18_000, 2_000] + nc_mortgage_and_property_tax_deduction: [15_000, 2_000] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.yaml new file mode 100644 index 00000000000..ba0d8270c52 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.yaml @@ -0,0 +1,459 @@ +# N.C. Gen. Stat. 105-153.5(a)(2)b and the D-401 instructions for Form D-400 +# Schedule A line 4: "For spouses filing as married filing separately or +# married filing jointly, the total mortgage interest and real estate taxes +# claimed by both spouses combined may not exceed $20,000." Above $20,000 the +# spouses' amounts "must be prorated based on the percentage paid by each +# spouse", and "for joint obligations paid from joint accounts, the proration +# is based on the income reported by each spouse". +# Spouses filing separately are two tax units linked by an explicit marital +# unit. Unless a case says otherwise, the amounts before limitation (line 3) +# are inputs. + +- name: A single filer's limitation is $20,000 + period: 2024 + input: + filing_status: SINGLE + nc_mortgage_and_property_tax_before_limitation: 25_000 + state_code: NC + output: + nc_mortgage_and_property_tax_limitation: 20_000 + +- name: A joint return has one $20,000 limitation for both spouses + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + tax_units: + tax_unit: + members: [spouse1, spouse2] + filing_status: JOINT + nc_mortgage_and_property_tax_before_limitation: 30_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + nc_mortgage_and_property_tax_limitation: 20_000 + +- name: Spouses filing separately who each paid $15,000 share the limitation equally + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # $30,000 combined exceeds $20,000: each paid 50%, so $20,000 x 50%. + nc_mortgage_and_property_tax_limitation: [10_000, 10_000] + +- name: Spouses filing separately with unequal payments prorate the limitation by the share each paid + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 18_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 6_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # $24,000 combined: $20,000 x 18/24 = $15,000 and $20,000 x 6/24 = $5,000. + nc_mortgage_and_property_tax_limitation: [15_000, 5_000] + +- name: Spouses filing separately keep the full limitation when their combined amounts are below $20,000 + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 8_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 7_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # $15,000 combined does not exceed $20,000, so nothing is prorated. + nc_mortgage_and_property_tax_limitation: [20_000, 20_000] + +- name: Spouses filing separately whose combined amounts equal $20,000 are not prorated + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 12_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 8_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # Proration applies only when the combined amount "exceeds" $20,000. + nc_mortgage_and_property_tax_limitation: [20_000, 20_000] + +- name: Joint obligations paid from a joint account prorate the limitation by each spouse's income + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + marital_units: + couple: + members: [spouse1, spouse2] + mortgage_and_real_estate_taxes_paid_from_joint_account: true + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + adjusted_gross_income: 120_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + adjusted_gross_income: 40_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # Income shares 120/160 = 75% and 40/160 = 25% of $20,000. + nc_mortgage_and_property_tax_limitation: [15_000, 5_000] + +- name: Joint-account proration gives no share to a spouse with negative income + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + marital_units: + couple: + members: [spouse1, spouse2] + mortgage_and_real_estate_taxes_paid_from_joint_account: true + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + adjusted_gross_income: -10_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + adjusted_gross_income: 60_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # Income below zero counts as zero: shares 0/60,000 and 60,000/60,000. + nc_mortgage_and_property_tax_limitation: [0, 20_000] + +- name: Joint-account proration is equal when neither spouse has income + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + marital_units: + couple: + members: [spouse1, spouse2] + mortgage_and_real_estate_taxes_paid_from_joint_account: true + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 18_000 + adjusted_gross_income: 0 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 6_000 + adjusted_gross_income: 0 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + nc_mortgage_and_property_tax_limitation: [10_000, 10_000] + +- name: A joint account changes nothing when the combined amounts are below $20,000 + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + marital_units: + couple: + members: [spouse1, spouse2] + mortgage_and_real_estate_taxes_paid_from_joint_account: true + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 8_000 + adjusted_gross_income: 150_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 7_000 + adjusted_gross_income: 10_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + nc_mortgage_and_property_tax_limitation: [20_000, 20_000] + +- name: The proration uses each spouse's allowed property taxes, not taxes above the federal cap + period: 2024 + input: + people: + spouse1: + age: 45 + home_mortgage_interest: 10_000 + # Only $5,000 is allowed under section 164 on a separate return. + real_estate_taxes: 9_000 + spouse2: + age: 45 + home_mortgage_interest: 10_000 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # Line 3 is $10,000 + $5,000 = $15,000 and $10,000; $25,000 combined. + # $20,000 x 15/25 = $12,000 and $20,000 x 10/25 = $8,000. + nc_mortgage_and_property_tax_before_limitation: [15_000, 10_000] + nc_mortgage_and_property_tax_limitation: [12_000, 8_000] + +- name: A spouse with a dependent on their separate return still shares the limitation + period: 2024 + input: + people: + spouse1: + age: 45 + spouse2: + age: 43 + child: + age: 10 + is_tax_unit_dependent: true + marital_units: + couple: + members: [spouse1, spouse2] + child_unit: + members: [child] + tax_units: + tax_unit1: + members: [spouse1, child] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + households: + household: + members: [spouse1, spouse2, child] + state_code: NC + output: + nc_mortgage_and_property_tax_limitation: [10_000, 10_000] + +- name: A separate filer whose spouse is not in the simulation keeps the $20,000 limitation + period: 2024 + input: + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 25_000 + state_code: NC + output: + # The spouse's amount is unknown, so none is counted against the cap. + nc_mortgage_and_property_tax_limitation: 20_000 + +- name: Two separate filers alone in a simulation without marital units are read as spouses + period: 2024 + input: + people: + person1: + age: 45 + person2: + age: 45 + tax_units: + tax_unit1: + members: [person1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + tax_unit2: + members: [person2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + households: + household: + members: [person1, person2] + state_code: NC + output: + # Both share the default marital unit, and married people filing + # separately must both file separately, so the two are spouses. + nc_mortgage_and_property_tax_limitation: [10_000, 10_000] + +- name: A separate filer does not share the limitation with an unrelated single filer + period: 2024 + input: + people: + person1: + age: 45 + person2: + age: 45 + tax_units: + tax_unit1: + members: [person1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + tax_unit2: + members: [person2] + filing_status: SINGLE + nc_mortgage_and_property_tax_before_limitation: 15_000 + households: + household1: + members: [person1] + state_code: NC + household2: + members: [person2] + state_code: NC + output: + nc_mortgage_and_property_tax_limitation: [20_000, 20_000] + +- name: Separate filers in a default marital unit of three people do not share the limitation + period: 2024 + input: + people: + person1: + age: 45 + person2: + age: 45 + person3: + age: 45 + tax_units: + tax_unit1: + members: [person1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + tax_unit2: + members: [person2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + tax_unit3: + members: [person3] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + households: + household: + members: [person1, person2, person3] + state_code: NC + output: + # A default unit of three cannot identify a spouse. + nc_mortgage_and_property_tax_limitation: [20_000, 20_000, 20_000] + +- name: Spouses filing separately share the limitation in 2026 + period: 2026 + input: + people: + spouse1: + age: 45 + spouse2: + age: 45 + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 30_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 10_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # $40,000 combined: $20,000 x 30/40 and $20,000 x 10/40. + nc_mortgage_and_property_tax_limitation: [15_000, 5_000] diff --git a/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_itemized_deductions.py b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_itemized_deductions.py index 994735af1a7..e9dc7209140 100644 --- a/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_itemized_deductions.py +++ b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_itemized_deductions.py @@ -11,18 +11,10 @@ class nc_itemized_deductions(Variable): defined_for = StateCode.NC def formula(tax_unit, period, parameters): - # Qualified Mortgage Interest and Real Estate Property Taxes. - filing_status = tax_unit("filing_status", period) - - mortgage_interest = add(tax_unit, period, ["mortgage_interest"]) - pirs = parameters(period).gov.irs.deductions.itemized.salt_and_real_estate - property_taxes = min_( - add(tax_unit, period, ["real_estate_taxes"]), - pirs.cap[filing_status], - ) - pco = parameters(period).gov.states.nc.tax.income.deductions.itemized.cap - capped_mortage_and_property_taxes = min_( - mortgage_interest + property_taxes, pco.mortgage_and_property_tax + # Qualified Mortgage Interest and Real Estate Property Taxes, after the + # $20,000 limitation shared by spouses (Schedule A line 5). + capped_mortage_and_property_taxes = tax_unit( + "nc_mortgage_and_property_tax_deduction", period ) # North Carolina specifies a state and local tax deduction cap which is currently not modeled in PolicyEngine diff --git a/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py new file mode 100644 index 00000000000..a3c470682e6 --- /dev/null +++ b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * + + +class nc_mortgage_and_property_tax_before_limitation(Variable): + value_type = float + entity = TaxUnit + label = "North Carolina mortgage interest and real estate property taxes before limitation" + unit = USD + definition_period = YEAR + documentation = "Form D-400 Schedule A line 3: home mortgage interest (line 1) plus real estate property taxes (line 2), before the $20,000 limitation." + reference = ( + # N.C. Gen. Stat. 105-153.5(a)(2)b + "https://www.ncleg.gov/EnactedLegislation/Statutes/HTML/BySection/Chapter_105/GS_105-153.5.html", + # 2025 Form D-401 instructions, Form D-400 Schedule A lines 1 to 3 + "https://www.ncdor.gov/2025-d-401-individual-income-tax-instructions/open#page=20", + ) + defined_for = StateCode.NC + + def formula(tax_unit, period, parameters): + filing_status = tax_unit("filing_status", period) + + mortgage_interest = add(tax_unit, period, ["mortgage_interest"]) + pirs = parameters(period).gov.irs.deductions.itemized.salt_and_real_estate + property_taxes = min_( + add(tax_unit, period, ["real_estate_taxes"]), + pirs.cap[filing_status], + ) + return mortgage_interest + property_taxes diff --git a/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.py b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.py new file mode 100644 index 00000000000..424b6c0d3d7 --- /dev/null +++ b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class nc_mortgage_and_property_tax_deduction(Variable): + value_type = float + entity = TaxUnit + label = "North Carolina mortgage interest and real estate property tax deduction" + unit = USD + definition_period = YEAR + documentation = "Form D-400 Schedule A line 5: the smaller of the amount before limitation (line 3) and the limitation (line 4)." + reference = ( + # N.C. Gen. Stat. 105-153.5(a)(2)b + "https://www.ncleg.gov/EnactedLegislation/Statutes/HTML/BySection/Chapter_105/GS_105-153.5.html", + # 2025 Form D-401 instructions, Form D-400 Schedule A line 5 + "https://www.ncdor.gov/2025-d-401-individual-income-tax-instructions/open#page=20", + ) + defined_for = StateCode.NC + + def formula(tax_unit, period, parameters): + before_limitation = tax_unit( + "nc_mortgage_and_property_tax_before_limitation", period + ) + limitation = tax_unit("nc_mortgage_and_property_tax_limitation", period) + return min_(before_limitation, limitation) diff --git a/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.py b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.py new file mode 100644 index 00000000000..3d90e8b6409 --- /dev/null +++ b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.py @@ -0,0 +1,78 @@ +from policyengine_us.model_api import * + + +class nc_mortgage_and_property_tax_limitation(Variable): + value_type = float + entity = TaxUnit + label = "North Carolina mortgage interest and real estate property tax limitation" + unit = USD + definition_period = YEAR + documentation = "Form D-400 Schedule A line 4: $20,000 per return, shared by spouses filing separate returns. When the spouses' combined amounts before limitation exceed $20,000, each spouse's limitation is their prorated share of $20,000." + reference = ( + # N.C. Gen. Stat. 105-153.5(a)(2)b + "https://www.ncleg.gov/EnactedLegislation/Statutes/HTML/BySection/Chapter_105/GS_105-153.5.html", + # 2025 Form D-401 instructions, Form D-400 Schedule A line 4 + "https://www.ncdor.gov/2025-d-401-individual-income-tax-instructions/open#page=20", + ) + defined_for = StateCode.NC + + def formula(tax_unit, period, parameters): + cap = parameters( + period + ).gov.states.nc.tax.income.deductions.itemized.cap.mortgage_and_property_tax + person = tax_unit.members + # "For spouses filing as married filing separately or married filing + # jointly, the total mortgage interest and real estate taxes claimed by + # both spouses combined may not exceed $20,000." A joint return holds + # both spouses, so the cap applies to it once. A spouse filing + # separately is in another tax unit. Reach them through the marital + # unit only when it holds exactly two people who each head a separate + # return: without explicit marital units, everyone shares one default + # unit. + filing_status = tax_unit("filing_status", period) + separate = filing_status == filing_status.possible_values.SEPARATE + head = person("is_tax_unit_head", period) + separate_head = head & tax_unit.project(separate) + couple = (person.marital_unit.nb_persons() == 2) & ( + person.marital_unit.sum(separate_head) == 2 + ) + # Each spouse's amount before limitation (Schedule A line 3), on the + # head of their own return. + before_limitation = tax_unit( + "nc_mortgage_and_property_tax_before_limitation", period + ) + own = head * tax_unit.project(before_limitation) + combined = person.marital_unit.sum(own) + # "If the amount of the mortgage interest and real estate taxes paid by + # both spouses exceeds $20,000, these deductions must be prorated based + # on the percentage paid by each spouse." + paid_share = np.divide( + own, combined, out=np.zeros_like(own), where=combined > 0 + ) + # "For joint obligations paid from joint accounts, the proration is + # based on the income reported by each spouse for that taxable year." + # Income is federal adjusted gross income (Form D-400 line 6), not + # below zero; the shares are equal when neither spouse has any. + agi = max_(tax_unit("adjusted_gross_income", period), 0) + own_income = head * tax_unit.project(agi) + combined_income = person.marital_unit.sum(own_income) + income_share = where( + combined_income > 0, + np.divide( + own_income, + combined_income, + out=np.zeros_like(own_income), + where=combined_income > 0, + ), + 0.5, + ) + joint_account = person.marital_unit( + "mortgage_and_real_estate_taxes_paid_from_joint_account", period + ) + share = where(joint_account, income_share, paid_share) + prorated = head & couple & (combined > cap) + return where( + tax_unit.any(prorated), + cap * tax_unit.sum(where(prorated, share, 0)), + cap, + ) diff --git a/policyengine_us/variables/household/expense/housing/mortgage_and_real_estate_taxes_paid_from_joint_account.py b/policyengine_us/variables/household/expense/housing/mortgage_and_real_estate_taxes_paid_from_joint_account.py new file mode 100644 index 00000000000..9699b5ca1ad --- /dev/null +++ b/policyengine_us/variables/household/expense/housing/mortgage_and_real_estate_taxes_paid_from_joint_account.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class mortgage_and_real_estate_taxes_paid_from_joint_account(Variable): + value_type = bool + entity = MaritalUnit + label = "Mortgage interest and real estate taxes paid from a joint account" + documentation = "Whether a married couple paid the home mortgage interest and real estate taxes they owe jointly from a joint account. North Carolina then shares its mortgage and property tax cap between spouses filing separately by income rather than by the amount each paid." + definition_period = YEAR + default_value = False + reference = "https://www.ncleg.gov/EnactedLegislation/Statutes/HTML/BySection/Chapter_105/GS_105-153.5.html" From 1932c539819e6b472e29d3d04d01326aa8528792 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 22:24:39 -0400 Subject: [PATCH 2/2] Include other-state spouses in North Carolina shared mortgage cap --- .../nc-mortgage-property-cap-mfs.fixed.md | 2 +- ..._and_property_tax_limitation_invariants.py | 22 +-- ...c_mortgage_and_property_tax_deduction.yaml | 168 ++++++++++++++++++ ..._mortgage_and_property_tax_limitation.yaml | 69 ++++++- ...gage_and_property_tax_before_limitation.py | 5 +- ...nc_mortgage_and_property_tax_limitation.py | 3 +- 6 files changed, 253 insertions(+), 16 deletions(-) diff --git a/changelog.d/nc-mortgage-property-cap-mfs.fixed.md b/changelog.d/nc-mortgage-property-cap-mfs.fixed.md index 484f545a42a..7ac5d07fdfc 100644 --- a/changelog.d/nc-mortgage-property-cap-mfs.fixed.md +++ b/changelog.d/nc-mortgage-property-cap-mfs.fixed.md @@ -1 +1 @@ -North Carolina's $20,000 cap on mortgage interest and real estate taxes is now shared by spouses filing separately, prorated by the share each paid or, for joint-account payments, by each spouse's income (G.S. 105-153.5(a)(2)b). +North Carolina's $20,000 cap on mortgage interest and real estate taxes is now shared by spouses filing separately, including a spouse in another state, prorated by the share each paid or, for joint-account payments, by each spouse's income (G.S. 105-153.5(a)(2)b). diff --git a/policyengine_us/tests/core/test_nc_mortgage_and_property_tax_limitation_invariants.py b/policyengine_us/tests/core/test_nc_mortgage_and_property_tax_limitation_invariants.py index 61fb839fa8a..e8cffe9a9a4 100644 --- a/policyengine_us/tests/core/test_nc_mortgage_and_property_tax_limitation_invariants.py +++ b/policyengine_us/tests/core/test_nc_mortgage_and_property_tax_limitation_invariants.py @@ -7,19 +7,21 @@ "based on the income reported by each spouse". Form D-400 Schedule A applies this as line 4 (the limitation) and line 5 (the smaller of line 3 and line 4). -Spouses filing separately are two tax units linked by a marital unit, so -YAML's single-situation cases cannot cover what this file checks: many mixed -returns in one vectorized simulation (separate couples with and without a -joint account, joint returns, single filers, a lone separate filer, and a -separate return with a dependent), the same returns with the spouses listed -in the other order, and the results under higher payments. Hypothesis draws -batches, and a seeded population of 120 returns adds breadth. Line 3 and AGI -are inputs, so no federal calculation runs. For every return: +Generated situations use nonnegative line 3, valid tax-unit heads, explicit +marital units, and NC households. They include separate couples with and +without a joint account, joint returns, single filers, a lone separate filer, +and a separate return with a dependent. YAML can represent mixed vectorized +populations; Python adds randomized generation, shrinking, and dynamic +comparisons after swapping spouse labels or raising payments. Hypothesis +draws six batches of 5 to 25 records, choosing 2024 or 2026 for each; a seeded +population of 120 records runs in both years. Line 3 and AGI are inputs, so +these tests do not cover the federal line-3 calculation. Within this domain: 1. Differential: lines 4 and 5 equal an independent restatement of the statute, with the $20,000 taken from the statute rather than the - parameter files. Separate filers are spouses only when an explicit marital - unit pairs them; the paid share is each spouse's line 3 over the couple's; + parameter files. In these fixtures, two separate heads in an explicit + two-person marital unit are spouses. The paid share is each spouse's line 3 + over the couple's total; the joint-account share is each spouse's federal AGI, not below zero, over the couple's, or one half when neither spouse has any. 2. Bounds: 0 <= line 5 <= line 3, and a couple's two lines 5 never exceed diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.yaml index e617361d3dc..b22505ec460 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_deduction.yaml @@ -164,3 +164,171 @@ # smaller of each spouse's line 3 ($15,000) and line 4. nc_mortgage_and_property_tax_limitation: [18_000, 2_000] nc_mortgage_and_property_tax_deduction: [15_000, 2_000] + +- name: Spouses in NC and VA paying $15,000 and $15,000 from a joint account share the cap + period: 2024 + input: + people: + nc_spouse: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 15_000 + va_spouse: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 15_000 + marital_units: + couple: + members: [nc_spouse, va_spouse] + mortgage_and_real_estate_taxes_paid_from_joint_account: true + tax_units: + nc_tax_unit: + members: [nc_spouse] + filing_status: SEPARATE + adjusted_gross_income: 50_000 + va_tax_unit: + members: [va_spouse] + filing_status: SEPARATE + adjusted_gross_income: 50_000 + households: + nc_household: + members: [nc_spouse] + state_code: NC + va_household: + members: [va_spouse] + state_code: VA + output: + # $15,000 + $15,000 = $30,000 exceeds $20,000. Equal incomes give + # $20,000 x $50,000 / $100,000 = $10,000; line 5 is + # min($15,000, $10,000) = $10,000 for the NC spouse. + # The VA spouse's line 3 counts in the allocation; NC lines 4 and 5 + # remain zero for their own return. + nc_mortgage_and_property_tax_before_limitation: [15_000, 15_000] + nc_mortgage_and_property_tax_limitation: [10_000, 0] + nc_mortgage_and_property_tax_deduction: [10_000, 0] + +- name: Spouses in NC and VA paying $25,000 and $15,000 from a joint account share the cap + period: 2024 + input: + people: + nc_spouse: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 25_000 + va_spouse: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 15_000 + marital_units: + couple: + members: [nc_spouse, va_spouse] + mortgage_and_real_estate_taxes_paid_from_joint_account: true + tax_units: + nc_tax_unit: + members: [nc_spouse] + filing_status: SEPARATE + adjusted_gross_income: 50_000 + va_tax_unit: + members: [va_spouse] + filing_status: SEPARATE + adjusted_gross_income: 50_000 + households: + nc_household: + members: [nc_spouse] + state_code: NC + va_household: + members: [va_spouse] + state_code: VA + output: + # $25,000 + $15,000 = $40,000 exceeds $20,000. Equal incomes give + # $20,000 x $50,000 / $100,000 = $10,000; line 5 is + # min($25,000, $10,000) = $10,000 for the NC spouse. + # The VA spouse's line 3 counts in the allocation; NC lines 4 and 5 + # remain zero for their own return. + nc_mortgage_and_property_tax_before_limitation: [25_000, 15_000] + nc_mortgage_and_property_tax_limitation: [10_000, 0] + nc_mortgage_and_property_tax_deduction: [10_000, 0] + +- name: Spouses in NC and VA paying $15,000 and $15,000 from separate accounts share the cap + period: 2024 + input: + people: + nc_spouse: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 15_000 + va_spouse: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 15_000 + marital_units: + couple: + members: [nc_spouse, va_spouse] + mortgage_and_real_estate_taxes_paid_from_joint_account: false + tax_units: + nc_tax_unit: + members: [nc_spouse] + filing_status: SEPARATE + adjusted_gross_income: 50_000 + va_tax_unit: + members: [va_spouse] + filing_status: SEPARATE + adjusted_gross_income: 50_000 + households: + nc_household: + members: [nc_spouse] + state_code: NC + va_household: + members: [va_spouse] + state_code: VA + output: + # $15,000 + $15,000 = $30,000 exceeds $20,000. The NC paid share is + # $15,000 / $30,000, so $20,000 x 15/30 = $10,000; line 5 is + # min($15,000, $10,000) = $10,000 for the NC spouse. + # The VA spouse's line 3 counts in the allocation; NC lines 4 and 5 + # remain zero for their own return. + nc_mortgage_and_property_tax_before_limitation: [15_000, 15_000] + nc_mortgage_and_property_tax_limitation: [10_000, 0] + nc_mortgage_and_property_tax_deduction: [10_000, 0] + +- name: Spouses in NC and VA paying $25,000 and $15,000 from separate accounts share the cap + period: 2024 + input: + people: + nc_spouse: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 25_000 + va_spouse: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 15_000 + marital_units: + couple: + members: [nc_spouse, va_spouse] + mortgage_and_real_estate_taxes_paid_from_joint_account: false + tax_units: + nc_tax_unit: + members: [nc_spouse] + filing_status: SEPARATE + adjusted_gross_income: 50_000 + va_tax_unit: + members: [va_spouse] + filing_status: SEPARATE + adjusted_gross_income: 50_000 + households: + nc_household: + members: [nc_spouse] + state_code: NC + va_household: + members: [va_spouse] + state_code: VA + output: + # $25,000 + $15,000 = $40,000 exceeds $20,000. The NC paid share is + # $25,000 / $40,000, so $20,000 x 25/40 = $12,500; line 5 is + # min($25,000, $12,500) = $12,500 for the NC spouse. + # The VA spouse's line 3 counts in the allocation; NC lines 4 and 5 + # remain zero for their own return. + nc_mortgage_and_property_tax_before_limitation: [25_000, 15_000] + nc_mortgage_and_property_tax_limitation: [12_500, 0] + nc_mortgage_and_property_tax_deduction: [12_500, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.yaml index ba0d8270c52..6d98d0761c4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.yaml @@ -368,8 +368,8 @@ members: [person1, person2] state_code: NC output: - # Both share the default marital unit, and married people filing - # separately must both file separately, so the two are spouses. + # Both separate heads share a default two-person marital unit, + # which the model uses to identify the two as spouses. nc_mortgage_and_property_tax_limitation: [10_000, 10_000] - name: A separate filer does not share the limitation with an unrelated single filer @@ -457,3 +457,68 @@ output: # $40,000 combined: $20,000 x 30/40 and $20,000 x 10/40. nc_mortgage_and_property_tax_limitation: [15_000, 5_000] + +- name: A considered-unmarried head-of-household spouse does not share the cap with a separate filer + period: 2024 + input: + people: + spouse1: + age: 45 + is_tax_unit_head: true + spouse2: + age: 45 + is_tax_unit_head: true + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: HEAD_OF_HOUSEHOLD + nc_mortgage_and_property_tax_before_limitation: 15_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # The considered-unmarried HOH filer is outside the spouses' shared + # cap: each has $20,000, and min($15,000, $20,000) is $15,000. + nc_mortgage_and_property_tax_limitation: [20_000, 20_000] + nc_mortgage_and_property_tax_deduction: [15_000, 15_000] + +- name: Separate returns with no tax-unit heads fall back to the per-return cap + period: 2024 + input: + people: + spouse1: + age: 45 + is_tax_unit_head: false + spouse2: + age: 45 + is_tax_unit_head: false + marital_units: + couple: + members: [spouse1, spouse2] + tax_units: + tax_unit1: + members: [spouse1] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + tax_unit2: + members: [spouse2] + filing_status: SEPARATE + nc_mortgage_and_property_tax_before_limitation: 15_000 + households: + household: + members: [spouse1, spouse2] + state_code: NC + output: + # Without two separate heads the spouse guard is false: each return + # keeps $20,000, and min($15,000, $20,000) is $15,000. These invalid filer + # roles are outside the property tests' valid-head shared-cap domain. + nc_mortgage_and_property_tax_limitation: [20_000, 20_000] + nc_mortgage_and_property_tax_deduction: [15_000, 15_000] diff --git a/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py index a3c470682e6..24da4bdf9a9 100644 --- a/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py +++ b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py @@ -7,14 +7,15 @@ class nc_mortgage_and_property_tax_before_limitation(Variable): label = "North Carolina mortgage interest and real estate property taxes before limitation" unit = USD definition_period = YEAR - documentation = "Form D-400 Schedule A line 3: home mortgage interest (line 1) plus real estate property taxes (line 2), before the $20,000 limitation." + documentation = "Form D-400 Schedule A line 3: home mortgage interest (line 1) plus real estate property taxes (line 2), before the $20,000 limitation. Calculated for all states so a separately filing spouse outside North Carolina contributes to the shared cap." reference = ( # N.C. Gen. Stat. 105-153.5(a)(2)b "https://www.ncleg.gov/EnactedLegislation/Statutes/HTML/BySection/Chapter_105/GS_105-153.5.html", # 2025 Form D-401 instructions, Form D-400 Schedule A lines 1 to 3 "https://www.ncdor.gov/2025-d-401-individual-income-tax-instructions/open#page=20", ) - defined_for = StateCode.NC + # Both spouses' amounts enter the shared cap even if one lives outside NC. + # Only the final limitation and deduction are restricted to NC returns. def formula(tax_unit, period, parameters): filing_status = tax_unit("filing_status", period) diff --git a/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.py b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.py index 3d90e8b6409..f13d4610775 100644 --- a/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.py +++ b/policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_limitation.py @@ -37,7 +37,8 @@ def formula(tax_unit, period, parameters): person.marital_unit.sum(separate_head) == 2 ) # Each spouse's amount before limitation (Schedule A line 3), on the - # head of their own return. + # head of their own return. Line 3 also computes federal amounts for + # an other-state spouse, whose payments count toward the shared cap. before_limitation = tax_unit( "nc_mortgage_and_property_tax_before_limitation", period )