From e58f36ed3abd81270d0354f84829284254e055bd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:03:39 +0000 Subject: [PATCH 1/9] Apply New York's pre-TCJA mortgage debt limits from 2018 N.Y. Tax Law 615(a) computes New York itemized deductions as they existed before P.L. 115-97, but ny_itemized_deductions_max added the federal interest_deduction, so post-2017 loans got the federal $750,000 limit and no home equity debt. A new ny_mortgage_interest_deduction applies the preserved $1,000,000 / $100,000 limits (halved for separate returns) with per-home fair market value room, and ny_interest_deduction substitutes it for the federal mortgage component. New inputs default to values that add no home equity debt. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6J5LH2sLC5oX6pUHnC --- .../ny-pre-tcja-mortgage-interest.fixed.md | 1 + .../interest/home_acquisition_debt_limit.yaml | 29 +++ .../interest/home_equity_debt_limit.yaml | 29 +++ .../test_ny_mortgage_debt_limit_invariants.py | 227 ++++++++++++++++++ .../ny_mortgage_interest_deduction.yaml | 208 ++++++++++++++++ .../itemized/ny_interest_deduction.py | 25 ++ .../itemized/ny_itemized_deductions_max.py | 5 +- .../ny_mortgage_interest_deduction.py | 182 ++++++++++++++ .../itemized/ny_mortgage_interest_inputs.py | 80 ++++++ .../ny_itemized_deductions_phase_out.py | 2 +- 10 files changed, 786 insertions(+), 2 deletions(-) create mode 100644 changelog.d/ny-pre-tcja-mortgage-interest.fixed.md create mode 100644 policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml create mode 100644 policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml create mode 100644 policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py create mode 100644 policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py create mode 100644 policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py diff --git a/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md b/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md new file mode 100644 index 00000000000..b9181cd44d1 --- /dev/null +++ b/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md @@ -0,0 +1 @@ +Apply New York's pre-TCJA mortgage debt limits ($1,000,000 acquisition and $100,000 home equity debt) to its itemized interest deduction from 2018, with new home value and non-acquisition debt inputs. diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml new file mode 100644 index 00000000000..02e2f769e7e --- /dev/null +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml @@ -0,0 +1,29 @@ +description: New York limits the home acquisition debt on which mortgage interest is deductible to this amount, reduced by any grandfathered debt, based on filing status. +SINGLE: + 2018-01-01: 1_000_000 +JOINT: + 2018-01-01: 1_000_000 +SEPARATE: + 2018-01-01: 500_000 +HEAD_OF_HOUSEHOLD: + 2018-01-01: 1_000_000 +SURVIVING_SPOUSE: + 2018-01-01: 1_000_000 +metadata: + propagate_metadata_to_children: true + breakdown: filing_status + unit: currency-USD + period: year + label: New York home acquisition debt limit + reference: + # N.Y. Tax Law 615(a) computes New York itemized deductions as they + # existed immediately before P.L. 115-97, so the IRC 163(h)(3)(B)(ii) + # limit applies rather than the IRC 163(h)(3)(F) limit for later debt. + - title: New York Tax Law 615(a) + href: https://www.nysenate.gov/legislation/laws/TAX/615 + - title: "L.2018, chapter 59 (S7509C), Part JJ, sections 3 and 7 #page 42-43" + href: https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42 + - title: 26 U.S. Code 163(h)(3)(B)(ii) (2017 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm + - title: New York State Department of Taxation and Finance, Itemized deductions (2025), Interest you paid + href: https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml new file mode 100644 index 00000000000..edf2e20e399 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml @@ -0,0 +1,29 @@ +description: New York limits the home equity debt on which mortgage interest is deductible to this amount, based on filing status. +SINGLE: + 2018-01-01: 100_000 +JOINT: + 2018-01-01: 100_000 +SEPARATE: + 2018-01-01: 50_000 +HEAD_OF_HOUSEHOLD: + 2018-01-01: 100_000 +SURVIVING_SPOUSE: + 2018-01-01: 100_000 +metadata: + propagate_metadata_to_children: true + breakdown: filing_status + unit: currency-USD + period: year + label: New York home equity debt limit + reference: + # N.Y. Tax Law 615(a) computes New York itemized deductions as they + # existed immediately before P.L. 115-97, so the IRC 163(h)(3)(C)(ii) + # limit applies and IRC 163(h)(3)(F) does not suspend it. + - title: New York Tax Law 615(a) + href: https://www.nysenate.gov/legislation/laws/TAX/615 + - title: "L.2018, chapter 59 (S7509C), Part JJ, sections 3 and 7 #page 42-43" + href: https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42 + - title: 26 U.S. Code 163(h)(3)(C)(ii) (2017 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm + - title: "2017 IRS Publication 936, Home equity debt limit and Table 1 #page 10-11" + href: https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=10 diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py new file mode 100644 index 00000000000..8f7b21d845a --- /dev/null +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -0,0 +1,227 @@ +"""Invariants for New York's pre-TCJA mortgage debt limits. + +N.Y. Tax Law 615(a) computes New York itemized deductions as they existed +immediately before P.L. 115-97, so from 2018 New York keeps the IRC +163(h)(3)(B)-(D) limits: grandfathered debt, incurred on or before October +13, 1987, has no dollar limit and reduces the $1,000,000 acquisition-debt +limit ($500,000 married filing separately); acquisition debt above that limit +and non-acquisition debt count as home equity debt up to $100,000 ($50,000), +and on each home only up to its fair market value less the debt on it +treated as acquisition or grandfathered debt. + +Hypothesis draws two homes, each with an acquisition balance and vintage, +non-acquisition debt and a fair market value, around every cutoff and limit, +and checks _ny_qualified_loan_limit: + +1. Differential: it equals an independent restatement in the statute's + order, which places the acquisition limit on each home by a separate loop. +2. Bounds: grandfathered debt <= New York limit <= total debt, and the New + York limit is at least PE's federal limit for the same acquisition loans. +3. Defaults: with no fair market value and no non-acquisition debt (the + input defaults), no debt counts as home equity debt. +4. Order: swapping the two homes never changes the limit. +5. Monotone: a higher fair market value never lowers the limit. + +And for _ny_allowed_interest: with no non-acquisition debt the result lies +between the federal deduction and the interest paid, and equals the federal +deduction when federal law disallows nothing. + +The limits are read from the New York and federal parameters for every +filing status in 2018, 2025 and 2026. The properties cannot pass trivially: +the differential compares against a separately written per-home calculation, +and the default and bound checks fail if the fair market value test, the +home equity limit or the acquisition limit is dropped or reversed. + +These are for-all properties of the helpers, which YAML cannot state. The +policy outcomes are checked in ny_mortgage_interest_deduction.yaml. +""" + +import numpy as np +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_us.system import system +from policyengine_us.variables.gov.states.ny.tax.income.taxable_income.deductions.itemized.ny_mortgage_interest_deduction import ( + _ny_allowed_interest, + _ny_deductible_share, + _ny_qualified_loan_limit, +) +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 +FILING_STATUSES = ( + "SINGLE", + "JOINT", + "SEPARATE", + "HEAD_OF_HOUSEHOLD", + "SURVIVING_SPOUSE", +) +TAX_YEARS = (2018, 2025, 2026) + + +def parameter_limits(year, filing_status): + p = system.parameters(f"{year}-01-01") + ny = p.gov.states.ny.tax.income.deductions.itemized.interest + federal = p.gov.irs.deductions.itemized.interest.mortgage + return ( + ( + float(ny.home_acquisition_debt_limit[filing_status]), + float(ny.home_equity_debt_limit[filing_status]), + ), + ( + float(federal.pre_tcja_cap[filing_status]), + float(federal.cap[filing_status]), + ), + ) + + +LIMITS = { + (year, filing_status): parameter_limits(year, filing_status) + for year in TAX_YEARS + for filing_status in FILING_STATUSES +} +TOLERANCE = 0.01 + +YEARS = (0, 1960, 1987, 1988, 2017, 2018, 2025) +BOUNDARY_AMOUNTS = ( + 0, + 1, + 50_000, + 100_000, + 375_000, + 500_000, + 550_000, + 750_000, + 1_000_000, + 1_100_000, +) +amounts = st.one_of( + st.sampled_from(BOUNDARY_AMOUNTS), + st.integers(0, 3_000_000), +).map(float) +# (acquisition balance, origination year, non-acquisition debt, fair market value) +homes = st.tuples(amounts, st.sampled_from(YEARS), amounts, amounts) + + +def grandfathered(year): + return 0 < year <= GRANDFATHERED_YEAR + + +def model_limit(pair, limits): + grandfathered_debts = [ + np.array([balance if grandfathered(year) else 0.0]) + for balance, year, _, _ in pair + ] + other_debts = [ + np.array([0.0 if grandfathered(year) else balance]) + for balance, year, _, _ in pair + ] + return float( + _ny_qualified_loan_limit( + grandfathered_debts, + other_debts, + [np.array([home[2]]) for home in pair], + [np.array([home[3]]) for home in pair], + *limits, + )[0] + ) + + +def statute(pair, limits): + """IRC 163(h)(3)(B)(ii), (C) and (D) before P.L. 115-97, home by home.""" + acquisition_limit, home_equity_limit = limits + old = sum(balance for balance, year, _, _ in pair if grandfathered(year)) + other = sum(balance for balance, year, _, _ in pair if not grandfathered(year)) + # (D)(ii): grandfathered debt reduces the $1,000,000 limit. + other_allowed = min(other, max(0.0, acquisition_limit - old)) + home_equity = 0.0 + for balance, year, non_acquisition, fair_market_value in pair: + if grandfathered(year): + on_home, excess = balance, 0.0 + else: + on_home = other_allowed * balance / other if other > 0 else 0.0 + excess = balance - on_home + # (C)(i): no more than fair market value less acquisition debt. + room = max(0.0, fair_market_value - on_home) + home_equity += min(excess + non_acquisition, room) + # (C)(ii): the aggregate home equity limit. + return old + other_allowed + min(home_equity, home_equity_limit) + + +def federal_limit(pair, limits): + debt = _debt_by_vintage( + [np.array([home[0]]) for home in pair], + [np.array([home[1]]) for home in pair], + GRANDFATHERED_YEAR, + PRE_TCJA_YEAR, + ) + return float(_qualified_loan_limit(*debt, *limits)[0]) + + +SETTINGS = dict(max_examples=400, deadline=None, derandomize=True) + + +def test_new_york_limits_are_the_statutory_amounts(): + for (year, filing_status), (ny_limits, _) in LIMITS.items(): + expected = ( + (500_000, 50_000) if filing_status == "SEPARATE" else (1_000_000, 100_000) + ) + assert ny_limits == expected, (year, filing_status) + + +@settings(**SETTINGS) +@given(first=homes, second=homes, status=st.sampled_from(sorted(LIMITS))) +def test_new_york_qualified_loan_limit_properties(first, second, status): + ny_limits, federal_limits = LIMITS[status] + pair = [first, second] + limit = model_limit(pair, ny_limits) + old = sum(home[0] for home in pair if grandfathered(home[1])) + total = sum(home[0] + home[2] for home in pair) + + # 1. Differential. + assert abs(limit - statute(pair, ny_limits)) <= TOLERANCE + # 2. Bounds. + assert old - TOLERANCE <= limit <= total + TOLERANCE + assert limit >= federal_limit(pair, federal_limits) - TOLERANCE + # 3. Defaults: no fair market value and no non-acquisition debt. + defaults = [(home[0], home[1], 0.0, 0.0) for home in pair] + acquisition = sum(home[0] for home in pair) + assert ( + abs(model_limit(defaults, ny_limits) - min(max(old, ny_limits[0]), acquisition)) + <= TOLERANCE + ) + # 4. Order. + assert abs(limit - model_limit([second, first], ny_limits)) <= TOLERANCE + # 5. Monotone in fair market value. + richer = [(home[0], home[1], home[2], home[3] + 250_000) for home in pair] + assert model_limit(richer, ny_limits) >= limit - TOLERANCE + + +@settings(**SETTINGS) +@given( + deductible=amounts, + non_deductible=amounts, + limit=amounts, + debt=amounts, +) +def test_new_york_allowed_interest_without_non_acquisition_debt( + deductible, non_deductible, limit, debt +): + share = _ny_deductible_share(np.array([min(limit, debt)]), np.array([debt])) + allowed = float( + _ny_allowed_interest( + np.array([deductible]), + np.array([non_deductible]), + np.array([0.0]), + share, + )[0] + ) + assert 0 <= share[0] <= 1 + assert deductible - TOLERANCE <= allowed + assert allowed <= deductible + non_deductible + TOLERANCE + if non_deductible == 0: + assert allowed == deductible diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml new file mode 100644 index 00000000000..779833a3c44 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml @@ -0,0 +1,208 @@ +# N.Y. Tax Law 615(a) computes New York itemized deductions as they existed +# immediately before P.L. 115-97, so from 2018 New York keeps the pre-TCJA +# mortgage limits of IRC 163(h)(3)(B)-(D). IRS Publication 936 (2017), +# Table 1 (PDF page 11): +# G = grandfathered debt, 1987 or earlier; A = other acquisition debt +# line 6 = min(max(G, $1,000,000), G + A) +# line 7 = min(home equity debt, $100,000), where home equity debt is the +# acquisition debt above line 6 plus non-acquisition debt, limited +# on each home to its fair market value less the debt on it +# counted in line 6 +# line 8 = line 6 + line 7 +# line 11 = line 8 / total debt, rounded to three places +# line 12 = interest x line 11 +# Limits are halved for married filing separately. PolicyEngine's federal +# deduction uses the $750,000 limit for debt from 2018 or later, no home +# equity debt and the exact ratio. + +- name: New York keeps the $1,000,000 acquisition-debt limit + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 36_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 1_200_000 + ny_first_home_non_acquisition_debt: 0 + output: + # Federal: $36,000 x $750,000 / $900,000 = $30,000. + deductible_mortgage_interest_tax_unit: 30_000 + # New York: the $1,000,000 limit covers all $900,000. Main allowed the + # federal $30,000. + ny_mortgage_interest_deduction: 36_000 + ny_interest_deduction: 36_000 + ny_itemized_deductions_max: 36_000 + +- name: Acquisition debt above the limit counts as home equity debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 2_000_000 + ny_first_home_non_acquisition_debt: 0 + output: + # Federal: $40,000 x $750,000 / $1,500,000 = $20,000. + deductible_mortgage_interest_tax_unit: 20_000 + # New York: ($1,000,000 + $100,000) / $1,500,000 = .733; $40,000 x .733. + ny_mortgage_interest_deduction: 29_320 + +- name: Separate returns halve both limits + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SEPARATE + home_mortgage_interest: 20_000 + first_home_mortgage_balance: 600_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 800_000 + ny_first_home_non_acquisition_debt: 0 + output: + # Federal: $20,000 x $375,000 / $600,000 = $12,500. + deductible_mortgage_interest_tax_unit: 12_500 + # New York: ($500,000 + $50,000) / $600,000 = .917; $20,000 x .917. + ny_mortgage_interest_deduction: 18_340 + +- name: Without a fair market value no debt counts as home equity debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + output: + deductible_mortgage_interest_tax_unit: 20_000 + # New York: $1,000,000 / $1,500,000 = .667; $40,000 x .667. + ny_mortgage_interest_deduction: 26_680 + +- name: Fair market value limits home equity debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 1_040_000 + output: + # Room for home equity debt: $1,040,000 - $1,000,000 = $40,000. + # New York: $1,040,000 / $1,500,000 = .693; $40,000 x .693. + ny_mortgage_interest_deduction: 27_720 + +- name: Non-acquisition debt counts as home equity debt up to the limit + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 35_000 + first_home_mortgage_balance: 500_000 + first_home_mortgage_origination_year: 2015 + ny_first_home_fair_market_value: 1_000_000 + ny_first_home_non_acquisition_debt: 200_000 + output: + # PE's federal calculation does not see the non-acquisition debt. + deductible_mortgage_interest_tax_unit: 35_000 + # New York: ($500,000 + $100,000) / $700,000 = .857; $35,000 x .857. + ny_mortgage_interest_deduction: 29_995 + +- name: Grandfathered debt reduces the acquisition-debt limit + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 50_000 + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 1986 + second_home_mortgage_balance: 800_000 + second_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 500_000 + ny_second_home_fair_market_value: 900_000 + output: + # Federal: line 6 = $400,000 of grandfathered debt; the later debt + # raises the limit only to $750,000; $50,000 x $750,000 / $1,200,000. + deductible_mortgage_interest_tax_unit: 31_250 + # New York: line 6 = max($400,000, $1,000,000) = $1,000,000, of which + # $600,000 is the second home's; its other $200,000 is home equity debt, + # within its room of $900,000 - $600,000 = $300,000 and limited to + # $100,000. ($1,000,000 + $100,000) / $1,200,000 = .917; $50,000 x .917. + ny_mortgage_interest_deduction: 45_850 + +- name: Each home's equity room limits its own excess debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + second_home_mortgage_balance: 600_000 + second_home_mortgage_origination_year: 2021 + ny_first_home_fair_market_value: 3_000_000 + ny_second_home_fair_market_value: 400_000 + output: + # Line 6 = $1,000,000, shared 9:6: $600,000 on the first home and + # $400,000 on the second. The first home's other $300,000 fits in its + # room; the second home has no room, so its other $200,000 does not + # count. Line 7 = min($300,000, $100,000). + # ($1,000,000 + $100,000) / $1,500,000 = .733; $60,000 x .733. + ny_mortgage_interest_deduction: 43_980 + +- name: A directly supplied deduction passes through when nothing is disallowed + period: 2025 + input: + state_code: NY + deductible_mortgage_interest: 12_000 + output: + ny_mortgage_interest_deduction: 12_000 + +- name: Before 2018 New York uses the federal amount + period: 2017 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2010 + ny_first_home_fair_market_value: 2_000_000 + output: + # Federal: $40,000 x $1,000,000 / $1,500,000. + deductible_mortgage_interest_tax_unit: 26_666.67 + ny_mortgage_interest_deduction: 26_666.67 + +- name: Rounding cannot take New York below the federal amount + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 1_200_000 + first_home_mortgage_origination_year: 2015 + output: + # Federal: $60,000 x $1,000,000 / $1,200,000 = $50,000. The worksheet + # gives $60,000 x .833 = $49,980; PE keeps the federal $50,000. + deductible_mortgage_interest_tax_unit: 50_000 + ny_mortgage_interest_deduction: 50_000 + +- name: Mortgage interest outside New York is zero + period: 2025 + input: + state_code: NJ + home_mortgage_interest: 36_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + output: + ny_mortgage_interest_deduction: 0 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py new file mode 100644 index 00000000000..a30c7ce2626 --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class ny_interest_deduction(Variable): + value_type = float + entity = TaxUnit + label = "New York interest deduction" + unit = USD + definition_period = YEAR + documentation = ( + "Interest New York allows as an itemized deduction: the federal " + "interest_deduction components, with New York's pre-TCJA home " + "mortgage interest in place of the federal home mortgage interest." + ) + reference = ( + "https://www.nysenate.gov/legislation/laws/TAX/615", + "https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest", + ) + defined_for = StateCode.NY + + adds = [ + "ny_mortgage_interest_deduction", + "non_mortgage_interest", + "deductible_mortgage_insurance_premiums", + ] diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_max.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_max.py index c217b06ddea..0b23e8bd25f 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_max.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_max.py @@ -21,12 +21,15 @@ class ny_itemized_deductions_max(Variable): - Medical expenses deductible above 10% of AGI (not the 7.5% floor) - Miscellaneous deductions still allowed with 2% AGI floor - Casualty losses not limited to federally declared disasters + - Mortgage interest under the pre-TCJA acquisition and home equity + debt limits (ny_interest_deduction) - NY-specific college tuition deduction addition per ยง 615(d) """ adds = [ # Federal deductions that don't differ from pre-TCJA "charitable_deduction", - "interest_deduction", + # Federal interest components with NY's pre-TCJA mortgage interest + "ny_interest_deduction", # NY-specific pre-TCJA versions "ny_medical_expense_deduction", "ny_salt_deduction", diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py new file mode 100644 index 00000000000..925176dde51 --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py @@ -0,0 +1,182 @@ +from policyengine_us.model_api import * + + +def _ny_qualified_loan_limit( + grandfathered_debts, + other_debts, + non_acquisition_debts, + fair_market_values, + home_acquisition_debt_limit, + home_equity_debt_limit, +): + """Pre-TCJA IRC 163(h)(3)(B)-(D), which N.Y. Tax Law 615(a) preserves. + + Each argument except the limits is a list with one array per home. + + Grandfathered debt, incurred on or before October 13, 1987, has no dollar + limit but reduces the acquisition-debt limit for other acquisition debt + (2017 Publication 936, Table 1, lines 1-6). Acquisition debt above that + limit, and debt that was never acquisition debt, may qualify as home + equity debt (Figure A, note 3). On each home, home equity debt cannot + exceed the home's fair market value less the debt on it treated as + acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)); across homes it + cannot exceed the home equity debt limit (line 7). + + The limit on other acquisition debt is shared among homes in proportion + to each home's other acquisition debt; the statute's aggregate limit does + not say how to place it on a particular home. + """ + grandfathered_debt = sum(grandfathered_debts) + other_debt = sum(other_debts) + acquisition_debt = grandfathered_debt + other_debt + # Lines 3-6: debt treated as acquisition or grandfathered debt. + line_6 = min_( + max_(grandfathered_debt, home_acquisition_debt_limit), acquisition_debt + ) + other_debt_treated = line_6 - grandfathered_debt + home_equity_debt = 0 + for grandfathered, other, non_acquisition, fair_market_value in zip( + grandfathered_debts, other_debts, non_acquisition_debts, fair_market_values + ): + other_share = np.divide( + other, + other_debt, + out=np.zeros_like(other_debt), + where=other_debt > 0, + ) + treated = grandfathered + other_debt_treated * other_share + excess_acquisition = other - other_debt_treated * other_share + equity_room = max_(fair_market_value - treated, 0) + home_equity_debt = home_equity_debt + min_( + excess_acquisition + non_acquisition, equity_room + ) + # Line 7. + line_7 = min_(home_equity_debt, home_equity_debt_limit) + # Line 8. + return line_6 + line_7 + + +def _ny_deductible_share(qualified_loan_limit, total_debt): + """2017 Publication 936, Table 1, line 11. + + The worksheet divides the qualified loan limit by total debt, rounded to + three places; PE rounds half up. Interest is treated as within the + limits, a share of one, when no balance is reported, as in the federal + calculation. The ratio is computed in double precision so that single + precision cannot move a ratio across a rounding boundary. + """ + limit = np.asarray(qualified_loan_limit, dtype=np.float64) + debt = np.asarray(total_debt, dtype=np.float64) + ratio = np.divide(limit, debt, out=np.ones_like(debt), where=debt > 0) + return np.floor(ratio * 1_000 + 0.5) / 1_000 + + +def _ny_allowed_interest( + deductible, non_deductible, non_acquisition_debt, deductible_share +): + """Worksheet lines 10 and 12, with PE's federal pass-through and floor. + + Interest paid (line 10) is the federally deductible and non-deductible + parts together, the raw interest rather than the federally allowed + amount. When federal law disallows none of the interest and no + non-acquisition debt is reported, New York's limits are at least as + large as the federal ones, so the federal amount passes through; this + keeps a deduction supplied directly. Otherwise the worksheet result + applies, floored at the federal deduction when no non-acquisition debt + is reported so that rounding to three places cannot take New York below + PE's unrounded federal amount. The federal calculation does not see + non-acquisition debt, so no floor applies when it is reported. + """ + worksheet = (deductible + non_deductible) * deductible_share + has_non_acquisition_debt = non_acquisition_debt > 0 + floor = where(has_non_acquisition_debt, 0, deductible) + uses_worksheet = (non_deductible > 0) | has_non_acquisition_debt + return where(uses_worksheet, max_(worksheet, floor), deductible) + + +class ny_mortgage_interest_deduction(Variable): + value_type = float + entity = TaxUnit + label = "New York home mortgage interest deduction" + unit = USD + definition_period = YEAR + documentation = ( + "Home mortgage interest New York allows. N.Y. Tax Law 615(a) computes " + "New York itemized deductions as they existed immediately before " + "P.L. 115-97, so from 2018 New York keeps interest on up to " + "$1,000,000 of home acquisition debt plus $100,000 of home equity " + "debt ($500,000 and $50,000 for married filing separately), rather " + "than the federal $750,000 limit for debt incurred after December 15, " + "2017 and the federal suspension of home equity interest. Before 2018 " + "New York uses the federal amount." + ) + reference = ( + "https://www.nysenate.gov/legislation/laws/TAX/615", + # L.2018, chapter 59, Part JJ, sections 3 and 7. PDF pages 42-43. + "https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42", + "https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest", + # 26 U.S.C. 163(h)(3)(B)-(D), 2017 edition. + "https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm", + # 2017 Publication 936: debt definitions, home equity debt limit, + # Table 1 and grandfathered debt. + # PDF pages 3, 10-11, 13 + "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=3", + ) + defined_for = StateCode.NY + + def formula(tax_unit, period, parameters): + # Before 2018, New York's itemized deductions follow the federal ones. + return add(tax_unit, period, ["deductible_mortgage_interest"]) + + def formula_2018(tax_unit, period, parameters): + balances = [ + tax_unit("first_home_mortgage_balance", period).astype(np.float64), + tax_unit("second_home_mortgage_balance", period).astype(np.float64), + ] + origination_years = [ + tax_unit("first_home_mortgage_origination_year", period), + tax_unit("second_home_mortgage_origination_year", period), + ] + non_acquisition_debts = [ + tax_unit("ny_first_home_non_acquisition_debt", period).astype(np.float64), + tax_unit("ny_second_home_non_acquisition_debt", period).astype(np.float64), + ] + fair_market_values = [ + tax_unit("ny_first_home_fair_market_value", period).astype(np.float64), + tax_unit("ny_second_home_fair_market_value", period).astype(np.float64), + ] + p_irs = parameters(period).gov.irs.deductions.itemized.interest.mortgage + # The shared origination-year convention: a loan from 1987 or earlier + # is grandfathered debt, and 0 means unknown, not grandfathered. + grandfathered_debts = [ + where( + (year > 0) & (year <= p_irs.grandfathered_origination_year), + balance, + 0, + ) + for balance, year in zip(balances, origination_years) + ] + other_debts = [ + balance - grandfathered + for balance, grandfathered in zip(balances, grandfathered_debts) + ] + filing_status = tax_unit("filing_status", period) + p = parameters(period).gov.states.ny.tax.income.deductions.itemized.interest + qualified_loan_limit = _ny_qualified_loan_limit( + grandfathered_debts, + other_debts, + non_acquisition_debts, + fair_market_values, + p.home_acquisition_debt_limit[filing_status], + p.home_equity_debt_limit[filing_status], + ) + non_acquisition_debt = sum(non_acquisition_debts) + # Line 9: total average balances. + total_debt = sum(balances) + non_acquisition_debt + deductible_share = _ny_deductible_share(qualified_loan_limit, total_debt) + return _ny_allowed_interest( + add(tax_unit, period, ["deductible_mortgage_interest"]), + add(tax_unit, period, ["non_deductible_mortgage_interest"]), + non_acquisition_debt, + deductible_share, + ) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py new file mode 100644 index 00000000000..5e9d7442f0c --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py @@ -0,0 +1,80 @@ +from policyengine_us.model_api import * + +_FAIR_MARKET_VALUE_DEFAULT = ( + "Leave at 0 if unknown. A value of 0 gives the home no room for home " + "equity debt, so New York then allows no home equity debt on it and the " + "deduction rests on acquisition and grandfathered debt alone." +) + +_NON_ACQUISITION_DEBT_DEFAULT = ( + "Include the interest on this debt in the home mortgage interest inputs. " + "Defaults to 0, which leaves New York's calculation to the acquisition " + "mortgage balances." +) + + +class ny_first_home_fair_market_value(Variable): + value_type = float + entity = TaxUnit + label = "New York first home fair market value" + unit = USD + definition_period = YEAR + default_value = 0 + defined_for = StateCode.NY + documentation = ( + "Fair market value of the main home, which limits the home equity " + "debt New York allows on it under pre-TCJA IRC 163(h)(3)(C)(i). " + + _FAIR_MARKET_VALUE_DEFAULT + ) + reference = "https://www.nysenate.gov/legislation/laws/TAX/615" + + +class ny_second_home_fair_market_value(Variable): + value_type = float + entity = TaxUnit + label = "New York second home fair market value" + unit = USD + definition_period = YEAR + default_value = 0 + defined_for = StateCode.NY + documentation = ( + "Fair market value of the second home, which limits the home equity " + "debt New York allows on it under pre-TCJA IRC 163(h)(3)(C)(i). " + + _FAIR_MARKET_VALUE_DEFAULT + ) + reference = "https://www.nysenate.gov/legislation/laws/TAX/615" + + +class ny_first_home_non_acquisition_debt(Variable): + value_type = float + entity = TaxUnit + label = "New York first home non-acquisition debt" + unit = USD + definition_period = YEAR + default_value = 0 + defined_for = StateCode.NY + documentation = ( + "Average balance of debt secured by the main home that is not home " + "acquisition debt (not used to buy, build or substantially improve " + "it), such as a home equity loan spent on other things. Pre-TCJA IRC " + "163(h)(3)(C) treats it as home equity debt for New York. " + + _NON_ACQUISITION_DEBT_DEFAULT + ) + reference = "https://www.nysenate.gov/legislation/laws/TAX/615" + + +class ny_second_home_non_acquisition_debt(Variable): + value_type = float + entity = TaxUnit + label = "New York second home non-acquisition debt" + unit = USD + definition_period = YEAR + default_value = 0 + defined_for = StateCode.NY + documentation = ( + "Average balance of debt secured by the second home that is not home " + "acquisition debt (not used to buy, build or substantially improve " + "it). Pre-TCJA IRC 163(h)(3)(C) treats it as home equity debt for " + "New York. " + _NON_ACQUISITION_DEBT_DEFAULT + ) + reference = "https://www.nysenate.gov/legislation/laws/TAX/615" diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py index db906ce1a2f..3a311801b19 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py @@ -45,7 +45,7 @@ class ny_itemized_deductions_phase_out(Variable): L3 = L1 - L2 (worksheet line 3) is therefore the mortgage interest, real estate taxes, charitable gifts, and 2%-floor miscellaneous deductions - exactly the 68(c) base subject to the limitation. Because investment - interest appears in both L1 (inside interest_deduction) and L2, it cancels + interest appears in both L1 (inside ny_interest_deduction) and L2, it cancels out of L3, as intended. """ From 4b893cfcb9df1d8c150f0aa33f9f4c29d4df38db Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:15:11 +0000 Subject: [PATCH 2/9] Keep directly supplied interest totals in New York's interest deduction Building ny_interest_deduction from its components dropped an interest total supplied as deductible_interest_expense, which ny_itemized_deductions_max.yaml uses. Start from interest_deduction and swap only the federal mortgage component for New York's. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6J5LH2sLC5oX6pUHnC --- .../itemized/ny_interest_deduction.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py index a30c7ce2626..24ba2108855 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py @@ -9,8 +9,8 @@ class ny_interest_deduction(Variable): definition_period = YEAR documentation = ( "Interest New York allows as an itemized deduction: the federal " - "interest_deduction components, with New York's pre-TCJA home " - "mortgage interest in place of the federal home mortgage interest." + "interest_deduction with New York's pre-TCJA home mortgage interest " + "in place of the federal home mortgage interest." ) reference = ( "https://www.nysenate.gov/legislation/laws/TAX/615", @@ -18,8 +18,13 @@ class ny_interest_deduction(Variable): ) defined_for = StateCode.NY - adds = [ - "ny_mortgage_interest_deduction", - "non_mortgage_interest", - "deductible_mortgage_insurance_premiums", - ] + def formula(tax_unit, period, parameters): + # Starting from interest_deduction keeps an interest total supplied + # directly (deductible_interest_expense); only the federal mortgage + # component is swapped for New York's. + federal_interest = tax_unit("interest_deduction", period) + federal_mortgage_interest = add( + tax_unit, period, ["deductible_mortgage_interest"] + ) + ny_mortgage_interest = tax_unit("ny_mortgage_interest_deduction", period) + return federal_interest - federal_mortgage_interest + ny_mortgage_interest From 7538a504f8a6f6ec021150e4891b8b75c37cd9b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 20:52:45 +0000 Subject: [PATCH 3/9] Treat an unknown home value as no limit and let New York use the larger allocation share Round 1 review of the New York pre-TCJA mortgage interest calculation: - A fair market value of 0 now means unknown, as the Hawaii calculation and the origination-year inputs treat it, so home equity debt on a home without a value is capped only by the $100,000 ($50,000 separate) limit instead of losing its interest. - The deductible share is the larger of the 2017 Publication 936 worksheet's three-place share and the unrounded ratio of Treas. Reg. 1.163-10T(d); CCA 201201017 allows any reasonable method. The federal floor, which followed no source, goes. The pass-through stays so a directly supplied deduction is not limited twice. - The non-acquisition debt inputs now say that PE's federal calculation does not disallow that interest after 2017. - YAML expectations, two new cases and the Hypothesis properties follow. Co-Authored-By: Claude Opus 5.5 --- .../test_ny_mortgage_debt_limit_invariants.py | 98 ++++++++++++++----- .../ny_mortgage_interest_deduction.yaml | 84 ++++++++++++---- .../ny_mortgage_interest_deduction.py | 56 ++++++----- .../itemized/ny_mortgage_interest_inputs.py | 8 +- 4 files changed, 179 insertions(+), 67 deletions(-) diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py index 8f7b21d845a..4d03bf23f14 100644 --- a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -7,7 +7,9 @@ limit ($500,000 married filing separately); acquisition debt above that limit and non-acquisition debt count as home equity debt up to $100,000 ($50,000), and on each home only up to its fair market value less the debt on it -treated as acquisition or grandfathered debt. +treated as acquisition or grandfathered debt. A fair market value of 0 means +unknown: the home counts as worth at least the debt on it, so only the dollar +limit applies. Hypothesis draws two homes, each with an acquisition balance and vintage, non-acquisition debt and a fair market value, around every cutoff and limit, @@ -15,16 +17,29 @@ 1. Differential: it equals an independent restatement in the statute's order, which places the acquisition limit on each home by a separate loop. + The restatement shares the acquisition limit between homes the same way + (in proportion to each home's other acquisition debt), so it cannot catch + a wrong sharing rule. 2. Bounds: grandfathered debt <= New York limit <= total debt, and the New York limit is at least PE's federal limit for the same acquisition loans. 3. Defaults: with no fair market value and no non-acquisition debt (the - input defaults), no debt counts as home equity debt. + input defaults), the limit is line 6 plus the acquisition debt above it, + up to the home equity debt limit. 4. Order: swapping the two homes never changes the limit. -5. Monotone: a higher fair market value never lowers the limit. +5. Monotone: a higher positive fair market value never lowers the limit. +6. Unknown value: setting every fair market value to 0 (unknown) never + lowers the limit. -And for _ny_allowed_interest: with no non-acquisition debt the result lies -between the federal deduction and the interest paid, and equals the federal -deduction when federal law disallows nothing. +For _ny_allowed_interest, interest on two homes with no non-acquisition debt +is split into the federally deductible and non-deductible parts by PE's +federal share for the same loans. The New York result lies between the +federal deduction and the interest paid, and equals the federal deduction +when federal law disallows nothing. The federal bound follows from the +limits, not from a floor. + +For _ny_deductible_share: the share is at least the unrounded ratio and at +least the ratio rounded half up to three places, at most one, and exceeds the +unrounded ratio by no more than .0005. The limits are read from the New York and federal parameters for every filing status in 2018, 2025 and 2026. The properties cannot pass trivially: @@ -36,6 +51,9 @@ policy outcomes are checked in ny_mortgage_interest_deduction.yaml. """ +import math +from fractions import Fraction + import numpy as np from hypothesis import given, settings from hypothesis import strategies as st @@ -145,8 +163,11 @@ def statute(pair, limits): else: on_home = other_allowed * balance / other if other > 0 else 0.0 excess = balance - on_home - # (C)(i): no more than fair market value less acquisition debt. - room = max(0.0, fair_market_value - on_home) + # (C)(i): no more than fair market value less acquisition debt. A + # value of 0 means unknown and sets no such limit. + room = ( + max(0.0, fair_market_value - on_home) if fair_market_value > 0 else math.inf + ) home_equity += min(excess + non_acquisition, room) # (C)(ii): the aggregate home equity limit. return old + other_allowed + min(home_equity, home_equity_limit) @@ -188,30 +209,47 @@ def test_new_york_qualified_loan_limit_properties(first, second, status): assert old - TOLERANCE <= limit <= total + TOLERANCE assert limit >= federal_limit(pair, federal_limits) - TOLERANCE # 3. Defaults: no fair market value and no non-acquisition debt. + acquisition_limit, home_equity_limit = ny_limits defaults = [(home[0], home[1], 0.0, 0.0) for home in pair] acquisition = sum(home[0] for home in pair) - assert ( - abs(model_limit(defaults, ny_limits) - min(max(old, ny_limits[0]), acquisition)) - <= TOLERANCE - ) + line_6 = min(max(old, acquisition_limit), acquisition) + expected = line_6 + min(acquisition - line_6, home_equity_limit) + assert abs(model_limit(defaults, ny_limits) - expected) <= TOLERANCE # 4. Order. assert abs(limit - model_limit([second, first], ny_limits)) <= TOLERANCE - # 5. Monotone in fair market value. - richer = [(home[0], home[1], home[2], home[3] + 250_000) for home in pair] + # 5. Monotone in a positive fair market value. + richer = [ + (home[0], home[1], home[2], home[3] + 250_000 if home[3] > 0 else 0.0) + for home in pair + ] assert model_limit(richer, ny_limits) >= limit - TOLERANCE + # 6. An unknown value (0) never lowers the limit. + unknown = [(home[0], home[1], home[2], 0.0) for home in pair] + assert model_limit(unknown, ny_limits) >= limit - TOLERANCE @settings(**SETTINGS) @given( - deductible=amounts, - non_deductible=amounts, - limit=amounts, - debt=amounts, + interest=amounts, + first=homes, + second=homes, + status=st.sampled_from(sorted(LIMITS)), ) def test_new_york_allowed_interest_without_non_acquisition_debt( - deductible, non_deductible, limit, debt + interest, first, second, status ): - share = _ny_deductible_share(np.array([min(limit, debt)]), np.array([debt])) + ny_limits, federal_limits = LIMITS[status] + pair = [(home[0], home[1], 0.0, home[3]) for home in (first, second)] + acquisition = sum(home[0] for home in pair) + if acquisition > 0: + federal_share = min(1.0, federal_limit(pair, federal_limits) / acquisition) + else: + federal_share = 1.0 + deductible = interest * federal_share + non_deductible = interest - deductible + share = _ny_deductible_share( + np.array([model_limit(pair, ny_limits)]), np.array([acquisition]) + ) allowed = float( _ny_allowed_interest( np.array([deductible]), @@ -220,8 +258,22 @@ def test_new_york_allowed_interest_without_non_acquisition_debt( share, )[0] ) - assert 0 <= share[0] <= 1 - assert deductible - TOLERANCE <= allowed - assert allowed <= deductible + non_deductible + TOLERANCE + assert deductible - TOLERANCE <= allowed <= interest + TOLERANCE if non_deductible == 0: assert allowed == deductible + + +@settings(**SETTINGS) +@given(limit=amounts, debt=amounts) +def test_new_york_deductible_share_is_the_larger_method(limit, debt): + debt = max(debt, 1.0) + limit = min(limit, debt) + share = float(_ny_deductible_share(np.array([limit]), np.array([debt]))[0]) + ratio = limit / debt + # Rounded half up to three places, computed exactly. + exact = Fraction(int(limit), int(debt)) + rounded = math.floor(exact * 1_000 + Fraction(1, 2)) / 1_000 + assert share >= ratio - 1e-12 + assert share >= rounded - 1e-12 + assert share <= 1 + 1e-12 + assert share - ratio <= 0.0005 + 1e-12 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml index 779833a3c44..c718feb93b1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml @@ -6,10 +6,12 @@ # line 6 = min(max(G, $1,000,000), G + A) # line 7 = min(home equity debt, $100,000), where home equity debt is the # acquisition debt above line 6 plus non-acquisition debt, limited -# on each home to its fair market value less the debt on it -# counted in line 6 +# on each home to its room: its fair market value less the debt on +# it counted in line 6 (a value of 0 means unknown and sets no +# limit) # line 8 = line 6 + line 7 -# line 11 = line 8 / total debt, rounded to three places +# line 11 = the larger of line 8 / total debt rounded to three places and +# the unrounded ratio # line 12 = interest x line 11 # Limits are halved for married filing separately. PolicyEngine's federal # deduction uses the $750,000 limit for debt from 2018 or later, no home @@ -49,8 +51,9 @@ output: # Federal: $40,000 x $750,000 / $1,500,000 = $20,000. deductible_mortgage_interest_tax_unit: 20_000 - # New York: ($1,000,000 + $100,000) / $1,500,000 = .733; $40,000 x .733. - ny_mortgage_interest_deduction: 29_320 + # New York: ($1,000,000 + $100,000) / $1,500,000 = .7333. The worksheet's + # .733 rounds down, so the unrounded share applies: $40,000 x 11/15. + ny_mortgage_interest_deduction: 29_333.33 - name: Separate returns halve both limits period: 2025 @@ -66,10 +69,11 @@ output: # Federal: $20,000 x $375,000 / $600,000 = $12,500. deductible_mortgage_interest_tax_unit: 12_500 - # New York: ($500,000 + $50,000) / $600,000 = .917; $20,000 x .917. + # New York: ($500,000 + $50,000) / $600,000 = .9167. The worksheet's .917 + # is the larger share: $20,000 x .917. ny_mortgage_interest_deduction: 18_340 -- name: Without a fair market value no debt counts as home equity debt +- name: Without a fair market value, only the dollar limit caps home equity debt period: 2025 absolute_error_margin: 0.01 input: @@ -80,8 +84,10 @@ first_home_mortgage_origination_year: 2020 output: deductible_mortgage_interest_tax_unit: 20_000 - # New York: $1,000,000 / $1,500,000 = .667; $40,000 x .667. - ny_mortgage_interest_deduction: 26_680 + # New York: a value of 0 means unknown, so the home has no room limit. + # Line 7 = min($500,000, $100,000). ($1,000,000 + $100,000) / $1,500,000 + # = .7333 (worksheet .733); $40,000 x 11/15. + ny_mortgage_interest_deduction: 29_333.33 - name: Fair market value limits home equity debt period: 2025 @@ -95,8 +101,9 @@ ny_first_home_fair_market_value: 1_040_000 output: # Room for home equity debt: $1,040,000 - $1,000,000 = $40,000. - # New York: $1,040,000 / $1,500,000 = .693; $40,000 x .693. - ny_mortgage_interest_deduction: 27_720 + # New York: $1,040,000 / $1,500,000 = .6933 (worksheet .693); + # $40,000 x 1.04 / 1.5. + ny_mortgage_interest_deduction: 27_733.33 - name: Non-acquisition debt counts as home equity debt up to the limit period: 2025 @@ -112,8 +119,9 @@ output: # PE's federal calculation does not see the non-acquisition debt. deductible_mortgage_interest_tax_unit: 35_000 - # New York: ($500,000 + $100,000) / $700,000 = .857; $35,000 x .857. - ny_mortgage_interest_deduction: 29_995 + # New York: ($500,000 + $100,000) / $700,000 = .8571 (worksheet .857); + # $35,000 x 6/7. + ny_mortgage_interest_deduction: 30_000 - name: Grandfathered debt reduces the acquisition-debt limit period: 2025 @@ -135,7 +143,8 @@ # New York: line 6 = max($400,000, $1,000,000) = $1,000,000, of which # $600,000 is the second home's; its other $200,000 is home equity debt, # within its room of $900,000 - $600,000 = $300,000 and limited to - # $100,000. ($1,000,000 + $100,000) / $1,200,000 = .917; $50,000 x .917. + # $100,000. ($1,000,000 + $100,000) / $1,200,000 = .9167. The worksheet's + # .917 is the larger share: $50,000 x .917. ny_mortgage_interest_deduction: 45_850 - name: Each home's equity room limits its own excess debt @@ -156,8 +165,9 @@ # $400,000 on the second. The first home's other $300,000 fits in its # room; the second home has no room, so its other $200,000 does not # count. Line 7 = min($300,000, $100,000). - # ($1,000,000 + $100,000) / $1,500,000 = .733; $60,000 x .733. - ny_mortgage_interest_deduction: 43_980 + # ($1,000,000 + $100,000) / $1,500,000 = .7333 (worksheet .733); + # $60,000 x 11/15. + ny_mortgage_interest_deduction: 44_000 - name: A directly supplied deduction passes through when nothing is disallowed period: 2025 @@ -167,6 +177,39 @@ output: ny_mortgage_interest_deduction: 12_000 +- name: A directly supplied deduction passes through even with balances + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + deductible_mortgage_interest: 12_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + output: + # With no home_mortgage_interest, non_deductible_mortgage_interest_tax_unit + # reads only home_mortgage_interest_tax_unit and the points, so it is 0 + # and the supplied $12,000 passes through. Without the pass-through, the + # worksheet would limit it again: $12,000 x 11/15 = $8,800. + ny_mortgage_interest_deduction: 12_000 + +- name: Without a fair market value, home equity debt keeps its interest + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 20_000 + first_home_mortgage_balance: 300_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_non_acquisition_debt: 50_000 + output: + # PE's federal calculation does not see the $50,000. + deductible_mortgage_interest_tax_unit: 20_000 + # New York: line 8 = $300,000 + min($50,000, $100,000) = $350,000, equal + # to line 9, so the share is 1. A value of 0 once gave this home no room + # and $17,140. + ny_mortgage_interest_deduction: 20_000 + - name: Before 2018 New York uses the federal amount period: 2017 absolute_error_margin: 0.01 @@ -182,7 +225,7 @@ deductible_mortgage_interest_tax_unit: 26_666.67 ny_mortgage_interest_deduction: 26_666.67 -- name: Rounding cannot take New York below the federal amount +- name: When the worksheet rounds down, New York uses the unrounded share period: 2025 absolute_error_margin: 0.01 input: @@ -191,9 +234,12 @@ home_mortgage_interest: 60_000 first_home_mortgage_balance: 1_200_000 first_home_mortgage_origination_year: 2015 + ny_first_home_fair_market_value: 1_000_000 output: - # Federal: $60,000 x $1,000,000 / $1,200,000 = $50,000. The worksheet - # gives $60,000 x .833 = $49,980; PE keeps the federal $50,000. + # Federal: $60,000 x $1,000,000 / $1,200,000 = $50,000. + # New York: room = $1,000,000 - $1,000,000 = $0, so line 7 is 0; + # $1,000,000 / $1,200,000 = .8333. The worksheet's .833 gives $49,980, + # the unrounded share $50,000, which applies. deductible_mortgage_interest_tax_unit: 50_000 ny_mortgage_interest_deduction: 50_000 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py index 925176dde51..abcbbe015ec 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py @@ -19,7 +19,8 @@ def _ny_qualified_loan_limit( limit, and debt that was never acquisition debt, may qualify as home equity debt (Figure A, note 3). On each home, home equity debt cannot exceed the home's fair market value less the debt on it treated as - acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)); across homes it + acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)); a value of 0 + means unknown and sets no such limit. Across homes it cannot exceed the home equity debt limit (line 7). The limit on other acquisition debt is shared among homes in proportion @@ -46,7 +47,11 @@ def _ny_qualified_loan_limit( ) treated = grandfathered + other_debt_treated * other_share excess_acquisition = other - other_debt_treated * other_share - equity_room = max_(fair_market_value - treated, 0) + # 0 means unknown: treat the home as worth at least the debt on it, + # as the Hawaii calculation does. + equity_room = where( + fair_market_value > 0, max_(fair_market_value - treated, 0), np.inf + ) home_equity_debt = home_equity_debt + min_( excess_acquisition + non_acquisition, equity_room ) @@ -57,41 +62,42 @@ def _ny_qualified_loan_limit( def _ny_deductible_share(qualified_loan_limit, total_debt): - """2017 Publication 936, Table 1, line 11. + """2017 Publication 936, Table 1, line 11, or the unrounded ratio. - The worksheet divides the qualified loan limit by total debt, rounded to - three places; PE rounds half up. Interest is treated as within the - limits, a share of one, when no balance is reported, as in the federal - calculation. The ratio is computed in double precision so that single - precision cannot move a ratio across a rounding boundary. + Until regulations issue, the House report on OBRA 1987 allows any + reasonable method of allocating interest on debt above the limits. The + IRS accepts the worksheet, which rounds the ratio to three places, and + the regulation's simplified method, which uses the unrounded ratio + (Treas. Reg. 1.163-10T(d); CCA 201201017). A taxpayer can use either, so + the larger share applies; PE rounds half up. The share is one when the + household reports no balance, as in the federal calculation. PE computes the + ratio in double precision so that single precision cannot move it + across a rounding boundary. """ limit = np.asarray(qualified_loan_limit, dtype=np.float64) debt = np.asarray(total_debt, dtype=np.float64) ratio = np.divide(limit, debt, out=np.ones_like(debt), where=debt > 0) - return np.floor(ratio * 1_000 + 0.5) / 1_000 + worksheet = np.floor(ratio * 1_000 + 0.5) / 1_000 + return np.maximum(worksheet, ratio) def _ny_allowed_interest( deductible, non_deductible, non_acquisition_debt, deductible_share ): - """Worksheet lines 10 and 12, with PE's federal pass-through and floor. + """Worksheet lines 10 and 12, with PE's federal pass-through. Interest paid (line 10) is the federally deductible and non-deductible parts together, the raw interest rather than the federally allowed - amount. When federal law disallows none of the interest and no - non-acquisition debt is reported, New York's limits are at least as - large as the federal ones, so the federal amount passes through; this - keeps a deduction supplied directly. Otherwise the worksheet result - applies, floored at the federal deduction when no non-acquisition debt - is reported so that rounding to three places cannot take New York below - PE's unrounded federal amount. The federal calculation does not see - non-acquisition debt, so no floor applies when it is reported. + amount. When federal law disallows none of the interest and the + household reports no non-acquisition debt, the federal amount passes + through, which keeps a deduction supplied directly. Otherwise line 12 + applies. New York's limits never fall below the federal ones for the + same loans, so without non-acquisition debt the result never falls + below PE's federal amount. """ worksheet = (deductible + non_deductible) * deductible_share - has_non_acquisition_debt = non_acquisition_debt > 0 - floor = where(has_non_acquisition_debt, 0, deductible) - uses_worksheet = (non_deductible > 0) | has_non_acquisition_debt - return where(uses_worksheet, max_(worksheet, floor), deductible) + uses_worksheet = (non_deductible > 0) | (non_acquisition_debt > 0) + return where(uses_worksheet, worksheet, deductible) class ny_mortgage_interest_deduction(Variable): @@ -121,6 +127,12 @@ class ny_mortgage_interest_deduction(Variable): # Table 1 and grandfathered debt. # PDF pages 3, 10-11, 13 "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=3", + # Treasury Regulation 1.163-10T(d) and (g): the unrounded simplified + # method and the taxpayer's choice of method. PDF pages 3-4, 6. + "https://www.govinfo.gov/content/pkg/CFR-2025-title26-vol3/pdf/CFR-2025-title26-vol3-sec1-163-10T.pdf#page=3", + # CCA 201201017 (nonprecedential): any reasonable allocation method. + # PDF pages 4-5. + "https://www.irs.gov/pub/irs-wd/1201017.pdf#page=4", ) defined_for = StateCode.NY diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py index 5e9d7442f0c..aace4efd0f1 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py @@ -1,13 +1,15 @@ from policyengine_us.model_api import * _FAIR_MARKET_VALUE_DEFAULT = ( - "Leave at 0 if unknown. A value of 0 gives the home no room for home " - "equity debt, so New York then allows no home equity debt on it and the " - "deduction rests on acquisition and grandfathered debt alone." + "Leave at 0 if unknown. New York then counts the home as worth at least " + "the debt on it, so only the $100,000 ($50,000 separate) home equity debt " + "limit applies, as in PolicyEngine's Hawaii calculation." ) _NON_ACQUISITION_DEBT_DEFAULT = ( "Include the interest on this debt in the home mortgage interest inputs. " + "PolicyEngine's federal calculation reads only the acquisition mortgage " + "balances, so it does not disallow that interest federally after 2017. " "Defaults to 0, which leaves New York's calculation to the acquisition " "mortgage balances." ) From 169fe4fd39fc37ae6d4e62a42bed776b4c330df8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 05:35:52 +0000 Subject: [PATCH 4/9] Round an exact tie in New York's worksheet share up, and tighten the text An exact tie such as 1,001,000 / 2,000,000 = .5005 comes to 500.49999999999994 after x 1,000 in binary, so the floor rounded it down to .500 and New York used the unrounded .5005 instead of the worksheet's .501. Twelve ratios from .5005 to .5115 did this. Rounding the scaled ratio to nine places before the floor makes the tie round up, as the docstring says. A YAML case and a Hypothesis example now pin the tie. Also from the round 2 review: - A YAML case for non-acquisition debt on the second home, which no test read before. - Property 3 now asserts that the Hawaii helper gives the same default limit, which the input documentation cites. - The input documentation credits the unknown-value rule to PolicyEngine, and says the federal calculation applies its share to home equity interest when the balances exceed the federal limit. - Active voice, a clearer subject and corrected Publication 936 pages in the docstrings and comments. - The Hawaii docstring and the rounding candidate in the source-conflict backlog now account for New York's home value input and its larger-share rule. Co-Authored-By: Claude Opus 5.5 --- .../test_ny_mortgage_debt_limit_invariants.py | 31 ++++++++----- .../ny_mortgage_interest_deduction.yaml | 43 +++++++++++++++++-- .../hi_mortgage_interest_deduction.py | 4 +- .../ny_mortgage_interest_deduction.py | 15 ++++--- .../itemized/ny_mortgage_interest_inputs.py | 11 ++--- source_conflicts/candidates.yaml | 7 ++- 6 files changed, 82 insertions(+), 29 deletions(-) diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py index 4d03bf23f14..5530d861791 100644 --- a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -24,15 +24,16 @@ York limit is at least PE's federal limit for the same acquisition loans. 3. Defaults: with no fair market value and no non-acquisition debt (the input defaults), the limit is line 6 plus the acquisition debt above it, - up to the home equity debt limit. + up to the home equity debt limit, which is also what the Hawaii helper + gives. 4. Order: swapping the two homes never changes the limit. 5. Monotone: a higher positive fair market value never lowers the limit. 6. Unknown value: setting every fair market value to 0 (unknown) never lowers the limit. -For _ny_allowed_interest, interest on two homes with no non-acquisition debt -is split into the federally deductible and non-deductible parts by PE's -federal share for the same loans. The New York result lies between the +For _ny_allowed_interest, PE's federal share for the same loans splits +interest on two homes with no non-acquisition debt into the federally +deductible and non-deductible parts. The New York result lies between the federal deduction and the interest paid, and equals the federal deduction when federal law disallows nothing. The federal bound follows from the limits, not from a floor. @@ -41,11 +42,11 @@ least the ratio rounded half up to three places, at most one, and exceeds the unrounded ratio by no more than .0005. -The limits are read from the New York and federal parameters for every -filing status in 2018, 2025 and 2026. The properties cannot pass trivially: -the differential compares against a separately written per-home calculation, -and the default and bound checks fail if the fair market value test, the -home equity limit or the acquisition limit is dropped or reversed. +The test reads the limits from the New York and federal parameters for +every filing status in 2018, 2025 and 2026. The properties cannot pass +trivially: the differential compares against a separately written per-home +calculation, and the default and bound checks fail if the fair market value +test, the home equity limit or the acquisition limit is dropped or reversed. These are for-all properties of the helpers, which YAML cannot state. The policy outcomes are checked in ny_mortgage_interest_deduction.yaml. @@ -55,10 +56,13 @@ from fractions import Fraction import numpy as np -from hypothesis import given, settings +from hypothesis import example, given, settings from hypothesis import strategies as st from policyengine_us.system import system +from policyengine_us.variables.gov.states.hi.tax.income.deductions.itemized.hi_mortgage_interest_deduction import ( + _hi_qualified_loan_limit, +) from policyengine_us.variables.gov.states.ny.tax.income.taxable_income.deductions.itemized.ny_mortgage_interest_deduction import ( _ny_allowed_interest, _ny_deductible_share, @@ -215,6 +219,12 @@ def test_new_york_qualified_loan_limit_properties(first, second, status): line_6 = min(max(old, acquisition_limit), acquisition) expected = line_6 + min(acquisition - line_6, home_equity_limit) assert abs(model_limit(defaults, ny_limits) - expected) <= TOLERANCE + hawaii = float( + _hi_qualified_loan_limit( + np.array([old]), np.array([acquisition - old]), *ny_limits + )[0] + ) + assert abs(expected - hawaii) <= TOLERANCE # 4. Order. assert abs(limit - model_limit([second, first], ny_limits)) <= TOLERANCE # 5. Monotone in a positive fair market value. @@ -265,6 +275,7 @@ def test_new_york_allowed_interest_without_non_acquisition_debt( @settings(**SETTINGS) @given(limit=amounts, debt=amounts) +@example(limit=1_001.0, debt=2_000.0) def test_new_york_deductible_share_is_the_larger_method(limit, debt): debt = max(debt, 1.0) limit = min(limit, debt) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml index c718feb93b1..5ebb83c3d0d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml @@ -13,7 +13,7 @@ # line 11 = the larger of line 8 / total debt rounded to three places and # the unrounded ratio # line 12 = interest x line 11 -# Limits are halved for married filing separately. PolicyEngine's federal +# Married filing separately halves the limits. PolicyEngine's federal # deduction uses the $750,000 limit for debt from 2018 or later, no home # equity debt and the exact ratio. @@ -123,6 +123,23 @@ # $35,000 x 6/7. ny_mortgage_interest_deduction: 30_000 +- name: Non-acquisition debt on the second home counts as home equity debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 30_000 + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 2015 + ny_second_home_non_acquisition_debt: 200_000 + output: + # PE's federal calculation does not see the non-acquisition debt. + deductible_mortgage_interest_tax_unit: 30_000 + # New York: ($400,000 + $100,000) / $600,000 = .8333 (worksheet .833); + # $30,000 x 5/6. + ny_mortgage_interest_deduction: 25_000 + - name: Grandfathered debt reduces the acquisition-debt limit period: 2025 absolute_error_margin: 0.01 @@ -187,9 +204,10 @@ first_home_mortgage_origination_year: 2020 output: # With no home_mortgage_interest, non_deductible_mortgage_interest_tax_unit - # reads only home_mortgage_interest_tax_unit and the points, so it is 0 - # and the supplied $12,000 passes through. Without the pass-through, the - # worksheet would limit it again: $12,000 x 11/15 = $8,800. + # is 0: it takes home_mortgage_interest_tax_unit plus the points, less + # deductible_mortgage_interest_tax_unit, and does not read the supplied + # person-level amount. So the supplied $12,000 passes through. Without the + # pass-through, the worksheet would limit it again: $12,000 x 11/15 = $8,800. ny_mortgage_interest_deduction: 12_000 - name: Without a fair market value, home equity debt keeps its interest @@ -243,6 +261,23 @@ deductible_mortgage_interest_tax_unit: 50_000 ny_mortgage_interest_deduction: 50_000 +- name: An exact tie in the worksheet's share rounds up + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 80_000 + first_home_mortgage_balance: 2_000_000 + first_home_mortgage_origination_year: 2020 + ny_first_home_fair_market_value: 1_001_000 + output: + # Federal: $80,000 x $750,000 / $2,000,000 = $30,000. + deductible_mortgage_interest_tax_unit: 30_000 + # New York: room = $1,001,000 - $1,000,000 = $1,000. $1,001,000 / + # $2,000,000 = .5005, an exact tie, so the worksheet's share is .501. + ny_mortgage_interest_deduction: 40_080 + - name: Mortgage interest outside New York is zero period: 2025 input: diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py index 67a8036904a..7c2e0662d13 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py @@ -19,8 +19,8 @@ def _hi_qualified_loan_limit( Debt above the acquisition-debt limit may qualify as home equity debt, up to the home equity debt limit (line 7; Publication 936 (2017), Figure A, note 3). The home equity debt limit is also capped by each home's fair - market value less its acquisition debt; PolicyEngine has no home value - input, so every home is treated as worth at least its mortgages. + market value less its acquisition debt; PolicyEngine has no Hawaii home + value input, so it counts every home as worth at least its mortgages. """ total_debt = grandfathered_debt + other_debt # Lines 3-6. diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py index abcbbe015ec..a61b4eb00ae 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py @@ -20,10 +20,10 @@ def _ny_qualified_loan_limit( equity debt (Figure A, note 3). On each home, home equity debt cannot exceed the home's fair market value less the debt on it treated as acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)); a value of 0 - means unknown and sets no such limit. Across homes it + means unknown and sets no such limit. Across homes, home equity debt cannot exceed the home equity debt limit (line 7). - The limit on other acquisition debt is shared among homes in proportion + PE shares the limit on other acquisition debt among homes in proportion to each home's other acquisition debt; the statute's aggregate limit does not say how to place it on a particular home. """ @@ -77,7 +77,9 @@ def _ny_deductible_share(qualified_loan_limit, total_debt): limit = np.asarray(qualified_loan_limit, dtype=np.float64) debt = np.asarray(total_debt, dtype=np.float64) ratio = np.divide(limit, debt, out=np.ones_like(debt), where=debt > 0) - worksheet = np.floor(ratio * 1_000 + 0.5) / 1_000 + # Round the scaled ratio first. In binary, 1,001,000 / 2,000,000 x 1,000 + # comes to 500.49999999999994, so the floor alone rounds this exact tie down. + worksheet = np.floor(np.round(ratio * 1_000, 9) + 0.5) / 1_000 return np.maximum(worksheet, ratio) @@ -123,9 +125,10 @@ class ny_mortgage_interest_deduction(Variable): "https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest", # 26 U.S.C. 163(h)(3)(B)-(D), 2017 edition. "https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm", - # 2017 Publication 936: debt definitions, home equity debt limit, - # Table 1 and grandfathered debt. - # PDF pages 3, 10-11, 13 + # 2017 Publication 936: Figure A note 3, home acquisition debt, home + # equity debt and its limit, grandfathered debt, and Table 1 with its + # line 7 instructions. + # PDF pages 3, 9-11, 13 "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=3", # Treasury Regulation 1.163-10T(d) and (g): the unrounded simplified # method and the taxpayer's choice of method. PDF pages 3-4, 6. diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py index aace4efd0f1..6334a21a858 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py @@ -1,17 +1,18 @@ from policyengine_us.model_api import * _FAIR_MARKET_VALUE_DEFAULT = ( - "Leave at 0 if unknown. New York then counts the home as worth at least " + "Leave at 0 if unknown. PolicyEngine then counts the home as worth at least " "the debt on it, so only the $100,000 ($50,000 separate) home equity debt " - "limit applies, as in PolicyEngine's Hawaii calculation." + "limit applies, as in its Hawaii calculation." ) _NON_ACQUISITION_DEBT_DEFAULT = ( "Include the interest on this debt in the home mortgage interest inputs. " "PolicyEngine's federal calculation reads only the acquisition mortgage " - "balances, so it does not disallow that interest federally after 2017. " - "Defaults to 0, which leaves New York's calculation to the acquisition " - "mortgage balances." + "balances, so after 2017 it treats that interest like interest on those " + "balances: fully deductible when they fit the federal limit, and deductible " + "at the same share when they don't. Defaults to 0, which leaves New York's " + "calculation to the acquisition mortgage balances." ) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 025bb5dd42b..104d42c38aa 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -10464,15 +10464,18 @@ candidates: - policyengine_us/variables/gov/states/al/tax/income/deductions/itemized/al_investment_interest.py - policyengine_us/variables/gov/states/nc/tax/income/deductions/nc_mortgage_and_property_tax_before_limitation.py - policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py + - policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py prs: - https://github.com/PolicyEngine/policyengine-us/pull/10274 + - https://github.com/PolicyEngine/policyengine-us/pull/10596 state: unreviewed - note: 'PE-US follows two conventions, so follows stays unknown until one is picked. The federal deduction + note: 'PE-US follows three conventions, so follows stays unknown until one is picked. The federal deduction (deductible_mortgage_interest_tax_unit) uses the exact ratio, and Alabama and North Carolina read that federal amount. Hawaii (hi_mortgage_interest_deduction) follows the 2017 worksheet as a gap-fill, rounding line 11 to three places, and floors the result at the federal deduction, a PE safeguard that binds only when the rounded share falls below the exact federal share. Settling the federal reading (rulespec-us#1413) and using it for - both would make that floor unnecessary.' + both would make that floor unnecessary. New York (ny_mortgage_interest_deduction) allows the larger of the + three-place share and the exact ratio.' - id: us:niit-excess-business-loss-addback program: income-tax years: From 53f8806db4aa11d7bc1ce0400301607051297ba4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 06:12:17 +0000 Subject: [PATCH 5/9] Make two sentences from the previous commit say what they mean The property 3 docstring's "which is also what the Hawaii helper gives" could attach to the home equity debt limit rather than the whole default limit, so it is now a sentence of its own. The rounding candidate's note said "using it for both" after listing three conventions; it now says Hawaii, whose floor the sentence is about. Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_ny_mortgage_debt_limit_invariants.py | 4 ++-- source_conflicts/candidates.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py index 5530d861791..b3a1e2a20c3 100644 --- a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -24,8 +24,8 @@ York limit is at least PE's federal limit for the same acquisition loans. 3. Defaults: with no fair market value and no non-acquisition debt (the input defaults), the limit is line 6 plus the acquisition debt above it, - up to the home equity debt limit, which is also what the Hawaii helper - gives. + up to the home equity debt limit. The Hawaii helper gives the same + limit. 4. Order: swapping the two homes never changes the limit. 5. Monotone: a higher positive fair market value never lowers the limit. 6. Unknown value: setting every fair market value to 0 (unknown) never diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 104d42c38aa..74d0591305a 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -10474,8 +10474,8 @@ candidates: amount. Hawaii (hi_mortgage_interest_deduction) follows the 2017 worksheet as a gap-fill, rounding line 11 to three places, and floors the result at the federal deduction, a PE safeguard that binds only when the rounded share falls below the exact federal share. Settling the federal reading (rulespec-us#1413) and using it for - both would make that floor unnecessary. New York (ny_mortgage_interest_deduction) allows the larger of the - three-place share and the exact ratio.' + Hawaii too would make that floor unnecessary. New York (ny_mortgage_interest_deduction) allows the larger of + the three-place share and the exact ratio.' - id: us:niit-excess-business-loss-addback program: income-tax years: From fdb25bde051680dd046580a3df01afddc93e082a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:34:07 +0000 Subject: [PATCH 6/9] Read home values from shared inputs and place the debt limit the taxpayer's better way Round 3 review of New York's pre-TCJA mortgage interest: - The main home's value now comes from primary_residence_value, summed over the tax unit, with 0 still meaning unknown. The model already had that input, so a value entered once now reaches New York. - New inputs cover only what the model lacked: second_residence_value and first_home_non_acquisition_debt / second_home_non_acquisition_debt. They carry no state prefix or gate, so other pre-TCJA calculations can read them. Their documentation says Publication 936 values each home on the date the last debt was secured by it, which current values approximate. - The acquisition limit now goes on the homes by whichever reasonable method gives more home equity debt: in proportion to each home's other acquisition debt, or loan by loan in date order with the most recent debt as the excess. The House report that CCA 201201017 quotes names chronological order as a reasonable method. - Two YAML cases pin which home each non-acquisition balance sits on, and they also tell the statute's per-home room from Publication 936's total. The source-conflict backlog records that choice. - New cases cover the allocation, spouses' shares of the home's value and the 2017 federal gap. The Hypothesis restatement now enumerates date orders by permutation, and an example pins same-year loans. - The Hawaii docstring now says its calculation doesn't read the home value inputs. Co-Authored-By: Claude Opus 5.5 --- .../test_ny_mortgage_debt_limit_invariants.py | 86 ++++-- .../ny_mortgage_interest_deduction.yaml | 261 ++++++++++++++++-- .../hi_mortgage_interest_deduction.py | 5 +- .../ny_mortgage_interest_deduction.py | 161 ++++++++--- .../itemized/ny_mortgage_interest_inputs.py | 83 ------ .../assets/second_residence_value.py | 32 +++ .../first_home_non_acquisition_debt.py | 35 +++ .../second_home_non_acquisition_debt.py | 34 +++ source_conflicts/candidates.yaml | 37 +++ 9 files changed, 559 insertions(+), 175 deletions(-) delete mode 100644 policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py create mode 100644 policyengine_us/variables/household/assets/second_residence_value.py create mode 100644 policyengine_us/variables/household/expense/tax_unit/first_home_non_acquisition_debt.py create mode 100644 policyengine_us/variables/household/expense/tax_unit/second_home_non_acquisition_debt.py diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py index b3a1e2a20c3..ecb7c2f12b6 100644 --- a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -16,10 +16,11 @@ and checks _ny_qualified_loan_limit: 1. Differential: it equals an independent restatement in the statute's - order, which places the acquisition limit on each home by a separate loop. - The restatement shares the acquisition limit between homes the same way - (in proportion to each home's other acquisition debt), so it cannot catch - a wrong sharing rule. + order. The restatement places the acquisition limit on the homes in each + reasonable way, in proportion to each home's other acquisition debt or + loan by loan in every date order that the origination years allow, and + keeps the largest home equity debt. It finds the date orders by sorting + permutations, not by the model's two explicit orders. 2. Bounds: grandfathered debt <= New York limit <= total debt, and the New York limit is at least PE's federal limit for the same acquisition loans. 3. Defaults: with no fair market value and no non-acquisition debt (the @@ -39,8 +40,9 @@ limits, not from a floor. For _ny_deductible_share: the share is at least the unrounded ratio and at -least the ratio rounded half up to three places, at most one, and exceeds the -unrounded ratio by no more than .0005. +least the ratio rounded half up to three places, and at most one. It exceeds +the unrounded ratio by no more than .0005 plus 5e-13, since the nine-place +rounding treats a scaled ratio within 5e-10 of a half as a tie. The test reads the limits from the New York and federal parameters for every filing status in 2018, 2025 and 2026. The properties cannot pass @@ -52,6 +54,7 @@ policy outcomes are checked in ny_mortgage_interest_deduction.yaml. """ +import itertools import math from fractions import Fraction @@ -146,6 +149,7 @@ def model_limit(pair, limits): _ny_qualified_loan_limit( grandfathered_debts, other_debts, + [np.array([year]) for _, year, _, _ in pair], [np.array([home[2]]) for home in pair], [np.array([home[3]]) for home in pair], *limits, @@ -160,21 +164,53 @@ def statute(pair, limits): other = sum(balance for balance, year, _, _ in pair if not grandfathered(year)) # (D)(ii): grandfathered debt reduces the $1,000,000 limit. other_allowed = min(other, max(0.0, acquisition_limit - old)) - home_equity = 0.0 - for balance, year, non_acquisition, fair_market_value in pair: - if grandfathered(year): - on_home, excess = balance, 0.0 - else: - on_home = other_allowed * balance / other if other > 0 else 0.0 - excess = balance - on_home - # (C)(i): no more than fair market value less acquisition debt. A - # value of 0 means unknown and sets no such limit. - room = ( - max(0.0, fair_market_value - on_home) if fair_market_value > 0 else math.inf - ) - home_equity += min(excess + non_acquisition, room) - # (C)(ii): the aggregate home equity limit. - return old + other_allowed + min(home_equity, home_equity_limit) + + def home_equity(placed): + total = 0.0 + for (balance, year, non_acquisition, fair_market_value), on_home in zip( + pair, placed + ): + if grandfathered(year): + on_home, excess = balance, 0.0 + else: + excess = balance - on_home + # (C)(i): no more than fair market value less acquisition debt. A + # value of 0 means unknown and sets no such limit. + room = ( + max(0.0, fair_market_value - on_home) + if fair_market_value > 0 + else math.inf + ) + total += min(excess + non_acquisition, room) + return total + + # The allowance shared in proportion to each home's other debt. + placements = [ + [ + 0.0 + if grandfathered(year) or other == 0 + else other_allowed * balance / other + for balance, year, _, _ in pair + ] + ] + # The allowance placed loan by loan, oldest first, so the most recent + # debt is the excess. Loans from the same year go in either order, and an + # unknown year (0) is the most recent. + dates = [year if year > 0 else math.inf for _, year, _, _ in pair] + for order in itertools.permutations(range(len(pair))): + if any(dates[a] > dates[b] for a, b in zip(order, order[1:])): + continue + remaining = other_allowed + placed = [0.0] * len(pair) + for index in order: + balance, year = pair[index][0], pair[index][1] + if not grandfathered(year): + placed[index] = min(balance, remaining) + remaining -= placed[index] + placements.append(placed) + # (C)(ii): the aggregate home equity limit, on the taxpayer's best placement. + best = max(home_equity(placed) for placed in placements) + return old + other_allowed + min(best, home_equity_limit) def federal_limit(pair, limits): @@ -200,6 +236,14 @@ def test_new_york_limits_are_the_statutory_amounts(): @settings(**SETTINGS) @given(first=homes, second=homes, status=st.sampled_from(sorted(LIMITS))) +# Loans from the same year, where only the second home's loan going first +# gives the full home equity debt: a fixed order fails the differential or +# the swap. +@example( + first=(900_000.0, 2018, 0.0, 540_000.0), + second=(900_000.0, 2018, 0.0, 150_000.0), + status=(2025, "JOINT"), +) def test_new_york_qualified_loan_limit_properties(first, second, status): ny_limits, federal_limits = LIMITS[status] pair = [first, second] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml index 5ebb83c3d0d..19be6e050c0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.yaml @@ -8,14 +8,17 @@ # acquisition debt above line 6 plus non-acquisition debt, limited # on each home to its room: its fair market value less the debt on # it counted in line 6 (a value of 0 means unknown and sets no -# limit) +# limit). The line 6 limit goes on the homes in proportion to +# their other acquisition debt or loan by loan in date order, +# whichever gives more home equity debt. # line 8 = line 6 + line 7 # line 11 = the larger of line 8 / total debt rounded to three places and # the unrounded ratio # line 12 = interest x line 11 -# Married filing separately halves the limits. PolicyEngine's federal -# deduction uses the $750,000 limit for debt from 2018 or later, no home -# equity debt and the exact ratio. +# The main home's value is primary_residence_value and the second home's is +# second_residence_value. Married filing separately halves the limits. +# PolicyEngine's federal deduction uses the $750,000 limit for debt from 2018 +# or later, no home equity debt and the exact ratio. - name: New York keeps the $1,000,000 acquisition-debt limit period: 2025 @@ -26,8 +29,8 @@ home_mortgage_interest: 36_000 first_home_mortgage_balance: 900_000 first_home_mortgage_origination_year: 2020 - ny_first_home_fair_market_value: 1_200_000 - ny_first_home_non_acquisition_debt: 0 + primary_residence_value: 1_200_000 + first_home_non_acquisition_debt: 0 output: # Federal: $36,000 x $750,000 / $900,000 = $30,000. deductible_mortgage_interest_tax_unit: 30_000 @@ -46,8 +49,8 @@ home_mortgage_interest: 40_000 first_home_mortgage_balance: 1_500_000 first_home_mortgage_origination_year: 2020 - ny_first_home_fair_market_value: 2_000_000 - ny_first_home_non_acquisition_debt: 0 + primary_residence_value: 2_000_000 + first_home_non_acquisition_debt: 0 output: # Federal: $40,000 x $750,000 / $1,500,000 = $20,000. deductible_mortgage_interest_tax_unit: 20_000 @@ -64,8 +67,8 @@ home_mortgage_interest: 20_000 first_home_mortgage_balance: 600_000 first_home_mortgage_origination_year: 2020 - ny_first_home_fair_market_value: 800_000 - ny_first_home_non_acquisition_debt: 0 + primary_residence_value: 800_000 + first_home_non_acquisition_debt: 0 output: # Federal: $20,000 x $375,000 / $600,000 = $12,500. deductible_mortgage_interest_tax_unit: 12_500 @@ -98,13 +101,45 @@ home_mortgage_interest: 40_000 first_home_mortgage_balance: 1_500_000 first_home_mortgage_origination_year: 2020 - ny_first_home_fair_market_value: 1_040_000 + primary_residence_value: 1_040_000 output: # Room for home equity debt: $1,040,000 - $1,000,000 = $40,000. # New York: $1,040,000 / $1,500,000 = .6933 (worksheet .693); # $40,000 x 1.04 / 1.5. ny_mortgage_interest_deduction: 27_733.33 +- name: Spouses' shares of the main home's value add up + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + home_mortgage_interest: 40_000 + primary_residence_value: 520_000 + spouse: + age: 45 + primary_residence_value: 520_000 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: NY + output: + deductible_mortgage_interest_tax_unit: 20_000 + # The home is worth $520,000 + $520,000 = $1,040,000, so the room is + # $40,000, as in the case above: $40,000 x 1.04 / 1.5. The head's share + # alone would leave no room and give $40,000 x .667 = $26,680. + ny_mortgage_interest_deduction: 27_733.33 + - name: Non-acquisition debt counts as home equity debt up to the limit period: 2025 absolute_error_margin: 0.01 @@ -114,8 +149,8 @@ home_mortgage_interest: 35_000 first_home_mortgage_balance: 500_000 first_home_mortgage_origination_year: 2015 - ny_first_home_fair_market_value: 1_000_000 - ny_first_home_non_acquisition_debt: 200_000 + primary_residence_value: 1_000_000 + first_home_non_acquisition_debt: 200_000 output: # PE's federal calculation does not see the non-acquisition debt. deductible_mortgage_interest_tax_unit: 35_000 @@ -132,7 +167,7 @@ home_mortgage_interest: 30_000 first_home_mortgage_balance: 400_000 first_home_mortgage_origination_year: 2015 - ny_second_home_non_acquisition_debt: 200_000 + second_home_non_acquisition_debt: 200_000 output: # PE's federal calculation does not see the non-acquisition debt. deductible_mortgage_interest_tax_unit: 30_000 @@ -140,6 +175,50 @@ # $30,000 x 5/6. ny_mortgage_interest_deduction: 25_000 +- name: The second home's value limits its own non-acquisition debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 30_000 + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 2015 + primary_residence_value: 2_000_000 + second_residence_value: 50_000 + second_home_non_acquisition_debt: 200_000 + output: + # Federal: $400,000 of 2015 debt fits the limit, so all $30,000. + deductible_mortgage_interest_tax_unit: 30_000 + # The second home has no acquisition debt, so its room is its $50,000 + # value and line 7 = $50,000. ($400,000 + $50,000) / $600,000 = .75; + # $30,000 x .75. With the debt on the main home (room $1,600,000), or + # with Publication 936's total room of $1,650,000, line 7 = $100,000 + # and the result is $25,000. + ny_mortgage_interest_deduction: 22_500 + +- name: The main home's value limits its own non-acquisition debt + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 30_000 + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 2015 + primary_residence_value: 450_000 + first_home_non_acquisition_debt: 200_000 + second_residence_value: 1_000_000 + output: + # Federal: $400,000 of 2015 debt fits the limit, so all $30,000. + deductible_mortgage_interest_tax_unit: 30_000 + # Room on the main home: $450,000 - $400,000 = $50,000, so line 7 = + # $50,000. ($400,000 + $50,000) / $600,000 = .75; $30,000 x .75. With + # the debt on the second home (room $1,000,000), or with Publication + # 936's total room of $1,050,000, line 7 = $100,000 and the result is + # $25,000. + ny_mortgage_interest_deduction: 22_500 + - name: Grandfathered debt reduces the acquisition-debt limit period: 2025 absolute_error_margin: 0.01 @@ -151,8 +230,8 @@ first_home_mortgage_origination_year: 1986 second_home_mortgage_balance: 800_000 second_home_mortgage_origination_year: 2020 - ny_first_home_fair_market_value: 500_000 - ny_second_home_fair_market_value: 900_000 + primary_residence_value: 500_000 + second_residence_value: 900_000 output: # Federal: line 6 = $400,000 of grandfathered debt; the later debt # raises the limit only to $750,000; $50,000 x $750,000 / $1,200,000. @@ -175,15 +254,136 @@ first_home_mortgage_origination_year: 2020 second_home_mortgage_balance: 600_000 second_home_mortgage_origination_year: 2021 - ny_first_home_fair_market_value: 3_000_000 - ny_second_home_fair_market_value: 400_000 + primary_residence_value: 3_000_000 + second_residence_value: 400_000 output: - # Line 6 = $1,000,000, shared 9:6: $600,000 on the first home and - # $400,000 on the second. The first home's other $300,000 fits in its + # Line 6 = $1,000,000. Shared 9:6, it puts $600,000 on the main home and + # $400,000 on the second. The main home's other $300,000 fits in its # room; the second home has no room, so its other $200,000 does not - # count. Line 7 = min($300,000, $100,000). - # ($1,000,000 + $100,000) / $1,500,000 = .7333 (worksheet .733); - # $60,000 x 11/15. + # count. In date order, the 2020 loan takes $900,000 and the second home + # carries the $500,000 excess, of which its $300,000 room counts. Either + # way line 7 = $100,000. ($1,000,000 + $100,000) / $1,500,000 = .7333 + # (worksheet .733); $60,000 x 11/15. + ny_mortgage_interest_deduction: 44_000 + +- name: Placing the limit on one loan first can beat sharing it + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 40_000 + second_residence_mortgage_interest: 20_000 + first_home_mortgage_balance: 1_000_000 + first_home_mortgage_origination_year: 2020 + primary_residence_value: 700_000 + second_home_mortgage_balance: 500_000 + second_home_mortgage_origination_year: 2020 + second_residence_value: 350_000 + output: + # Federal: $60,000 x $750,000 / $1,500,000 = $30,000. + deductible_mortgage_interest_tax_unit: 30_000 + # Line 6 = $1,000,000. Shared 2:1, it leaves rooms of $700,000 - + # $666,666.67 and $350,000 - $333,333.33, so line 7 = $50,000 and the + # share is .7 ($42,000). Both loans are from 2020, so either can go + # first. With the main home's first, the second home's $500,000 is the + # excess and fits its $350,000 room up to the $100,000 limit: + # ($1,000,000 + $100,000) / $1,500,000 = .7333, above the worksheet's + # .733 ($43,980). $60,000 x 11/15. + ny_mortgage_interest_deduction: 44_000 + +- name: In date order the most recent loan carries the excess + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 36_000 + second_residence_mortgage_interest: 36_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2015 + primary_residence_value: 540_000 + second_home_mortgage_balance: 900_000 + second_home_mortgage_origination_year: 2020 + second_residence_value: 150_000 + output: + # Federal: $900,000 of 2015 debt keeps the $1,000,000 limit, which caps + # the later debt at $900,000; $72,000 x $900,000 / $1,800,000. + deductible_mortgage_interest_tax_unit: 36_000 + # Line 6 = $1,000,000. Shared evenly, $500,000 goes on each home: the + # main home's room is $40,000 and the second home's 0, so line 7 = + # $40,000. In date order the 2015 loan takes $900,000 and the 2020 loan + # $100,000; its $800,000 excess fits a room of $150,000 - $100,000 = + # $50,000. Line 7 = $50,000. ($1,000,000 + $50,000) / $1,800,000 = + # .5833 (worksheet .583); $72,000 x 7/12. Putting the 2020 loan first + # would give line 7 = $100,000, but that is not date order. + ny_mortgage_interest_deduction: 42_000 + +- name: Loans from the same year can go in either order + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 36_000 + second_residence_mortgage_interest: 36_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + primary_residence_value: 540_000 + second_home_mortgage_balance: 900_000 + second_home_mortgage_origination_year: 2020 + second_residence_value: 150_000 + output: + # Federal: $72,000 x $750,000 / $1,800,000 = $30,000. + deductible_mortgage_interest_tax_unit: 30_000 + # As above, but both loans are from 2020. With the second home's loan + # first, it takes $900,000 and the main home's loan $100,000; its + # $800,000 excess fits a room of $540,000 - $100,000 = $440,000 up to + # the $100,000 limit. ($1,000,000 + $100,000) / $1,800,000 = .6111 + # (worksheet .611); $72,000 x 11/18. + ny_mortgage_interest_deduction: 44_000 + +- name: A loan with an unknown year counts as the most recent + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 36_000 + second_residence_mortgage_interest: 36_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2015 + primary_residence_value: 540_000 + second_home_mortgage_balance: 900_000 + second_residence_value: 150_000 + output: + # Federal: an unknown year counts as later debt, as in the 2020 case. + deductible_mortgage_interest_tax_unit: 36_000 + # The second home's loan has no year, so it goes after the 2015 loan, as + # in the date-order case: line 7 = $50,000 and $72,000 x 7/12. + ny_mortgage_interest_deduction: 42_000 + +- name: Sharing the limit applies when it gives more than date order + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: JOINT + home_mortgage_interest: 36_000 + second_residence_mortgage_interest: 36_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2015 + primary_residence_value: 3_000_000 + second_home_mortgage_balance: 900_000 + second_home_mortgage_origination_year: 2020 + second_residence_value: 120_000 + output: + deductible_mortgage_interest_tax_unit: 36_000 + # In date order the 2020 loan carries the $800,000 excess, but its room + # is only $120,000 - $100,000 = $20,000. Shared evenly, the main home + # carries $400,000 of excess within a room of $2,500,000, so line 7 = + # $100,000. ($1,000,000 + $100,000) / $1,800,000 = .6111 (worksheet + # .611); $72,000 x 11/18. Date order alone would give $40,824. ny_mortgage_interest_deduction: 44_000 - name: A directly supplied deduction passes through when nothing is disallowed @@ -219,7 +419,7 @@ home_mortgage_interest: 20_000 first_home_mortgage_balance: 300_000 first_home_mortgage_origination_year: 2020 - ny_first_home_non_acquisition_debt: 50_000 + first_home_non_acquisition_debt: 50_000 output: # PE's federal calculation does not see the $50,000. deductible_mortgage_interest_tax_unit: 20_000 @@ -237,9 +437,14 @@ home_mortgage_interest: 40_000 first_home_mortgage_balance: 1_500_000 first_home_mortgage_origination_year: 2010 - ny_first_home_fair_market_value: 2_000_000 + primary_residence_value: 2_000_000 output: - # Federal: $40,000 x $1,000,000 / $1,500,000. + # Federal: $40,000 x $1,000,000 / $1,500,000. This records PE's federal + # amount, which has no home equity debt before 2018. The 2017 law let the + # $500,000 above the limit count as home equity debt up to $100,000 + # (Rev. Rul. 2010-25), which would give $1,100,000 / $1,500,000: + # $29,333.33, or $29,320 on the worksheet's .733. A federal follow-up + # would change both outputs. deductible_mortgage_interest_tax_unit: 26_666.67 ny_mortgage_interest_deduction: 26_666.67 @@ -252,7 +457,7 @@ home_mortgage_interest: 60_000 first_home_mortgage_balance: 1_200_000 first_home_mortgage_origination_year: 2015 - ny_first_home_fair_market_value: 1_000_000 + primary_residence_value: 1_000_000 output: # Federal: $60,000 x $1,000,000 / $1,200,000 = $50,000. # New York: room = $1,000,000 - $1,000,000 = $0, so line 7 is 0; @@ -270,7 +475,7 @@ home_mortgage_interest: 80_000 first_home_mortgage_balance: 2_000_000 first_home_mortgage_origination_year: 2020 - ny_first_home_fair_market_value: 1_001_000 + primary_residence_value: 1_001_000 output: # Federal: $80,000 x $750,000 / $2,000,000 = $30,000. deductible_mortgage_interest_tax_unit: 30_000 diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py index 7c2e0662d13..5e88d7db818 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py @@ -19,8 +19,9 @@ def _hi_qualified_loan_limit( Debt above the acquisition-debt limit may qualify as home equity debt, up to the home equity debt limit (line 7; Publication 936 (2017), Figure A, note 3). The home equity debt limit is also capped by each home's fair - market value less its acquisition debt; PolicyEngine has no Hawaii home - value input, so it counts every home as worth at least its mortgages. + market value less its acquisition debt. This calculation doesn't read + the home value inputs (primary_residence_value and second_residence_value), + so it counts every home as worth at least its mortgages. """ total_debt = grandfathered_debt + other_debt # Lines 3-6. diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py index a61b4eb00ae..ee12ef4dfc4 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py @@ -1,9 +1,45 @@ from policyengine_us.model_api import * +def _ny_home_equity_debt( + other_debts_treated, + grandfathered_debts, + other_debts, + non_acquisition_debts, + fair_market_values, +): + """Home equity debt on both homes for one placement of the acquisition limit. + + Each argument is a list with one array per home. other_debts_treated is + the part of each home's other acquisition debt that the placement treats + as acquisition debt. On each home, the rest of that debt and the debt that + was never acquisition debt count as home equity debt, up to the home's + room. The room is the home's fair market value less the debt on it + treated as acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)). A + value of 0 means unknown and sets no room limit. + """ + home_equity_debt = 0 + for treated_other, grandfathered, other, non_acquisition, value in zip( + other_debts_treated, + grandfathered_debts, + other_debts, + non_acquisition_debts, + fair_market_values, + ): + treated = grandfathered + treated_other + # 0 means unknown: treat the home as worth at least the debt on it, + # as the Hawaii calculation does. + room = where(value > 0, max_(value - treated, 0), np.inf) + home_equity_debt = home_equity_debt + min_( + other - treated_other + non_acquisition, room + ) + return home_equity_debt + + def _ny_qualified_loan_limit( grandfathered_debts, other_debts, + origination_years, non_acquisition_debts, fair_market_values, home_acquisition_debt_limit, @@ -11,52 +47,81 @@ def _ny_qualified_loan_limit( ): """Pre-TCJA IRC 163(h)(3)(B)-(D), which N.Y. Tax Law 615(a) preserves. - Each argument except the limits is a list with one array per home. + Each argument except the limits is a list with two arrays: the main home, + then the second home. Grandfathered debt, incurred on or before October 13, 1987, has no dollar limit but reduces the acquisition-debt limit for other acquisition debt (2017 Publication 936, Table 1, lines 1-6). Acquisition debt above that limit, and debt that was never acquisition debt, may qualify as home - equity debt (Figure A, note 3). On each home, home equity debt cannot - exceed the home's fair market value less the debt on it treated as - acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)); a value of 0 - means unknown and sets no such limit. Across homes, home equity debt + equity debt (Figure A, note 3; Rev. Rul. 2010-25). On each home, home + equity debt cannot exceed the home's fair market value less the debt on + it treated as acquisition or grandfathered debt (IRC 163(h)(3)(C)(i)). + Publication 936 caps the total instead: the sum of the homes' rooms. PE + applies the statute's room home by home. Across homes, home equity debt cannot exceed the home equity debt limit (line 7). - PE shares the limit on other acquisition debt among homes in proportion - to each home's other acquisition debt; the statute's aggregate limit does - not say how to place it on a particular home. + The acquisition limit is an aggregate, so where it falls decides each + home's room. Grandfathered debt counts in full on its own home. Until + regulations issue, the House report on OBRA 1987 asks for a reasonable + method of placing the rest, and names chronological order as one; CCA + 201201017 accepts any reasonable method. PE uses whichever of two methods + gives more home equity debt. One shares the limit in proportion to each + home's other acquisition debt. The other places it loan by loan in the + order the loans were incurred, so the most recent debt is the excess. + Loans from the same year can go in either order, and a loan with an + unknown year (0) counts as the most recent, as in the federal vintage + rule. """ grandfathered_debt = sum(grandfathered_debts) other_debt = sum(other_debts) - acquisition_debt = grandfathered_debt + other_debt # Lines 3-6: debt treated as acquisition or grandfathered debt. line_6 = min_( - max_(grandfathered_debt, home_acquisition_debt_limit), acquisition_debt + max_(grandfathered_debt, home_acquisition_debt_limit), + grandfathered_debt + other_debt, ) other_debt_treated = line_6 - grandfathered_debt - home_equity_debt = 0 - for grandfathered, other, non_acquisition, fair_market_value in zip( - grandfathered_debts, other_debts, non_acquisition_debts, fair_market_values - ): - other_share = np.divide( + # The limit shared in proportion to each home's other acquisition debt. + proportional = [ + other_debt_treated + * np.divide( other, other_debt, out=np.zeros_like(other_debt), where=other_debt > 0, ) - treated = grandfathered + other_debt_treated * other_share - excess_acquisition = other - other_debt_treated * other_share - # 0 means unknown: treat the home as worth at least the debt on it, - # as the Hawaii calculation does. - equity_room = where( - fair_market_value > 0, max_(fair_market_value - treated, 0), np.inf - ) - home_equity_debt = home_equity_debt + min_( - excess_acquisition + non_acquisition, equity_room + for other in other_debts + ] + # The limit placed on one home's loan first, then the other's. + first_other, second_other = other_debts + first_home_part = min_(first_other, other_debt_treated) + first_loan_first = [first_home_part, other_debt_treated - first_home_part] + second_home_part = min_(second_other, other_debt_treated) + second_loan_first = [other_debt_treated - second_home_part, second_home_part] + + def home_equity_debt(other_debts_treated): + return _ny_home_equity_debt( + other_debts_treated, + grandfathered_debts, + other_debts, + non_acquisition_debts, + fair_market_values, ) - # Line 7. - line_7 = min_(home_equity_debt, home_equity_debt_limit) + + # The older loan goes first. A year of 0 is unknown and counts as the + # most recent; loans from the same year can go in either order. + first_year, second_year = [ + where(year > 0, year, np.inf) for year in origination_years + ] + in_date_order = max_( + where(first_year <= second_year, home_equity_debt(first_loan_first), 0), + where(second_year <= first_year, home_equity_debt(second_loan_first), 0), + ) + # Line 7: the larger of the two methods, up to the home equity debt limit. + line_7 = min_( + max_(home_equity_debt(proportional), in_date_order), + home_equity_debt_limit, + ) # Line 8. return line_6 + line_7 @@ -94,8 +159,10 @@ def _ny_allowed_interest( household reports no non-acquisition debt, the federal amount passes through, which keeps a deduction supplied directly. Otherwise line 12 applies. New York's limits never fall below the federal ones for the - same loans, so without non-acquisition debt the result never falls - below PE's federal amount. + same loans. So without non-acquisition debt, the result never falls below + the federal amount PE computes from home_mortgage_interest. A + deductible_mortgage_interest supplied alongside home_mortgage_interest can + exceed the result. """ worksheet = (deductible + non_deductible) * deductible_share uses_worksheet = (non_deductible > 0) | (non_acquisition_debt > 0) @@ -111,30 +178,38 @@ class ny_mortgage_interest_deduction(Variable): documentation = ( "Home mortgage interest New York allows. N.Y. Tax Law 615(a) computes " "New York itemized deductions as they existed immediately before " - "P.L. 115-97, so from 2018 New York keeps interest on up to " + "P.L. 115-97. So from 2018 New York keeps interest on up to " "$1,000,000 of home acquisition debt plus $100,000 of home equity " - "debt ($500,000 and $50,000 for married filing separately), rather " - "than the federal $750,000 limit for debt incurred after December 15, " - "2017 and the federal suspension of home equity interest. Before 2018 " - "New York uses the federal amount." + "debt ($500,000 and $50,000 for married filing separately). Federal " + "law instead limits debt incurred after December 15, 2017 to $750,000 " + "and allows no interest on home equity debt. The home values come " + "from primary_residence_value and second_residence_value, where 0 " + "means unknown. Before 2018 New York uses the federal amount." ) reference = ( "https://www.nysenate.gov/legislation/laws/TAX/615", # L.2018, chapter 59, Part JJ, sections 3 and 7. PDF pages 42-43. "https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42", "https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest", - # 26 U.S.C. 163(h)(3)(B)-(D), 2017 edition. - "https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm", + # 2025 Form IT-196 instructions, Limit on home mortgage interest. + "https://www.tax.ny.gov/pdf/2025/inc/it196i_2025.pdf#page=2", + # 26 U.S.C. 163(h)(3)(B)-(D) and (h)(4)(A), 2016 edition, the text in + # force immediately before P.L. 115-97. + "https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapB-partVI-sec163.htm", # 2017 Publication 936: Figure A note 3, home acquisition debt, home # equity debt and its limit, grandfathered debt, and Table 1 with its # line 7 instructions. # PDF pages 3, 9-11, 13 "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=3", + # Rev. Rul. 2010-25, 2010-44 I.R.B. 571: acquisition debt above the + # $1,000,000 limit can be home equity debt, as Figure A note 3 says. + "https://www.irs.gov/irb/2010-44_IRB#RR-2010-25", # Treasury Regulation 1.163-10T(d) and (g): the unrounded simplified # method and the taxpayer's choice of method. PDF pages 3-4, 6. "https://www.govinfo.gov/content/pkg/CFR-2025-title26-vol3/pdf/CFR-2025-title26-vol3-sec1-163-10T.pdf#page=3", - # CCA 201201017 (nonprecedential): any reasonable allocation method. - # PDF pages 4-5. + # CCA 201201017 (nonprecedential): the House report on OBRA 1987, + # whose example of a reasonable allocation is chronological order, + # and any reasonable method. PDF pages 4-5. "https://www.irs.gov/pub/irs-wd/1201017.pdf#page=4", ) defined_for = StateCode.NY @@ -153,12 +228,15 @@ def formula_2018(tax_unit, period, parameters): tax_unit("second_home_mortgage_origination_year", period), ] non_acquisition_debts = [ - tax_unit("ny_first_home_non_acquisition_debt", period).astype(np.float64), - tax_unit("ny_second_home_non_acquisition_debt", period).astype(np.float64), + tax_unit("first_home_non_acquisition_debt", period).astype(np.float64), + tax_unit("second_home_non_acquisition_debt", period).astype(np.float64), ] + # Each member's share of each home's value, summed. Publication 936 + # values a home on the date the last debt was secured by it; PE uses + # these values and the current balances instead. fair_market_values = [ - tax_unit("ny_first_home_fair_market_value", period).astype(np.float64), - tax_unit("ny_second_home_fair_market_value", period).astype(np.float64), + add(tax_unit, period, ["primary_residence_value"]).astype(np.float64), + add(tax_unit, period, ["second_residence_value"]).astype(np.float64), ] p_irs = parameters(period).gov.irs.deductions.itemized.interest.mortgage # The shared origination-year convention: a loan from 1987 or earlier @@ -180,6 +258,7 @@ def formula_2018(tax_unit, period, parameters): qualified_loan_limit = _ny_qualified_loan_limit( grandfathered_debts, other_debts, + origination_years, non_acquisition_debts, fair_market_values, p.home_acquisition_debt_limit[filing_status], diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py deleted file mode 100644 index 6334a21a858..00000000000 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py +++ /dev/null @@ -1,83 +0,0 @@ -from policyengine_us.model_api import * - -_FAIR_MARKET_VALUE_DEFAULT = ( - "Leave at 0 if unknown. PolicyEngine then counts the home as worth at least " - "the debt on it, so only the $100,000 ($50,000 separate) home equity debt " - "limit applies, as in its Hawaii calculation." -) - -_NON_ACQUISITION_DEBT_DEFAULT = ( - "Include the interest on this debt in the home mortgage interest inputs. " - "PolicyEngine's federal calculation reads only the acquisition mortgage " - "balances, so after 2017 it treats that interest like interest on those " - "balances: fully deductible when they fit the federal limit, and deductible " - "at the same share when they don't. Defaults to 0, which leaves New York's " - "calculation to the acquisition mortgage balances." -) - - -class ny_first_home_fair_market_value(Variable): - value_type = float - entity = TaxUnit - label = "New York first home fair market value" - unit = USD - definition_period = YEAR - default_value = 0 - defined_for = StateCode.NY - documentation = ( - "Fair market value of the main home, which limits the home equity " - "debt New York allows on it under pre-TCJA IRC 163(h)(3)(C)(i). " - + _FAIR_MARKET_VALUE_DEFAULT - ) - reference = "https://www.nysenate.gov/legislation/laws/TAX/615" - - -class ny_second_home_fair_market_value(Variable): - value_type = float - entity = TaxUnit - label = "New York second home fair market value" - unit = USD - definition_period = YEAR - default_value = 0 - defined_for = StateCode.NY - documentation = ( - "Fair market value of the second home, which limits the home equity " - "debt New York allows on it under pre-TCJA IRC 163(h)(3)(C)(i). " - + _FAIR_MARKET_VALUE_DEFAULT - ) - reference = "https://www.nysenate.gov/legislation/laws/TAX/615" - - -class ny_first_home_non_acquisition_debt(Variable): - value_type = float - entity = TaxUnit - label = "New York first home non-acquisition debt" - unit = USD - definition_period = YEAR - default_value = 0 - defined_for = StateCode.NY - documentation = ( - "Average balance of debt secured by the main home that is not home " - "acquisition debt (not used to buy, build or substantially improve " - "it), such as a home equity loan spent on other things. Pre-TCJA IRC " - "163(h)(3)(C) treats it as home equity debt for New York. " - + _NON_ACQUISITION_DEBT_DEFAULT - ) - reference = "https://www.nysenate.gov/legislation/laws/TAX/615" - - -class ny_second_home_non_acquisition_debt(Variable): - value_type = float - entity = TaxUnit - label = "New York second home non-acquisition debt" - unit = USD - definition_period = YEAR - default_value = 0 - defined_for = StateCode.NY - documentation = ( - "Average balance of debt secured by the second home that is not home " - "acquisition debt (not used to buy, build or substantially improve " - "it). Pre-TCJA IRC 163(h)(3)(C) treats it as home equity debt for " - "New York. " + _NON_ACQUISITION_DEBT_DEFAULT - ) - reference = "https://www.nysenate.gov/legislation/laws/TAX/615" diff --git a/policyengine_us/variables/household/assets/second_residence_value.py b/policyengine_us/variables/household/assets/second_residence_value.py new file mode 100644 index 00000000000..9c5772bafb5 --- /dev/null +++ b/policyengine_us/variables/household/assets/second_residence_value.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class second_residence_value(Variable): + value_type = float + entity = Person + label = "Second residence value" + documentation = ( + "Market value of the person's share of their second residence, not " + "assessed value: the one home besides the main home that the taxpayer " + "treats as a qualified residence for mortgage interest, such as a " + "vacation home. Under the pre-TCJA mortgage interest limits, which New " + "York keeps, home equity debt on this home can't exceed its value, the " + "sum of these shares in the tax unit, less the part of " + "second_home_mortgage_balance treated as acquisition debt. " + "primary_residence_value plays the same part for the main home. " + "Publication 936 (2017) measures the value and that debt on " + "the date the last debt was secured by the home; PolicyEngine " + "approximates both with this value and the current balances. Leave at " + "0 if unknown: PolicyEngine then counts the home as worth at least the " + "debt on it." + ) + unit = USD + quantity_type = STOCK + definition_period = YEAR + uprating = "gov.bls.cpi.cpi_u" + reference = ( + # 26 U.S.C. 163(h)(3)(C)(i) and (h)(4)(A), 2016 edition. + "https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapB-partVI-sec163.htm", + # PDF pages 10, 13 + "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=10", + ) diff --git a/policyengine_us/variables/household/expense/tax_unit/first_home_non_acquisition_debt.py b/policyengine_us/variables/household/expense/tax_unit/first_home_non_acquisition_debt.py new file mode 100644 index 00000000000..c5208dd2b8d --- /dev/null +++ b/policyengine_us/variables/household/expense/tax_unit/first_home_non_acquisition_debt.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * + + +class first_home_non_acquisition_debt(Variable): + value_type = float + entity = TaxUnit + label = "First home non-acquisition debt" + unit = USD + definition_period = YEAR + default_value = 0 + documentation = ( + "Average balance of debt secured by the main home that is not home " + "acquisition debt (not used to buy, build or substantially improve " + "it), such as a home equity loan spent on other things. The pre-TCJA " + "mortgage interest limits, which New York keeps, treat it as home " + "equity debt, up to $100,000 ($50,000 married filing separately) " + "across both homes. On this home, that debt also can't exceed the " + "home's value (primary_residence_value) less the part of " + "first_home_mortgage_balance treated as acquisition debt. Publication " + "936 (2017) measures that value and debt on the date the last debt was " + "secured by the home; PolicyEngine approximates both with current " + "values. Include the interest on this debt in home_mortgage_interest. " + "PolicyEngine's federal calculation reads only the acquisition " + "mortgage balances, so after 2017 it treats that interest like " + "interest on those balances: fully deductible when they fit the " + "federal limit, and deductible at the same share when they don't. " + "Defaults to 0, which leaves the pre-TCJA calculation to the " + "acquisition mortgage balances." + ) + reference = ( + # 26 U.S.C. 163(h)(3)(C), 2016 edition. + "https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapB-partVI-sec163.htm", + # PDF pages 10, 13 + "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=10", + ) diff --git a/policyengine_us/variables/household/expense/tax_unit/second_home_non_acquisition_debt.py b/policyengine_us/variables/household/expense/tax_unit/second_home_non_acquisition_debt.py new file mode 100644 index 00000000000..7870b39f18b --- /dev/null +++ b/policyengine_us/variables/household/expense/tax_unit/second_home_non_acquisition_debt.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * + + +class second_home_non_acquisition_debt(Variable): + value_type = float + entity = TaxUnit + label = "Second home non-acquisition debt" + unit = USD + definition_period = YEAR + default_value = 0 + documentation = ( + "Average balance of debt secured by the second home that is not home " + "acquisition debt (not used to buy, build or substantially improve " + "it). The pre-TCJA mortgage interest limits, which New York keeps, " + "treat it as home equity debt, up to $100,000 ($50,000 married filing " + "separately) across both homes. On this home, that debt also can't " + "exceed the home's value (second_residence_value) less the part of " + "second_home_mortgage_balance treated as acquisition debt. Publication " + "936 (2017) measures that value and debt on the date the last debt was " + "secured by the home; PolicyEngine approximates both with current " + "values. Include the interest on this debt in " + "second_residence_mortgage_interest. PolicyEngine's federal " + "calculation reads only the acquisition mortgage balances, so after " + "2017 it treats that interest like interest on those balances: fully " + "deductible when they fit the federal limit, and deductible at the " + "same share when they don't. Defaults to 0, which leaves the pre-TCJA " + "calculation to the acquisition mortgage balances." + ) + reference = ( + # 26 U.S.C. 163(h)(3)(C), 2016 edition. + "https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapB-partVI-sec163.htm", + # PDF pages 10, 13 + "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=10", + ) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 62937e037c1..a1ff10ee504 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -7721,6 +7721,43 @@ candidates: state: ruled note: Both credits halve exactly, as the statute says; the tables round for the form. Section 606 authorizes nearest-dollar tables only for the real property tax credit. +- id: us-ny:pre-tcja-home-equity-room-per-residence + program: income-tax + years: + from: 2018 + type: tax + question: Under the pre-TCJA mortgage limits that New York keeps, does each home's value less its acquisition debt + cap the home equity debt on that home, or does the total of both homes' rooms cap the total home equity debt? + sources: + - kind: statute + citation: 26 U.S.C. 163(h)(3)(C)(i) (2016 edition), which N.Y. Tax Law 615(a) keeps for New York from 2018 + url: https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapB-partVI-sec163.htm + - kind: guidance + citation: 2017 Pub. 936, Home equity debt limit (p. 10) and the Table 1 line 7 instructions (p. 13) + url: https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=10 + - kind: instructions + citation: 2025 Form IT-196 instructions, Limit on home mortgage interest, which point filers to the 2017 Pub. 936 + url: https://www.tax.ny.gov/pdf/2025/inc/it196i_2025.pdf#page=2 + readings: + statute: Home equity indebtedness is debt secured by a qualified residence "to the extent the aggregate amount of + such indebtedness does not exceed" that residence's fair market value less its acquisition indebtedness. + other: Pub. 936 caps "the total home equity debt on your main home and second home" at the smaller of $100,000 + and the total of each home's value reduced (not below zero) by its acquisition and grandfathered debt. + example: 'A 2025 joint return has $30,000 of interest, a $400,000 loan from 2015 on a $2,000,000 main home, and $200,000 + of non-acquisition debt on a $50,000 second home. Home by home, line 7 is $50,000 and $22,500 is deductible; with + the total room of $1,650,000, line 7 is $100,000 and $25,000 is deductible.' + pe_us: + follows: statute + code: + - policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10596 + state: ruled + note: 'PE-US applies the room home by home in _ny_qualified_loan_limit and _ny_home_equity_debt. Two cases in + ny_mortgage_interest_deduction.yaml pin it: "The second home''s value limits its own non-acquisition debt" and "The + main home''s value limits its own non-acquisition debt" give $22,500 home by home and $25,000 with Pub. 936''s total. + The readings differ only when one home''s home equity debt exceeds its own room while the other home has room to + spare. Ruled in November 2026 to follow the statute.' - id: us-ny:real-property-credit-claimable-spouse program: income-tax years: From 1e068558f8e051b12f12ad3e7e769764b6639d03 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:34:27 +0000 Subject: [PATCH 7/9] Test ny_interest_deduction on its own and cite the sources the review named - ny_interest_deduction.yaml shows that investment interest and mortgage insurance premiums pass through while New York's mortgage interest replaces the federal amount, and that nothing changes before 2018. The variable's documentation now says the premiums stay in it until ny_itemized_deductions_max removes them. - Both limit parameters cite the 2016 edition of 26 U.S.C. 163, the text in force immediately before P.L. 115-97, and the 2018 and 2025 IT-196 instructions, which state the limits for those years. - The changelog names the shared inputs. Co-Authored-By: Claude Opus 5.5 --- .../ny-pre-tcja-mortgage-interest.fixed.md | 2 +- .../interest/home_acquisition_debt_limit.yaml | 8 +- .../interest/home_equity_debt_limit.yaml | 8 +- .../itemized/ny_interest_deduction.yaml | 78 +++++++++++++++++++ .../itemized/ny_interest_deduction.py | 6 +- 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.yaml diff --git a/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md b/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md index b9181cd44d1..479cd961c52 100644 --- a/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md +++ b/changelog.d/ny-pre-tcja-mortgage-interest.fixed.md @@ -1 +1 @@ -Apply New York's pre-TCJA mortgage debt limits ($1,000,000 acquisition and $100,000 home equity debt) to its itemized interest deduction from 2018, with new home value and non-acquisition debt inputs. +Apply New York's pre-TCJA mortgage debt limits ($1,000,000 acquisition and $100,000 home equity debt) to its itemized interest deduction from 2018. The main home's value comes from primary_residence_value, and new inputs give the second home's value and each home's non-acquisition debt. diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml index 02e2f769e7e..229d0d5be8a 100644 --- a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml @@ -23,7 +23,11 @@ metadata: href: https://www.nysenate.gov/legislation/laws/TAX/615 - title: "L.2018, chapter 59 (S7509C), Part JJ, sections 3 and 7 #page 42-43" href: https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42 - - title: 26 U.S. Code 163(h)(3)(B)(ii) (2017 edition) - href: https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm + - title: 26 U.S. Code 163(h)(3)(B)(ii) (2016 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapB-partVI-sec163.htm - title: New York State Department of Taxation and Finance, Itemized deductions (2025), Interest you paid href: https://www.tax.ny.gov/pit/file/itemized-deductions-2025.htm#interest + - title: 2018 Form IT-196 instructions, Interest you paid + href: https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=2 + - title: 2025 Form IT-196 instructions, Limit on home mortgage interest + href: https://www.tax.ny.gov/pdf/2025/inc/it196i_2025.pdf#page=2 diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml index edf2e20e399..165d940f455 100644 --- a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml @@ -23,7 +23,11 @@ metadata: href: https://www.nysenate.gov/legislation/laws/TAX/615 - title: "L.2018, chapter 59 (S7509C), Part JJ, sections 3 and 7 #page 42-43" href: https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42 - - title: 26 U.S. Code 163(h)(3)(C)(ii) (2017 edition) - href: https://www.govinfo.gov/content/pkg/USCODE-2017-title26/html/USCODE-2017-title26-subtitleA-chap1-subchapB-partVI-sec163.htm + - title: 26 U.S. Code 163(h)(3)(C)(ii) (2016 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2016-title26/html/USCODE-2016-title26-subtitleA-chap1-subchapB-partVI-sec163.htm - title: "2017 IRS Publication 936, Home equity debt limit and Table 1 #page 10-11" href: https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=10 + - title: 2018 Form IT-196 instructions, Interest you paid + href: https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=2 + - title: 2025 Form IT-196 instructions, Limit on home mortgage interest + href: https://www.tax.ny.gov/pdf/2025/inc/it196i_2025.pdf#page=2 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.yaml new file mode 100644 index 00000000000..bb47009fb62 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.yaml @@ -0,0 +1,78 @@ +# ny_interest_deduction = the federal interest_deduction - the federal +# mortgage interest (deductible_mortgage_interest) + New York's mortgage +# interest (ny_mortgage_interest_deduction). Investment interest and mortgage +# insurance premiums pass through unchanged; ny_itemized_deductions_max +# removes the premiums New York disallows. + +- name: Investment interest stays when New York replaces the mortgage interest + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + investment_interest_expense: 5_000 + output: + # Federal: $40,000 x $750,000 / $1,500,000 = $20,000 of mortgage + # interest, plus $5,000 of investment interest. + deductible_mortgage_interest_tax_unit: 20_000 + interest_deduction: 25_000 + # New York: ($1,000,000 + $100,000) / $1,500,000 = .7333; + # $40,000 x 11/15 = $29,333.33 replaces the $20,000. + ny_mortgage_interest_deduction: 29_333.33 + ny_interest_deduction: 34_333.33 + +- name: Mortgage insurance premiums stay in the interest total + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + adjusted_gross_income: 80_000 + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2020 + investment_interest_expense: 5_000 + mortgage_insurance_premiums: 1_000 + output: + # Federal: $20,000 of mortgage interest, $5,000 of investment interest + # and $1,000 of premiums (AGI is below the $100,000 phase-out start). + deductible_mortgage_insurance_premiums: 1_000 + deductible_mortgage_interest_tax_unit: 20_000 + interest_deduction: 26_000 + # $26,000 - $20,000 + $29,333.33. + ny_mortgage_interest_deduction: 29_333.33 + ny_interest_deduction: 35_333.33 + # ny_itemized_deductions_max then removes the $1,000 of premiums. + ny_itemized_deductions_max: 34_333.33 + +- name: Before 2018 the federal interest deduction passes through + period: 2017 + absolute_error_margin: 0.01 + input: + state_code: NY + filing_status: SINGLE + home_mortgage_interest: 40_000 + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2010 + investment_interest_expense: 5_000 + output: + # Federal: $40,000 x $1,000,000 / $1,500,000 = $26,666.67, plus $5,000. + # New York uses the federal mortgage amount before 2018, so nothing + # changes. + interest_deduction: 31_666.67 + ny_mortgage_interest_deduction: 26_666.67 + ny_interest_deduction: 31_666.67 + +- name: Interest outside New York is zero + period: 2025 + input: + state_code: NJ + home_mortgage_interest: 36_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2020 + investment_interest_expense: 5_000 + output: + ny_interest_deduction: 0 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py index 24ba2108855..1549d150935 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py @@ -8,9 +8,11 @@ class ny_interest_deduction(Variable): unit = USD definition_period = YEAR documentation = ( - "Interest New York allows as an itemized deduction: the federal " + "Interest for New York's itemized deduction: the federal " "interest_deduction with New York's pre-TCJA home mortgage interest " - "in place of the federal home mortgage interest." + "in place of the federal home mortgage interest. Mortgage insurance " + "premiums stay in it, and ny_itemized_deductions_max removes those " + "New York disallows." ) reference = ( "https://www.nysenate.gov/legislation/laws/TAX/615", From 5112f990324538e35bd7d7076d581e6ed022c03a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:35:13 +0000 Subject: [PATCH 8/9] Say how the property test's restatement finds date orders It checks every permutation of the homes; it doesn't sort them. Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_ny_mortgage_debt_limit_invariants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py index ecb7c2f12b6..0b0804e40db 100644 --- a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -19,8 +19,8 @@ order. The restatement places the acquisition limit on the homes in each reasonable way, in proportion to each home's other acquisition debt or loan by loan in every date order that the origination years allow, and - keeps the largest home equity debt. It finds the date orders by sorting - permutations, not by the model's two explicit orders. + keeps the largest home equity debt. It finds the date orders by checking + every permutation of the homes, not from the model's two explicit orders. 2. Bounds: grandfathered debt <= New York limit <= total debt, and the New York limit is at least PE's federal limit for the same acquisition loans. 3. Defaults: with no fair market value and no non-acquisition debt (the From 32fe9f5af6164f7f9a57ef87c80886ad828eeaa8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:55:32 +0000 Subject: [PATCH 9/9] Pin the date-order rule in the property test with two more examples Hypothesis's 400 draws rarely give two loans from different years where the placement method matters, so mutants that ignore the dates, drop proportional sharing or treat an unknown year as the oldest passed it. One example has an unknown year and one has proportional sharing beating date order; with the same-year example, the differential now fails each of those mutants. Co-Authored-By: Claude Opus 5.5 --- .../test_ny_mortgage_debt_limit_invariants.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py index 0b0804e40db..992f9759ab3 100644 --- a/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -244,6 +244,21 @@ def test_new_york_limits_are_the_statutory_amounts(): second=(900_000.0, 2018, 0.0, 150_000.0), status=(2025, "JOINT"), ) +# The same homes with the second loan's year unknown: it counts as the most +# recent, so the main home's loan goes first and the second home's room of +# $50,000 binds. +@example( + first=(900_000.0, 2017, 0.0, 540_000.0), + second=(900_000.0, 0, 0.0, 150_000.0), + status=(2025, "JOINT"), +) +# Loans from different years, where sharing the limit gives more home equity +# debt than date order does. +@example( + first=(900_000.0, 2017, 0.0, 3_000_000.0), + second=(900_000.0, 2018, 0.0, 120_000.0), + status=(2025, "JOINT"), +) def test_new_york_qualified_loan_limit_properties(first, second, status): ny_limits, federal_limits = LIMITS[status] pair = [first, second]