From 936ee3cc401104eed64723ee6255e5e5e8a2a3b6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 08:35:40 -0400 Subject: [PATCH 1/3] Apply federal mortgage debt limits by loan vintage Grandfathered debt incurred on or before October 13, 1987 has no dollar limit and reduces the other limits (26 U.S.C. 163(h)(3)(D)). Debt incurred on or before December 15, 2017 keeps the $1,000,000 limit and reduces the $750,000 limit for later debt ((h)(3)(F)). Compute the qualified loan limit as in IRS Publication 936 Table 1, so it no longer depends on which loan is larger or which slot it is in (fixes #10011). Co-Authored-By: Claude Opus 5.5 --- .../mortgage-grandfathered-debt.fixed.md | 1 + .../itemized/interest/mortgage/cap.yaml | 2 +- .../grandfathered_origination_year.yaml | 11 + .../interest/mortgage/pre_tcja_cap.yaml | 6 +- .../mortgage/pre_tcja_origination_year.yaml | 2 +- .../test_mortgage_debt_limit_invariants.py | 235 ++++++++++++++++++ ...deductible_mortgage_interest_tax_unit.yaml | 235 ++++++++++++++++++ .../mt_itemized_deductions_joint.yaml | 34 +++ .../tax_unit/mortgage_interest_structure.py | 109 +++++--- 9 files changed, 589 insertions(+), 46 deletions(-) create mode 100644 changelog.d/mortgage-grandfathered-debt.fixed.md create mode 100644 policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/grandfathered_origination_year.yaml create mode 100644 policyengine_us/tests/core/test_mortgage_debt_limit_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml diff --git a/changelog.d/mortgage-grandfathered-debt.fixed.md b/changelog.d/mortgage-grandfathered-debt.fixed.md new file mode 100644 index 00000000000..584e5e7b1fa --- /dev/null +++ b/changelog.d/mortgage-grandfathered-debt.fixed.md @@ -0,0 +1 @@ +Apply the federal mortgage debt limits by loan vintage: grandfathered debt incurred on or before October 13, 1987 is no longer capped and reduces the other limits, and older debt larger than the $750,000 limit no longer shrinks the qualified balance of two mortgages to $750,000 (26 U.S.C. 163(h)(3)(D) and (F), Publication 936 Table 1). diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/cap.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/cap.yaml index f42dc0c672d..125ed83991b 100644 --- a/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/cap.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/cap.yaml @@ -1,4 +1,4 @@ -description: The IRS allows an itemized deduction for interest paid on non-grandfathered home acquisition debt up to this mortgage balance, based on filing status. +description: The IRS allows an itemized deduction for interest paid on home acquisition debt incurred after December 15, 2017, up to this mortgage balance less any earlier debt, based on filing status. SINGLE: 1900-01-01: 1_000_000 2018-01-01: 750_000 diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/grandfathered_origination_year.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/grandfathered_origination_year.yaml new file mode 100644 index 00000000000..c5b97c31f6a --- /dev/null +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/grandfathered_origination_year.yaml @@ -0,0 +1,11 @@ +description: Home acquisition debt originated in or before this calendar year is treated as grandfathered debt, incurred on or before October 13, 1987, for the federal mortgage interest deduction. +values: + 1900-01-01: 1987 +metadata: + unit: year + label: IRS grandfathered mortgage origination year cutoff + reference: + - title: 26 U.S. Code § 163(h)(3)(D) - Treatment of indebtedness incurred on or before October 13, 1987 + href: https://www.law.cornell.edu/uscode/text/26/163#h_3_D + - title: "IRS Publication 936 (2025), Grandfathered Debt and Table 1 #page 13-14" + href: https://www.irs.gov/pub/irs-prior/p936--2025.pdf#page=13 diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/pre_tcja_cap.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/pre_tcja_cap.yaml index 49d8ae350a9..9a4b85d7356 100644 --- a/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/pre_tcja_cap.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/pre_tcja_cap.yaml @@ -1,4 +1,4 @@ -description: The IRS allows an itemized deduction for interest paid on pre-TCJA grandfathered home acquisition debt up to this mortgage balance, based on filing status. +description: The IRS allows an itemized deduction for interest paid on home acquisition debt incurred on or before December 15, 2017, up to this mortgage balance less any grandfathered debt, based on filing status. SINGLE: 1900-01-01: 1_000_000 JOINT: @@ -16,5 +16,5 @@ metadata: period: year label: IRS pre-TCJA home mortgage value cap reference: - - title: 26 U.S. Code § 163 - Interest, (h)(3)(F) - href: https://www.law.cornell.edu/uscode/text/26/163 + - title: 26 U.S. Code § 163 - Interest, (h)(3)(B)(ii), (D)(ii) and (F) + href: https://www.law.cornell.edu/uscode/text/26/163#h_3 diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/pre_tcja_origination_year.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/pre_tcja_origination_year.yaml index 9ad331604a6..44567b89c68 100644 --- a/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/pre_tcja_origination_year.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/pre_tcja_origination_year.yaml @@ -1,4 +1,4 @@ -description: Home acquisition debt originated on or before this calendar year is treated as pre-TCJA grandfathered debt for the federal mortgage interest deduction. +description: Home acquisition debt originated in or before this calendar year is treated as incurred on or before December 15, 2017, for the federal mortgage interest deduction. values: 1900-01-01: 2017 metadata: diff --git a/policyengine_us/tests/core/test_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_mortgage_debt_limit_invariants.py new file mode 100644 index 00000000000..280d988754d --- /dev/null +++ b/policyengine_us/tests/core/test_mortgage_debt_limit_invariants.py @@ -0,0 +1,235 @@ +"""Invariants for the federal acquisition-debt limits on mortgage interest. + +26 U.S.C. 163(h)(3) limits deductible mortgage interest by the vintage of +each loan: + +- grandfathered debt, incurred on or before October 13, 1987, is acquisition + debt with no dollar limit, and it reduces the $1,000,000 limit + ($500,000 married filing separately) for other debt ((h)(3)(D)); +- debt incurred on or before December 15, 2017 keeps that $1,000,000 limit, + and for taxable years after 2017 debt incurred later has a $750,000 limit + ($375,000), reduced by the earlier debt treated as acquisition debt + ((h)(3)(F)(i)(II) and (IV)). + +IRS Publication 936 (2025), Table 1 (PDF page 14), computes the qualified loan +limit in that order, reducing each limit by grandfathered debt once ("The +limits above are reduced (but not below zero) by the amount of your +grandfathered debt", PDF page 12). Interest is deductible in the ratio of the +limit to the total balance (lines 12-15). + +The model stores two loans per tax unit with a balance and an origination +year. Hypothesis draws pairs of loans around every cutoff and limit and checks +the model's helpers against two independent restatements, one line by line +from Table 1 with its "stop here" branch and one in the statute's order: + +1. Differential: the helpers equal both restatements. +2. Bounds: grandfathered debt <= qualified limit <= total debt; the limit is + at least min(total debt, the post-2017 limit) and at most + max(grandfathered debt, the $1,000,000 limit). +3. Order: swapping the two loans never changes the limit. +4. Monotone: a larger balance, or an older vintage, never lowers the limit. + +One vectorized simulation then checks a seeded grid of tax units in 2017 and +2024 end to end: the deductible interest equals the restatement, lies between +0 and the interest paid, and with the non-deductible interest adds up to the +interest paid. The YAML cases in +deductible_mortgage_interest_tax_unit.yaml check single households; this grid +checks the parameter lookups for mixed filing statuses in one vectorized run. + +Cutoff years are written here, not read from the parameters: only the year is +known, so 1987 counts as on or before October 13, 1987 and 2017 as on or +before December 15, 2017. An unknown year (0) counts as post-2017 debt. +""" + +from itertools import product + +import numpy as np +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.variables.household.expense.tax_unit.mortgage_interest_structure import ( + _debt_by_vintage, + _qualified_loan_limit, +) + +GRANDFATHERED_YEAR = 1987 +PRE_TCJA_YEAR = 2017 +# (limit for debt on or before December 15, 2017, limit for later debt) +LIMITS = { + "2018+": (1_000_000, 750_000), + "2018+ separate": (500_000, 375_000), + "pre-2018": (1_000_000, 1_000_000), + "pre-2018 separate": (500_000, 500_000), +} +TOLERANCE = 0.01 + +YEARS = (0, 1960, 1986, 1987, 1988, 2000, 2016, 2017, 2018, 2025) +BOUNDARY_BALANCES = (0, 1, 375_000, 500_000, 750_000, 775_000, 1_000_000) +balances = st.one_of( + st.sampled_from(BOUNDARY_BALANCES), + st.integers(0, 3_000_000), +).map(float) +loans = st.tuples(balances, st.sampled_from(YEARS)) + + +def vintage(year): + if year <= 0: + return "post" + if year <= GRANDFATHERED_YEAR: + return "grandfathered" + if year <= PRE_TCJA_YEAR: + return "pre" + return "post" + + +def split(loan_list): + debt = {"grandfathered": 0.0, "pre": 0.0, "post": 0.0} + for balance, year in loan_list: + debt[vintage(year)] += balance + return debt["grandfathered"], debt["pre"], debt["post"] + + +def table_1(loan_list, limits): + """Publication 936 (2025) Table 1, lines 1-11, with its branch.""" + pre_limit, post_limit = limits + line_1, line_2, line_7 = split(loan_list) + line_3 = pre_limit + line_4 = max(line_1, line_3) + line_5 = line_1 + line_2 + line_6 = min(line_4, line_5) + # "If you have no home acquisition debt incurred after December 15, 2017, + # or the amount on line 6 is $750,000 ... or more, line 6 is your + # qualified loan limit." + if line_7 == 0 or line_6 >= post_limit: + return line_6 + line_8 = post_limit + line_9 = max(line_6, line_8) + line_10 = line_6 + line_7 + return min(line_9, line_10) + + +def statute(loan_list, limits): + """26 U.S.C. 163(h)(3)(B)(ii), (D) and (F), applied in order.""" + pre_limit, post_limit = limits + grandfathered, pre, post = split(loan_list) + allowed_pre = min(pre, max(0.0, pre_limit - grandfathered)) + allowed_post = min(post, max(0.0, post_limit - grandfathered - allowed_pre)) + return grandfathered + allowed_pre + allowed_post + + +def model_limit(loan_list, limits): + debt = _debt_by_vintage( + [np.array([balance]) for balance, _ in loan_list], + [np.array([year]) for _, year in loan_list], + GRANDFATHERED_YEAR, + PRE_TCJA_YEAR, + ) + return float(_qualified_loan_limit(*debt, *limits)[0]) + + +SETTINGS = dict(max_examples=400, deadline=None, derandomize=True) + + +@settings(**SETTINGS) +@given(first=loans, second=loans, limits=st.sampled_from(sorted(LIMITS))) +def test_qualified_loan_limit_properties(first, second, limits): + limits = LIMITS[limits] + pair = [first, second] + limit = model_limit(pair, limits) + grandfathered, pre, post = split(pair) + total = grandfathered + pre + post + + # 1. Differential. + assert abs(limit - table_1(pair, limits)) <= TOLERANCE + assert abs(limit - statute(pair, limits)) <= TOLERANCE + + # 2. Bounds. + assert grandfathered - TOLERANCE <= limit <= total + TOLERANCE + assert limit >= min(total, limits[1]) - TOLERANCE + assert limit <= max(grandfathered, limits[0]) + TOLERANCE + + # 3. Order. + assert abs(model_limit([second, first], limits) - limit) <= TOLERANCE + + # 4. Monotone in balances and in vintage. + for i, (balance, year) in enumerate(pair): + bigger = list(pair) + bigger[i] = (balance + 100_000, year) + assert model_limit(bigger, limits) >= limit - TOLERANCE + for older_year in YEARS: + older = ("grandfathered", "pre", "post").index(vintage(older_year)) + if older <= ("grandfathered", "pre", "post").index(vintage(year)): + moved = list(pair) + moved[i] = (balance, older_year) + assert model_limit(moved, limits) >= limit - TOLERANCE + + +def test_vintage_split_conserves_debt(): + balances = np.array([100.0, 200.0, 300.0, 400.0, 500.0]) + years = np.array([0, 1987, 1988, 2017, 2018]) + debt = _debt_by_vintage([balances], [years], GRANDFATHERED_YEAR, PRE_TCJA_YEAR) + np.testing.assert_array_equal(debt[0], [0, 200, 0, 0, 0]) + np.testing.assert_array_equal(debt[1], [0, 0, 300, 400, 0]) + np.testing.assert_array_equal(debt[2], [100, 0, 0, 0, 500]) + np.testing.assert_array_equal(sum(debt), balances) + + +GRID_BALANCES = (0, 400_000, 900_000, 1_200_000) +GRID_YEARS = (0, 1986, 1987, 1988, 2017, 2018) +GRID_STATUSES = ("JOINT", "SEPARATE") +GRID_PERIODS = (2017, 2024) +INTEREST = 60_000.0 + + +def test_simulated_deduction_matches_restatement(): + loan_options = list(product(GRID_BALANCES, GRID_YEARS)) + rng = np.random.default_rng(20261009) + pairs = [ + (loan_options[i], loan_options[j]) + for i, j in rng.integers(0, len(loan_options), size=(150, 2)) + ] + units = list(product(pairs, GRID_STATUSES)) + people, tax_units, households = {}, {}, {} + for k, ((first, second), status) in enumerate(units): + values = { + "first_home_mortgage_balance": first[0], + "first_home_mortgage_origination_year": first[1], + "second_home_mortgage_balance": second[0], + "second_home_mortgage_origination_year": second[1], + "filing_status": status, + } + people[f"p{k}"] = { + "age": {str(p): 50 for p in GRID_PERIODS}, + "home_mortgage_interest": {str(p): INTEREST for p in GRID_PERIODS}, + } + tax_units[f"t{k}"] = { + "members": [f"p{k}"], + **{ + name: {str(p): value for p in GRID_PERIODS} + for name, value in values.items() + }, + } + households[f"h{k}"] = {"members": [f"p{k}"]} + sim = Simulation( + situation={"people": people, "tax_units": tax_units, "households": households} + ) + for period in GRID_PERIODS: + deductible = sim.calculate("deductible_mortgage_interest_tax_unit", period) + non_deductible = sim.calculate( + "non_deductible_mortgage_interest_tax_unit", period + ) + expected = [] + for (first, second), status in units: + key = ("2018+" if period >= 2018 else "pre-2018") + ( + " separate" if status == "SEPARATE" else "" + ) + total = first[0] + second[0] + limit = table_1([first, second], LIMITS[key]) + expected.append(INTEREST * (min(1, limit / total) if total else 1)) + np.testing.assert_allclose(deductible, expected, atol=TOLERANCE) + assert (deductible >= -TOLERANCE).all() + assert (deductible <= INTEREST + TOLERANCE).all() + np.testing.assert_allclose( + deductible + non_deductible, INTEREST, atol=TOLERANCE + ) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml new file mode 100644 index 00000000000..fd8bff90fcc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml @@ -0,0 +1,235 @@ +# Acquisition-debt limits by vintage, 26 U.S.C. 163(h)(3)(B)(ii), (D) and (F), +# computed as in IRS Publication 936 (2025), Table 1 (PDF page 14): +# G = grandfathered debt, incurred on or before October 13, 1987 (line 1) +# P = other debt incurred on or before December 15, 2017 (line 2) +# N = debt incurred after December 15, 2017 (line 7) +# line 6 = min(max(G, $1,000,000), G + P) +# line 11 = min(max(line 6, $750,000), line 6 + N) (qualified loan limit) +# deductible interest = interest x min(1, line 11 / (G + P + N)) +# Limits are halved for married filing separately. Grandfathered debt has no +# dollar limit (Pub. 936 p. 13: "Grandfathered debt isn't limited"). The model +# knows only the origination year, so 1987 counts as on or before +# October 13, 1987 and 2017 as on or before December 15, 2017. +# https://www.irs.gov/pub/irs-prior/p936--2025.pdf#page=13 + +- name: Grandfathered debt above $1,000,000 is fully deductible + period: 2024 + input: + filing_status: JOINT + home_mortgage_interest: 15_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 1986 + output: + # G = 1,500,000: line 6 = min(max(1,500,000, 1,000,000), 1,500,000) + # = 1,500,000 = line 11 = line 12, so all interest is deductible. + deductible_mortgage_interest_tax_unit: 15_000 + non_deductible_mortgage_interest_tax_unit: 0 + +- name: Grandfathered debt is not limited for a separate filer + period: 2024 + input: + filing_status: SEPARATE + home_mortgage_interest: 30_000 + first_home_mortgage_balance: 600_000 + first_home_mortgage_origination_year: 1985 + output: + # The $500,000 separate-return limit does not apply to grandfathered debt. + deductible_mortgage_interest_tax_unit: 30_000 + +- name: Debt from 1987 counts as grandfathered + period: 2024 + input: + filing_status: SINGLE + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 1_200_000 + first_home_mortgage_origination_year: 1987 + output: + # Model convention: only the year is known, and January 1 to October 13 + # is most of 1987, as January 1 to December 15 is most of 2017. + deductible_mortgage_interest_tax_unit: 60_000 + +- name: Debt from 1988 is not grandfathered + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: SINGLE + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 1_200_000 + first_home_mortgage_origination_year: 1988 + output: + # P = 1,200,000: line 6 = min(1,000,000, 1,200,000) = 1,000,000 = line 11. + # 60,000 x 1,000,000 / 1,200,000 = 50,000. + deductible_mortgage_interest_tax_unit: 50_000 + non_deductible_mortgage_interest_tax_unit: 10_000 + +- name: Two grandfathered mortgages are both fully deductible + period: 2024 + input: + filing_status: JOINT + home_mortgage_interest: 52_000 + first_home_mortgage_balance: 700_000 + first_home_mortgage_origination_year: 1984 + second_home_mortgage_balance: 600_000 + second_home_mortgage_origination_year: 1986 + output: + # G = 1,300,000 = line 6 = line 11 = line 12. + deductible_mortgage_interest_tax_unit: 52_000 + +- name: Grandfathered debt reduces the $1,000,000 limit for later pre-2018 debt + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: JOINT + home_mortgage_interest: 65_000 + first_home_mortgage_balance: 600_000 + first_home_mortgage_origination_year: 1986 + second_home_mortgage_balance: 700_000 + second_home_mortgage_origination_year: 2010 + output: + # line 6 = min(max(600,000, 1,000,000), 1,300,000) = 1,000,000 = line 11. + # 65,000 x 1,000,000 / 1,300,000 = 50,000. + deductible_mortgage_interest_tax_unit: 50_000 + +- name: Grandfathered debt above $1,000,000 leaves no limit for later pre-2018 debt + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: JOINT + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 1_200_000 + first_home_mortgage_origination_year: 1985 + second_home_mortgage_balance: 300_000 + second_home_mortgage_origination_year: 2010 + output: + # line 6 = min(max(1,200,000, 1,000,000), 1,500,000) = 1,200,000 = line 11. + # 60,000 x 1,200,000 / 1,500,000 = 48,000. + deductible_mortgage_interest_tax_unit: 48_000 + +- name: Grandfathered debt reduces the $750,000 limit for post-2017 debt + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: JOINT + home_mortgage_interest: 45_000 + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 1986 + second_home_mortgage_balance: 500_000 + second_home_mortgage_origination_year: 2022 + output: + # line 6 = 400,000; line 11 = min(max(400,000, 750,000), 900,000) = 750,000. + # 45,000 x 750,000 / 900,000 = 37,500. + deductible_mortgage_interest_tax_unit: 37_500 + +- name: Grandfathered debt above $750,000 leaves no limit for post-2017 debt + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: JOINT + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 2020 + second_home_mortgage_balance: 1_100_000 + second_home_mortgage_origination_year: 1986 + output: + # line 6 = 1,100,000; line 11 = min(max(1,100,000, 750,000), 1,500,000) + # = 1,100,000. 60,000 x 1,100,000 / 1,500,000 = 44,000. + deductible_mortgage_interest_tax_unit: 44_000 + +- name: Refinanced grandfathered debt with a new home improvement loan + period: 2025 + input: + filing_status: JOINT + home_mortgage_interest: 39_000 + first_home_mortgage_balance: 500_000 + first_home_mortgage_origination_year: 1986 + second_home_mortgage_balance: 150_000 + second_home_mortgage_origination_year: 2025 + output: + # Adapted from Pub. 936 (2025) Example 1, PDF pages 15-16: a 1986 balloon + # mortgage refinanced in 1996 stays grandfathered debt (enter the 1986 + # year), and $150,000 is the average home acquisition debt from a 2025 + # second mortgage. The $500,000 grandfathered balance and the interest are + # illustrative. line 6 = 500,000; line 11 = min(750,000, 650,000) + # = 650,000 = line 12, so all interest is deductible. + deductible_mortgage_interest_tax_unit: 39_000 + +- name: Older debt larger than the $750,000 limit leaves no limit for newer debt (issue 10011) + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: JOINT + home_mortgage_interest: 17_000 + first_home_mortgage_balance: 800_000 + first_home_mortgage_origination_year: 2016 + second_home_mortgage_balance: 900_000 + second_home_mortgage_origination_year: 2020 + output: + # line 6 = 800,000; line 11 = min(max(800,000, 750,000), 1,700,000) + # = 800,000. 17,000 x 800,000 / 1,700,000 = 8,000. + deductible_mortgage_interest_tax_unit: 8_000 + +- name: Mixed-vintage limit does not depend on loan order (issue 10011) + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: JOINT + home_mortgage_interest: 17_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + second_home_mortgage_balance: 800_000 + second_home_mortgage_origination_year: 2016 + output: + deductible_mortgage_interest_tax_unit: 8_000 + +- name: Equal balances with the newer loan first (issue 10011) + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: JOINT + home_mortgage_interest: 62_000 + first_home_mortgage_balance: 775_000 + first_home_mortgage_origination_year: 2018 + second_home_mortgage_balance: 775_000 + second_home_mortgage_origination_year: 2017 + output: + # line 6 = 775,000; line 11 = min(max(775,000, 750,000), 1,550,000) + # = 775,000. 62,000 x 775,000 / 1,550,000 = 31,000. + deductible_mortgage_interest_tax_unit: 31_000 + +- name: Equal balances with the older loan first (issue 10011) + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: JOINT + home_mortgage_interest: 62_000 + first_home_mortgage_balance: 775_000 + first_home_mortgage_origination_year: 2017 + second_home_mortgage_balance: 775_000 + second_home_mortgage_origination_year: 2018 + output: + deductible_mortgage_interest_tax_unit: 31_000 + +- name: Grandfathered debt is not limited before 2018 + period: 2017 + input: + filing_status: JOINT + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 1_200_000 + first_home_mortgage_origination_year: 1985 + output: + # Pub. 936 (2017), PDF page 10: "Grandfathered debt isn't limited." + # Table 1 (PDF page 11): line 6 = min(max(1,200,000, 1,000,000), + # 1,200,000) = 1,200,000 = line 8 (no home equity debt) = line 9. + deductible_mortgage_interest_tax_unit: 60_000 + +- name: Unknown origination year takes the post-2017 limit + period: 2024 + absolute_error_margin: 0.01 + input: + filing_status: SINGLE + home_mortgage_interest: 45_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 0 + output: + # 45,000 x 750,000 / 900,000 = 37,500. + deductible_mortgage_interest_tax_unit: 37_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml index 8289ef0c24c..0bc321928e8 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml @@ -170,3 +170,37 @@ # filing a separate return, so $15,000 of the $40,000 is allowed. deductible_mortgage_interest: 15_000 mt_itemized_deductions_joint: 15_000 + +- name: Mortgage interest on grandfathered pre-October 14, 1987 debt has no cap + period: 2022 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + home_mortgage_interest: 15_000 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + spouse: + is_tax_unit_spouse: true + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + tax_unit: + members: [head, spouse] + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 1986 + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + # 26 U.S.C. 163(h)(3)(D)(i): debt incurred on or before October 13, 1987 + # is acquisition indebtedness without the $1,000,000 limit, so federal + # law allows all $15,000, and so does Montana (2022 Form 2 line 9). + deductible_mortgage_interest: [15_000, 0] + mt_itemized_deductions_joint: [15_000, 0] diff --git a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py index fed7ca75751..cb6275eb5b1 100644 --- a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py +++ b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py @@ -1,29 +1,48 @@ from policyengine_us.model_api import * -def _mortgage_balance_cap( - origination_year, pre_tcja_cap, post_tcja_cap, pre_tcja_origination_year +def _debt_by_vintage( + balances, + origination_years, + grandfathered_origination_year, + pre_tcja_origination_year, ): - # An origination year of 0 means no mortgage; treat as post-TCJA so the - # lower cap applies (harmless because the balance will also be 0). - return where( - (origination_year > 0) & (origination_year <= pre_tcja_origination_year), - pre_tcja_cap, - post_tcja_cap, - ) + """Sum loan balances into IRS Publication 936 Table 1 lines 1, 2 and 7. + + Returns grandfathered debt (incurred on or before October 13, 1987), other + debt incurred on or before December 15, 2017, and later debt. Only the + calendar year is known, so each cutoff year counts as the older vintage. + An origination year of 0 or less is unknown and counts as later debt. + """ + grandfathered_debt = pre_tcja_debt = post_tcja_debt = 0 + for balance, year in zip(balances, origination_years): + grandfathered = (year > 0) & (year <= grandfathered_origination_year) + pre_tcja = (year > grandfathered_origination_year) & ( + year <= pre_tcja_origination_year + ) + grandfathered_debt = grandfathered_debt + where(grandfathered, balance, 0) + pre_tcja_debt = pre_tcja_debt + where(pre_tcja, balance, 0) + post_tcja_debt = post_tcja_debt + where(grandfathered | pre_tcja, 0, balance) + return grandfathered_debt, pre_tcja_debt, post_tcja_debt -def _limited_mortgage_balance(first_balance, second_balance, first_cap, second_cap): - # Under §163(h)(3)(F), pre-TCJA debt keeps the $1M cap and post-TCJA debt - # gets max(0, $750K − pre_TCJA_debt). This is equivalent to computing the - # combined deductible balance as: - # min(larger_cap, max(larger_balance, smaller_cap)) - # where "larger/smaller" refer to the balance sizes (not vintages). - first_is_smaller = first_balance < second_balance - smaller_cap = where(first_is_smaller, first_cap, second_cap) - larger_cap = where(first_is_smaller, second_cap, first_cap) - larger_balance = max_(first_balance, second_balance) - return min_(larger_cap, max_(larger_balance, smaller_cap)) +def _qualified_loan_limit( + grandfathered_debt, pre_tcja_debt, post_tcja_debt, pre_tcja_cap, post_tcja_cap +): + """IRS Publication 936 Table 1, lines 3-6 and 8-11. + + Grandfathered debt has no dollar limit, but it reduces the limit for other + debt (26 U.S.C. 163(h)(3)(D)). Debt incurred on or before December 15, + 2017 keeps the $1,000,000 limit, and reduces the $750,000 limit for later + debt (26 U.S.C. 163(h)(3)(F)). The result never depends on which slot a + loan is in. + """ + # Lines 3-6. + line_6 = min_( + max_(grandfathered_debt, pre_tcja_cap), grandfathered_debt + pre_tcja_debt + ) + # Lines 8-11. + return min_(max_(line_6, post_tcja_cap), line_6 + post_tcja_debt) class first_home_mortgage_balance(Variable): @@ -90,7 +109,11 @@ class first_home_mortgage_origination_year(Variable): label = "First home mortgage origination year" definition_period = YEAR default_value = 0 - documentation = "Calendar year when the first home acquisition mortgage originated." + documentation = ( + "Calendar year when the first home acquisition mortgage originated. The " + "federal debt limits treat 1987 as before October 14, 1987 and 2017 as " + "before December 16, 2017. Enter 0 if unknown." + ) class second_home_mortgage_origination_year(Variable): @@ -100,7 +123,9 @@ class second_home_mortgage_origination_year(Variable): definition_period = YEAR default_value = 0 documentation = ( - "Calendar year when the second home acquisition mortgage originated." + "Calendar year when the second home acquisition mortgage originated. The " + "federal debt limits treat 1987 as before October 14, 1987 and 2017 as " + "before December 16, 2017. Enter 0 if unknown." ) @@ -135,9 +160,17 @@ class deductible_mortgage_interest_tax_unit(Variable): definition_period = YEAR documentation = ( "Federal deductible mortgage interest after applying the statutory " - "acquisition-debt caps to up to two mortgages." + "acquisition-debt caps to up to two mortgages. Grandfathered debt, " + "incurred on or before October 13, 1987, has no dollar limit but " + "reduces the limits for other debt; debt incurred on or before " + "December 15, 2017 keeps the $1,000,000 limit and reduces the $750,000 " + "limit for later debt (IRS Publication 936, Table 1)." + ) + reference = ( + "https://www.law.cornell.edu/uscode/text/26/163#h_3", + # PDF pages 13-14 + "https://www.irs.gov/pub/irs-prior/p936--2025.pdf#page=13", ) - reference = "https://www.law.cornell.edu/uscode/text/26/163" def formula(tax_unit, period, parameters): first_balance = tax_unit("first_home_mortgage_balance", period) @@ -151,24 +184,18 @@ def formula(tax_unit, period, parameters): filing_status = tax_unit("filing_status", period) p = parameters(period).gov.irs.deductions.itemized.interest.mortgage - pre_tcja_cap = p.pre_tcja_cap[filing_status] - post_tcja_cap = p.cap[filing_status] - pre_tcja_origination_year = p.pre_tcja_origination_year - - first_cap = _mortgage_balance_cap( - first_year, - pre_tcja_cap, - post_tcja_cap, - pre_tcja_origination_year, - ) - second_cap = _mortgage_balance_cap( - second_year, - pre_tcja_cap, - post_tcja_cap, - pre_tcja_origination_year, + grandfathered_debt, pre_tcja_debt, post_tcja_debt = _debt_by_vintage( + [first_balance, second_balance], + [first_year, second_year], + p.grandfathered_origination_year, + p.pre_tcja_origination_year, ) - limited_balance = _limited_mortgage_balance( - first_balance, second_balance, first_cap, second_cap + limited_balance = _qualified_loan_limit( + grandfathered_debt, + pre_tcja_debt, + post_tcja_debt, + p.pre_tcja_cap[filing_status], + p.cap[filing_status], ) # When no acquisition-debt balance is provided, assume the mortgage is From 248d68cdb848de6c7b3c5795ff92a009919328d9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 09:08:26 -0400 Subject: [PATCH 2/3] Document refinanced grandfathered debt, move vectorized checks to YAML Address review: the origination-year inputs now say a refinancing that still counts as grandfathered debt takes the original year (163(h)(3)(D) (iii)-(iv)); the Pub. 936 balloon-note case distinguishes 1986 from 1996; the YAML header and property test note the exact ratio (Table 1 line 14 rounds to three decimals); the Simulation grid becomes two vectorized YAML cases covering every filing status in 2017 and 2024; cap.yaml cites (B)(ii) and (F)(i)(II), (IV) and says the reduction stops at zero. Co-Authored-By: Claude Opus 5.5 --- .../itemized/interest/mortgage/cap.yaml | 7 +- .../test_mortgage_debt_limit_invariants.py | 76 +-------- ...deductible_mortgage_interest_tax_unit.yaml | 159 ++++++++++++++++-- .../tax_unit/mortgage_interest_structure.py | 20 ++- 4 files changed, 170 insertions(+), 92 deletions(-) diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/cap.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/cap.yaml index 125ed83991b..64179a1edd3 100644 --- a/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/cap.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/interest/mortgage/cap.yaml @@ -1,4 +1,4 @@ -description: The IRS allows an itemized deduction for interest paid on home acquisition debt incurred after December 15, 2017, up to this mortgage balance less any earlier debt, based on filing status. +description: The IRS allows an itemized deduction for interest paid on home acquisition debt incurred after December 15, 2017, up to this mortgage balance reduced, but not below zero, by earlier debt treated as acquisition debt, based on filing status. SINGLE: 1900-01-01: 1_000_000 2018-01-01: 750_000 @@ -24,6 +24,5 @@ metadata: # OBBB extends TCJA limits on mortgage interest deduction. - title: H.R.1 - One Big Beautiful Bill Act href: https://www.congress.gov/bill/119th-congress/house-bill/1/text - # TCJA adjustments described in (h)(3)(F)(i)(II) - - title: 26 U.S. Code § 163 - Interest, (h)(3)(b)(ii) - href: https://www.law.cornell.edu/uscode/text/26/163 + - title: 26 U.S. Code § 163 - Interest, (h)(3)(B)(ii) and (F)(i)(II), (IV) + href: https://www.law.cornell.edu/uscode/text/26/163#h_3 diff --git a/policyengine_us/tests/core/test_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_mortgage_debt_limit_invariants.py index 280d988754d..49140fd9527 100644 --- a/policyengine_us/tests/core/test_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_mortgage_debt_limit_invariants.py @@ -15,7 +15,8 @@ limit in that order, reducing each limit by grandfathered debt once ("The limits above are reduced (but not below zero) by the amount of your grandfathered debt", PDF page 12). Interest is deductible in the ratio of the -limit to the total balance (lines 12-15). +limit to the total balance (lines 12-15); the model uses the exact ratio, where +line 14 rounds it to three decimals. The model stores two loans per tax unit with a balance and an origination year. Hypothesis draws pairs of loans around every cutoff and limit and checks @@ -29,25 +30,20 @@ 3. Order: swapping the two loans never changes the limit. 4. Monotone: a larger balance, or an older vintage, never lowers the limit. -One vectorized simulation then checks a seeded grid of tax units in 2017 and -2024 end to end: the deductible interest equals the restatement, lies between -0 and the interest paid, and with the non-deductible interest adds up to the -interest paid. The YAML cases in -deductible_mortgage_interest_tax_unit.yaml check single households; this grid -checks the parameter lookups for mixed filing statuses in one vectorized run. +These are for-all properties of the helpers, which YAML cannot state. The +policy outcomes, parameter lookups for every filing status, and the interest +proration and accounting are checked in deductible_mortgage_interest_tax_unit.yaml, +including vectorized runs that mix filing statuses and vintages. Cutoff years are written here, not read from the parameters: only the year is known, so 1987 counts as on or before October 13, 1987 and 2017 as on or before December 15, 2017. An unknown year (0) counts as post-2017 debt. """ -from itertools import product - import numpy as np from hypothesis import given, settings from hypothesis import strategies as st -from policyengine_us import Simulation from policyengine_us.variables.household.expense.tax_unit.mortgage_interest_structure import ( _debt_by_vintage, _qualified_loan_limit, @@ -173,63 +169,3 @@ def test_vintage_split_conserves_debt(): np.testing.assert_array_equal(debt[1], [0, 0, 300, 400, 0]) np.testing.assert_array_equal(debt[2], [100, 0, 0, 0, 500]) np.testing.assert_array_equal(sum(debt), balances) - - -GRID_BALANCES = (0, 400_000, 900_000, 1_200_000) -GRID_YEARS = (0, 1986, 1987, 1988, 2017, 2018) -GRID_STATUSES = ("JOINT", "SEPARATE") -GRID_PERIODS = (2017, 2024) -INTEREST = 60_000.0 - - -def test_simulated_deduction_matches_restatement(): - loan_options = list(product(GRID_BALANCES, GRID_YEARS)) - rng = np.random.default_rng(20261009) - pairs = [ - (loan_options[i], loan_options[j]) - for i, j in rng.integers(0, len(loan_options), size=(150, 2)) - ] - units = list(product(pairs, GRID_STATUSES)) - people, tax_units, households = {}, {}, {} - for k, ((first, second), status) in enumerate(units): - values = { - "first_home_mortgage_balance": first[0], - "first_home_mortgage_origination_year": first[1], - "second_home_mortgage_balance": second[0], - "second_home_mortgage_origination_year": second[1], - "filing_status": status, - } - people[f"p{k}"] = { - "age": {str(p): 50 for p in GRID_PERIODS}, - "home_mortgage_interest": {str(p): INTEREST for p in GRID_PERIODS}, - } - tax_units[f"t{k}"] = { - "members": [f"p{k}"], - **{ - name: {str(p): value for p in GRID_PERIODS} - for name, value in values.items() - }, - } - households[f"h{k}"] = {"members": [f"p{k}"]} - sim = Simulation( - situation={"people": people, "tax_units": tax_units, "households": households} - ) - for period in GRID_PERIODS: - deductible = sim.calculate("deductible_mortgage_interest_tax_unit", period) - non_deductible = sim.calculate( - "non_deductible_mortgage_interest_tax_unit", period - ) - expected = [] - for (first, second), status in units: - key = ("2018+" if period >= 2018 else "pre-2018") + ( - " separate" if status == "SEPARATE" else "" - ) - total = first[0] + second[0] - limit = table_1([first, second], LIMITS[key]) - expected.append(INTEREST * (min(1, limit / total) if total else 1)) - np.testing.assert_allclose(deductible, expected, atol=TOLERANCE) - assert (deductible >= -TOLERANCE).all() - assert (deductible <= INTEREST + TOLERANCE).all() - np.testing.assert_allclose( - deductible + non_deductible, INTEREST, atol=TOLERANCE - ) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml index fd8bff90fcc..390d6c8f111 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml @@ -6,6 +6,8 @@ # line 6 = min(max(G, $1,000,000), G + P) # line 11 = min(max(line 6, $750,000), line 6 + N) (qualified loan limit) # deductible interest = interest x min(1, line 11 / (G + P + N)) +# The model prorates by the exact ratio; Table 1 line 14 rounds it to three +# decimals, which would move some amounts below by up to $20. # Limits are halved for married filing separately. Grandfathered debt has no # dollar limit (Pub. 936 p. 13: "Grandfathered debt isn't limited"). The model # knows only the origination year, so 1987 counts as on or before @@ -135,23 +137,20 @@ # = 1,100,000. 60,000 x 1,100,000 / 1,500,000 = 44,000. deductible_mortgage_interest_tax_unit: 44_000 -- name: Refinanced grandfathered debt with a new home improvement loan +- name: Refinanced grandfathered balloon loan keeps the original year period: 2025 input: filing_status: JOINT - home_mortgage_interest: 39_000 - first_home_mortgage_balance: 500_000 + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 1_200_000 first_home_mortgage_origination_year: 1986 - second_home_mortgage_balance: 150_000 - second_home_mortgage_origination_year: 2025 output: - # Adapted from Pub. 936 (2025) Example 1, PDF pages 15-16: a 1986 balloon - # mortgage refinanced in 1996 stays grandfathered debt (enter the 1986 - # year), and $150,000 is the average home acquisition debt from a 2025 - # second mortgage. The $500,000 grandfathered balance and the interest are - # illustrative. line 6 = 500,000; line 11 = min(750,000, 650,000) - # = 650,000 = line 12, so all interest is deductible. - deductible_mortgage_interest_tax_unit: 39_000 + # Pub. 936 (2025) p. 13 example, with a larger balance: a 1986 ten-year + # balloon note refinanced in 1996 with a 30-year mortgage stays + # grandfathered debt for that whole term (163(h)(3)(D)(iii)(II), (iv)(II)), + # so the 1986 year is entered and all interest is deductible. Entering the + # 1996 refinancing year would cap it at 60,000 x 1,000,000 / 1,200,000. + deductible_mortgage_interest_tax_unit: 60_000 - name: Older debt larger than the $750,000 limit leaves no limit for newer debt (issue 10011) period: 2024 @@ -233,3 +232,139 @@ output: # 45,000 x 750,000 / 900,000 = 37,500. deductible_mortgage_interest_tax_unit: 37_500 + +- name: Mixed filing statuses and vintages in one vectorized run + period: 2024 + absolute_error_margin: 0.01 + input: + people: + p1: + home_mortgage_interest: 15_000 + p2: + home_mortgage_interest: 25_000 + p3: + home_mortgage_interest: 17_000 + p4: + home_mortgage_interest: 62_000 + p5: + home_mortgage_interest: 60_000 + p6: + home_mortgage_interest: 40_000 + tax_units: + t1: + members: [p1] + filing_status: JOINT + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 1986 + t2: + members: [p2] + filing_status: SEPARATE + first_home_mortgage_balance: 500_000 + first_home_mortgage_origination_year: 2018 + t3: + members: [p3] + filing_status: SINGLE + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + second_home_mortgage_balance: 800_000 + second_home_mortgage_origination_year: 2016 + t4: + members: [p4] + filing_status: HEAD_OF_HOUSEHOLD + first_home_mortgage_balance: 775_000 + first_home_mortgage_origination_year: 2018 + second_home_mortgage_balance: 775_000 + second_home_mortgage_origination_year: 2017 + t5: + members: [p5] + filing_status: SURVIVING_SPOUSE + first_home_mortgage_balance: 300_000 + first_home_mortgage_origination_year: 2010 + second_home_mortgage_balance: 1_200_000 + second_home_mortgage_origination_year: 1985 + t6: + members: [p6] + filing_status: SEPARATE + first_home_mortgage_balance: 600_000 + first_home_mortgage_origination_year: 1985 + second_home_mortgage_balance: 200_000 + second_home_mortgage_origination_year: 2020 + households: + h1: + members: [p1] + h2: + members: [p2] + h3: + members: [p3] + h4: + members: [p4] + h5: + members: [p5] + h6: + members: [p6] + output: + # t1: G = 1,500,000, no limit. t2: 25,000 x 375,000 / 500,000. + # t3: line 11 = 800,000; 17,000 x 800,000 / 1,700,000. + # t4: line 11 = 775,000; 62,000 x 775,000 / 1,550,000. + # t5: line 6 = 1,200,000 = line 11; 60,000 x 1,200,000 / 1,500,000. + # t6: separate limits $500,000 / $375,000; line 6 = min(max(600,000, + # 500,000), 600,000) = 600,000 = line 11; 40,000 x 600,000 / 800,000. + # Deductible plus non-deductible interest equals interest paid. + deductible_mortgage_interest_tax_unit: [15_000, 18_750, 8_000, 31_000, 48_000, 30_000] + non_deductible_mortgage_interest_tax_unit: [0, 6_250, 9_000, 31_000, 12_000, 10_000] + +- name: Mixed filing statuses and vintages before 2018 + period: 2017 + absolute_error_margin: 0.01 + input: + people: + p1: + home_mortgage_interest: 60_000 + p2: + home_mortgage_interest: 35_000 + p3: + home_mortgage_interest: 35_000 + p4: + home_mortgage_interest: 45_000 + tax_units: + t1: + members: [p1] + filing_status: JOINT + first_home_mortgage_balance: 1_200_000 + first_home_mortgage_origination_year: 1985 + t2: + members: [p2] + filing_status: SEPARATE + first_home_mortgage_balance: 700_000 + first_home_mortgage_origination_year: 1986 + t3: + members: [p3] + filing_status: SINGLE + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 2010 + second_home_mortgage_balance: 300_000 + second_home_mortgage_origination_year: 2016 + t4: + members: [p4] + filing_status: JOINT + first_home_mortgage_balance: 300_000 + first_home_mortgage_origination_year: 2012 + second_home_mortgage_balance: 600_000 + second_home_mortgage_origination_year: 1986 + households: + h1: + members: [p1] + h2: + members: [p2] + h3: + members: [p3] + h4: + members: [p4] + output: + # 2017 Pub. 936, PDF pp. 10-11: grandfathered debt has no limit (t1, and + # t2 above the $500,000 separate limit); t3 and t4 total 700,000 and + # 900,000, within $1,000,000 less grandfathered debt. These cases stay + # within the acquisition-debt limit for non-grandfathered debt, because + # the pre-2018 home equity allowance is not modeled. + deductible_mortgage_interest_tax_unit: [60_000, 35_000, 35_000, 45_000] + non_deductible_mortgage_interest_tax_unit: [0, 0, 0, 0] diff --git a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py index cb6275eb5b1..6f21c1b4342 100644 --- a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py +++ b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py @@ -103,6 +103,16 @@ class second_home_mortgage_interest(Variable): ) +_ORIGINATION_YEAR_CONVENTION = ( + "The federal debt limits treat 1987 as on or before October 13, 1987 and " + "2017 as on or before December 15, 2017. A refinancing that still counts as " + "grandfathered debt takes the original debt's year: up to the principal " + "refinanced, and only for the original term or, for a balloon loan, the " + "first refinancing's term up to 30 years (26 U.S.C. 163(h)(3)(D)(iii)-(iv)). " + "Enter 0 if unknown." +) + + class first_home_mortgage_origination_year(Variable): value_type = int entity = TaxUnit @@ -110,9 +120,8 @@ class first_home_mortgage_origination_year(Variable): definition_period = YEAR default_value = 0 documentation = ( - "Calendar year when the first home acquisition mortgage originated. The " - "federal debt limits treat 1987 as before October 14, 1987 and 2017 as " - "before December 16, 2017. Enter 0 if unknown." + "Calendar year when the first home acquisition mortgage originated. " + + _ORIGINATION_YEAR_CONVENTION ) @@ -123,9 +132,8 @@ class second_home_mortgage_origination_year(Variable): definition_period = YEAR default_value = 0 documentation = ( - "Calendar year when the second home acquisition mortgage originated. The " - "federal debt limits treat 1987 as before October 14, 1987 and 2017 as " - "before December 16, 2017. Enter 0 if unknown." + "Calendar year when the second home acquisition mortgage originated. " + + _ORIGINATION_YEAR_CONVENTION ) From 8929b0d7096160e48c1e1787ccfe9a912f0454f9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 09:33:44 -0400 Subject: [PATCH 3/3] Correct the 2017 vectorized case comment Co-Authored-By: Claude Opus 5.5 --- .../deductions/deductible_mortgage_interest_tax_unit.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml index 390d6c8f111..4515312e17e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest_tax_unit.yaml @@ -362,8 +362,9 @@ members: [p4] output: # 2017 Pub. 936, PDF pp. 10-11: grandfathered debt has no limit (t1, and - # t2 above the $500,000 separate limit); t3 and t4 total 700,000 and - # 900,000, within $1,000,000 less grandfathered debt. These cases stay + # t2 above the $500,000 separate limit). t3's 700,000 is within + # $1,000,000; t4's 300,000 of 2012 debt is within $1,000,000 less its + # 600,000 of grandfathered debt. These cases stay # within the acquisition-debt limit for non-grandfathered debt, because # the pre-2018 home equity allowance is not modeled. deductible_mortgage_interest_tax_unit: [60_000, 35_000, 35_000, 45_000]