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..b3a1e2a20c3 --- /dev/null +++ b/policyengine_us/tests/core/test_ny_mortgage_debt_limit_invariants.py @@ -0,0 +1,290 @@ +"""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. 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, +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. +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), the limit is line 6 plus the acquisition debt above it, + 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 + lowers the limit. + +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. + +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 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. +""" + +import math +from fractions import Fraction + +import numpy as np +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, + _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. 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 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. + 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) + 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. + 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( + interest=amounts, + first=homes, + second=homes, + status=st.sampled_from(sorted(LIMITS)), +) +def test_new_york_allowed_interest_without_non_acquisition_debt( + interest, first, second, status +): + 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]), + np.array([non_deductible]), + np.array([0.0]), + share, + )[0] + ) + assert deductible - TOLERANCE <= allowed <= interest + TOLERANCE + if non_deductible == 0: + assert allowed == deductible + + +@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) + 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 new file mode 100644 index 00000000000..5ebb83c3d0d --- /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,289 @@ +# 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 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 = 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. + +- 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 = .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 + 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 = .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, only the dollar limit caps 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: 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 + 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 = .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 + 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 = .8571 (worksheet .857); + # $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 + 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 = .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 + 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 = .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 + input: + state_code: NY + deductible_mortgage_interest: 12_000 + 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 + # 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 + 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 + 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: When the worksheet rounds down, New York uses the unrounded share + 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 + ny_first_home_fair_market_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; + # $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 + +- 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: + 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/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_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..24ba2108855 --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_interest_deduction.py @@ -0,0 +1,30 @@ +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 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 + + 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 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..a61b4eb00ae --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_deduction.py @@ -0,0 +1,197 @@ +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)); a value of 0 + means unknown and sets no such limit. 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. + """ + 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 + # 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 + ) + # 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, or the unrounded ratio. + + 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) + # 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) + + +def _ny_allowed_interest( + deductible, non_deductible, non_acquisition_debt, deductible_share +): + """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 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 + uses_worksheet = (non_deductible > 0) | (non_acquisition_debt > 0) + return where(uses_worksheet, worksheet, 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: 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. + "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 + + 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..6334a21a858 --- /dev/null +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_mortgage_interest_inputs.py @@ -0,0 +1,83 @@ +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/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. """ diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 025bb5dd42b..74d0591305a 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.' + 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: