diff --git a/changelog.d/hi-mortgage-interest-cap.fixed.md b/changelog.d/hi-mortgage-interest-cap.fixed.md new file mode 100644 index 00000000000..bb9765576c4 --- /dev/null +++ b/changelog.d/hi-mortgage-interest-cap.fixed.md @@ -0,0 +1 @@ +Apply Hawaii's pre-TCJA mortgage debt limits ($1 million acquisition plus $100,000 home equity debt), round the qualified-debt ratio to three decimal places, and preserve supplied deductible mortgage interest when gross interest is unavailable. Exclude dependents' canonical mortgage-interest inputs from the shared federal aggregate nationwide, changing federal deductions and state deductions that use federal mortgage-interest or itemized-deduction amounts. Use deprecated structured interest only when no tax-unit member reports canonical interest. Preserve filers' own legacy gross interest and supplied deductible interest in Hawaii even when dependents report canonical interest, while excluding dependents' legacy gross interest and supplied deductible interest from Hawaii's mortgage deduction. diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml new file mode 100644 index 00000000000..6b9baedb9d9 --- /dev/null +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/interest/home_acquisition_debt_limit.yaml @@ -0,0 +1,31 @@ +description: Hawaii limits the home acquisition debt on which mortgage interest is deductible to this amount, based on filing status. +SINGLE: + 2018-01-01: 1_000_000 +JOINT: + 2018-01-01: 1_000_000 +HEAD_OF_HOUSEHOLD: + 2018-01-01: 1_000_000 +SEPARATE: + 2018-01-01: 500_000 +SURVIVING_SPOUSE: + 2018-01-01: 1_000_000 +metadata: + propagate_metadata_to_children: true + breakdown: + - filing_status + unit: currency-USD + period: year + label: Hawaii home acquisition debt limit + reference: + # Hawaii makes the TCJA limit in IRC 163(h)(3)(F) inoperative, so the + # pre-TCJA limit in IRC 163(h)(3)(B)(ii) applies. + - title: Hawaii Revised Statutes 235-2.4(j)(3) + href: https://data.capitol.hawaii.gov/hrscurrent/Vol04_Ch0201-0257/HRS0235/HRS_0235-0002_0004.htm + - title: Act 35, SLH 2026 (HB2329 CD1), HRS 235-2.4(i)(3) + href: https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=20 + - title: 2018 Hawaii N-11 Instructions, Line 21c + href: https://files.hawaii.gov/tax/forms/2018/n11ins.pdf#page=19 + - title: 2025 Hawaii N-11 Instructions, Line 21c + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=17 + - title: 2017 IRS Publication 936, Home acquisition debt limit + href: https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=9 diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml new file mode 100644 index 00000000000..72cb63ef0e1 --- /dev/null +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/interest/home_equity_debt_limit.yaml @@ -0,0 +1,31 @@ +description: Hawaii 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 +HEAD_OF_HOUSEHOLD: + 2018-01-01: 100_000 +SEPARATE: + 2018-01-01: 50_000 +SURVIVING_SPOUSE: + 2018-01-01: 100_000 +metadata: + propagate_metadata_to_children: true + breakdown: + - filing_status + unit: currency-USD + period: year + label: Hawaii home equity debt limit + reference: + # Hawaii makes the TCJA suspension of home equity interest in IRC + # 163(h)(3)(F) inoperative, so the limit in IRC 163(h)(3)(C)(ii) applies. + - title: Hawaii Revised Statutes 235-2.4(j)(3) + href: https://data.capitol.hawaii.gov/hrscurrent/Vol04_Ch0201-0257/HRS0235/HRS_0235-0002_0004.htm + - title: Act 35, SLH 2026 (HB2329 CD1), HRS 235-2.4(i)(3) + href: https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=20 + - title: 2018 Hawaii N-11 Instructions, Line 21c + href: https://files.hawaii.gov/tax/forms/2018/n11ins.pdf#page=19 + - title: 2025 Hawaii N-11 Instructions, Line 21c + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=17 + - title: 2017 IRS Publication 936, Home equity debt limit + href: https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=10 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml index c437e3030f3..fa36031f481 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml @@ -144,3 +144,156 @@ output: # Post-TCJA cap: 45,000 x 750,000 / 900,000 = 37,500 deductible_mortgage_interest_tax_unit: 37_500 + +# Publication 936 (2017), pages 2 and 8: only the taxpayer's paid share +# belongs on the return, including a spouse's share on a joint return. +# https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=2 +- name: Dependent-only canonical interest cannot create a federal filer deduction + period: 2024 + input: + people: + head: + age: 45 + is_tax_unit_head: true + dependent: + age: 20 + is_tax_unit_dependent: true + home_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + households: + household: + members: [head, dependent] + state_code: CA + output: + home_mortgage_interest_tax_unit: 0 + deductible_mortgage_interest_tax_unit: 0 + non_deductible_mortgage_interest_tax_unit: 0 + deductible_mortgage_interest: [0, 0] + interest_deduction: 0 + +- name: Dataset-like structured totals cannot restore dependent-only canonical interest + period: 2024 + input: + people: + head: + age: 45 + is_tax_unit_head: true + dependent: + age: 20 + is_tax_unit_dependent: true + home_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + # Dataset exports duplicate canonical interest in deprecated inputs. + first_home_mortgage_interest: 12_000 + households: + household: + members: [head, dependent] + state_code: CA + output: + home_mortgage_interest_tax_unit: 0 + deductible_mortgage_interest_tax_unit: 0 + non_deductible_mortgage_interest_tax_unit: 0 + deductible_mortgage_interest: [0, 0] + interest_deduction: 0 + +- name: Federal mortgage interest includes the single filer's payment only + period: 2024 + input: + people: + head: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 10_000 + dependent: + age: 20 + is_tax_unit_dependent: true + home_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + households: + household: + members: [head, dependent] + state_code: CA + output: + home_mortgage_interest_tax_unit: 10_000 + deductible_mortgage_interest_tax_unit: 10_000 + non_deductible_mortgage_interest_tax_unit: 0 + deductible_mortgage_interest: [10_000, 0] + interest_deduction: 10_000 + +- name: Capped federal interest allocates both spouses' payments without dependent interest + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 30_000 + spouse: + age: 44 + is_tax_unit_spouse: true + home_mortgage_interest: 15_000 + dependent: + age: 20 + is_tax_unit_dependent: true + home_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, spouse, dependent] + filing_status: JOINT + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [head, spouse, dependent] + state_code: CA + output: + # $45,000 * $750,000 / $900,000 = $37,500, split 2:1. + home_mortgage_interest_tax_unit: 45_000 + deductible_mortgage_interest_tax_unit: 37_500 + non_deductible_mortgage_interest_tax_unit: 7_500 + deductible_mortgage_interest: [25_000, 12_500, 0] + non_deductible_mortgage_interest: [5_000, 2_500, 0] + interest_deduction: 37_500 + +- name: No member canonical interest preserves the federal structured-interest fallback + period: 2024 + input: + people: + head: + age: 45 + is_tax_unit_head: true + spouse: + age: 44 + is_tax_unit_spouse: true + dependent: + age: 20 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, dependent] + filing_status: JOINT + first_home_mortgage_interest: 45_000 + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [head, spouse, dependent] + state_code: CA + output: + # With no member canonical payments, each spouse gets half the deduction. + home_mortgage_interest_tax_unit: 45_000 + deductible_mortgage_interest_tax_unit: 37_500 + non_deductible_mortgage_interest_tax_unit: 7_500 + deductible_mortgage_interest: [18_750, 18_750, 0] + non_deductible_mortgage_interest: [3_750, 3_750, 0] + interest_deduction: 37_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_interest_deduction.yaml new file mode 100644 index 00000000000..bd1948563f3 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_interest_deduction.yaml @@ -0,0 +1,97 @@ +- name: Case 1, a $1.5 million mortgage deducts interest on $1.1 million of debt. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 90_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [person1] + state_code: HI + output: + # Table 1, line 11: round($1,100,000 / $1,500,000, 3) * $90,000. + hi_interest_deduction: 65_970 + +- name: Case 2, mortgage interest is capped and the bundled investment-interest input is preserved. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 65_000 + investment_interest_expense: 5_000 + taxable_interest_income: 2_000 + person2: + age: 45 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + first_home_mortgage_balance: 800_000 + first_home_mortgage_origination_year: 2017 + second_home_mortgage_balance: 500_000 + second_home_mortgage_origination_year: 2020 + households: + household: + members: [person1, person2] + state_code: HI + output: + # Mortgage: round($1,100,000 / $1,300,000, 3) * $65,000 = $54,990. + hi_mortgage_interest_deduction: 54_990 + # Preserve the bundled $5,000 input pending the dataset leaf-input split. + hi_interest_deduction: 59_990 + +- name: Case 3, mortgage interest within the debt limit and the bundled input are deductible. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 20_000 + investment_interest_expense: 1_000 + qualified_dividend_income: 4_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 400_000 + first_home_mortgage_origination_year: 2021 + households: + household: + members: [person1] + state_code: HI + output: + hi_interest_deduction: 21_000 + +- name: Case 4, supplied deductible mortgage interest with a balance is kept alongside the bundled input. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + deductible_mortgage_interest: 45_000 + investment_interest_expense: 1_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 45_000 + hi_interest_deduction: 46_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.yaml new file mode 100644 index 00000000000..47323dbb83c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.yaml @@ -0,0 +1,678 @@ +- name: Case 1, a 2023 mortgage over the limits keeps the pre-TCJA limits in Hawaii. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 90_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [person1] + state_code: HI + output: + # Federal: $750,000 / $1,500,000 * $90,000 = $45,000. + deductible_mortgage_interest_tax_unit: 45_000 + # Hawaii (2017 Pub 936 Table 1): $1,000,000 acquisition debt plus + # $100,000 of the excess as home equity debt. + # Line 11 rounds $1,100,000 / $1,500,000 to 0.733. + # 0.733 * $90,000 = $65,970. + hi_mortgage_interest_deduction: 65_970 + +- name: Case 2, a post-TCJA mortgage under $1 million is fully deductible in Hawaii. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 45_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 900_000 + first_home_mortgage_origination_year: 2018 + households: + household: + members: [person1] + state_code: HI + output: + # Federal: $750,000 / $900,000 * $45,000 = $37,500. + deductible_mortgage_interest_tax_unit: 37_500 + hi_mortgage_interest_deduction: 45_000 + +- name: Case 3, married filing separately uses the $500,000 and $50,000 limits. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 30_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SEPARATE + first_home_mortgage_balance: 600_000 + first_home_mortgage_origination_year: 2019 + households: + household: + members: [person1] + state_code: HI + output: + # Federal: $375,000 / $600,000 * $30,000 = $18,750. + deductible_mortgage_interest_tax_unit: 18_750 + # Hawaii: round(($500,000 + $50,000) / $600,000, 3) = 0.917. + # 0.917 * $30,000 = $27,510. + hi_mortgage_interest_deduction: 27_510 + +- name: Case 4, joint filers with two mortgages apply the limits to the combined balance. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 40_000 + person2: + age: 45 + home_mortgage_interest: 25_000 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + first_home_mortgage_balance: 800_000 + first_home_mortgage_origination_year: 2017 + second_home_mortgage_balance: 500_000 + second_home_mortgage_origination_year: 2020 + households: + household: + members: [person1, person2] + state_code: HI + output: + # Federal: $800,000 / $1,300,000 * $65,000 = $40,000. + deductible_mortgage_interest_tax_unit: 40_000 + # Hawaii: round($1,100,000 / $1,300,000, 3) * $65,000 = $54,990. + hi_mortgage_interest_deduction: 54_990 + +- name: Case 5, debt exactly at the combined $1.1 million limit is fully deductible. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 55_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1_100_000 + first_home_mortgage_origination_year: 2024 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 55_000 + +- name: Case 6, debt between $1 million and $1.1 million is fully deductible. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 52_500 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1_050_000 + first_home_mortgage_origination_year: 2024 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 52_500 + +- name: Case 7, head of household in 2018, the first year Hawaii decoupled. + period: 2018 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 100_000 + person2: + age: 10 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: HEAD_OF_HOUSEHOLD + first_home_mortgage_balance: 2_000_000 + first_home_mortgage_origination_year: 2018 + households: + household: + members: [person1, person2] + state_code: HI + output: + # $1,100,000 / $2,000,000 * $100,000 = $55,000. + hi_mortgage_interest_deduction: 55_000 + +- name: Case 8, surviving spouse uses the $1 million and $100,000 limits. + period: 2022 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 60 + home_mortgage_interest: 60_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SURVIVING_SPOUSE + first_home_mortgage_balance: 1_200_000 + first_home_mortgage_origination_year: 2019 + households: + household: + members: [person1] + state_code: HI + output: + # round($1,100,000 / $1,200,000, 3) * $60,000 = $55,020. + hi_mortgage_interest_deduction: 55_020 + +- name: Case 9, interest without a reported balance is fully deductible. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 10_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 10_000 + +- name: Case 10, a directly supplied deductible mortgage interest input is kept. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + deductible_mortgage_interest: 20_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 20_000 + +- name: Case 11, no mortgage means no deduction. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 0 + +- name: Case 12, a very large mortgage is limited to $1.1 million of debt. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 150_000 + tax_units: + tax_unit: + members: [person1] + filing_status: JOINT + first_home_mortgage_balance: 3_000_000 + first_home_mortgage_origination_year: 2010 + households: + household: + members: [person1] + state_code: HI + output: + # round($1,100,000 / $3,000,000, 3) * $150,000 = $55,050. + hi_mortgage_interest_deduction: 55_050 + +- name: Case 13, supplied deductible interest is not capped again when only a balance is supplied. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + deductible_mortgage_interest: 45000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1500000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 45000 + +- name: Case 14, canonical gross interest takes priority over a direct legacy gross amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 12000 + mortgage_interest: 90000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1500000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 8796 + +- name: Case 15, deprecated structured gross interest still receives the Hawaii debt cap. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1500000 + first_home_mortgage_origination_year: 2023 + first_home_mortgage_interest: 90000 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 65970 + +- name: Case 16, direct legacy gross interest still receives the Hawaii debt cap. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + mortgage_interest: 90000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1500000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 65970 + +- name: Case 17, supplied deductible and nondeductible components reconstruct gross interest. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + deductible_mortgage_interest: 45000 + non_deductible_mortgage_interest: 45000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1500000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 65970 + +- name: Case 18, canonical gross interest is used with a supplied federal deduction. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 90000 + deductible_mortgage_interest: 45000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1500000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 65970 + +- name: Case 19, canonical gross interest takes priority over deprecated structured interest. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 12000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1500000 + first_home_mortgage_origination_year: 2023 + first_home_mortgage_interest: 90000 + households: + household: + members: [person1] + state_code: HI + output: + hi_mortgage_interest_deduction: 8796 + +- name: Case 20, gross interest and deductible-only fallbacks are handled per tax unit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + home_mortgage_interest: 90000 + person2: + age: 45 + deductible_mortgage_interest: 45000 + tax_units: + gross_interest_unit: + members: [person1] + filing_status: SINGLE + first_home_mortgage_balance: 1500000 + first_home_mortgage_origination_year: 2023 + deductible_only_unit: + members: [person2] + filing_status: SINGLE + first_home_mortgage_balance: 1500000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [person1, person2] + state_code: HI + output: + hi_mortgage_interest_deduction: [65970, 45000] + +# 2017 Publication 936, page 2: deduct interest paid on a home owned by +# the filer (or spouse on a joint return), excluding a dependent's own home. +- name: Case 21, dependent canonical interest preserves the filer's legacy gross-interest fallback. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + legacy_filer: + age: 45 + mortgage_interest: 90_000 + dependent_owner: + age: 20 + is_tax_unit_dependent: true + home_mortgage_interest: 12_000 + canonical_filer: + age: 45 + mortgage_interest: 90_000 + home_mortgage_interest: 12_000 + control_dependent: + age: 20 + is_tax_unit_dependent: true + tax_units: + dependent_interest_unit: + members: [legacy_filer, dependent_owner] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + filer_interest_unit: + members: [canonical_filer, control_dependent] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [legacy_filer, dependent_owner, canonical_filer, control_dependent] + state_code: HI + output: + # The dependent's payment cannot erase the filer's own legacy interest: + # $90,000 * 0.733 = $65,970. The other unit's canonical filer interest + # takes priority over legacy gross: $12,000 * 0.733 = $8,796. + hi_mortgage_interest_deduction: [65_970, 8_796] + +- name: Case 22, joint filers combine both spouses' interest without dependent interest. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + is_tax_unit_head: true + home_mortgage_interest: 40_000 + spouse: + age: 44 + is_tax_unit_spouse: true + home_mortgage_interest: 25_000 + dependent: + age: 20 + is_tax_unit_dependent: true + home_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, spouse, dependent] + filing_status: JOINT + first_home_mortgage_balance: 800_000 + first_home_mortgage_origination_year: 2017 + second_home_mortgage_balance: 500_000 + second_home_mortgage_origination_year: 2020 + households: + household: + members: [head, spouse, dependent] + state_code: HI + output: + # $65,000 * round($1,100,000 / $1,300,000, 3) = $54,990. + hi_mortgage_interest_deduction: 54_990 + +- name: Case 23, dependent-only canonical interest suppresses matching dataset structured interest. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + dependent: + age: 20 + is_tax_unit_dependent: true + home_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + first_home_mortgage_interest: 12_000 + households: + household: + members: [head, dependent] + state_code: HI + output: + # The federal aggregate already excludes the dependent's payment and its + # matching structured export, so the calculated filer fallbacks are zero. + home_mortgage_interest_tax_unit: 0 + deductible_mortgage_interest_tax_unit: 0 + hi_mortgage_interest_deduction: 0 + +- name: Case 24, supplied deductible interest excludes dependent amounts. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + deductible_mortgage_interest: 45_000 + dependent: + age: 20 + is_tax_unit_dependent: true + deductible_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [head, dependent] + state_code: HI + output: + hi_mortgage_interest_deduction: 45_000 + +- name: Case 25, legacy gross interest excludes dependent amounts. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + mortgage_interest: 90_000 + dependent: + age: 20 + is_tax_unit_dependent: true + mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [head, dependent] + state_code: HI + output: + hi_mortgage_interest_deduction: 65_970 + +- name: Case 26, dependent-only canonical interest cannot create a filer deduction. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + dependent: + age: 20 + is_tax_unit_dependent: true + home_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [head, dependent] + state_code: HI + output: + hi_mortgage_interest_deduction: 0 + +- name: Case 27, dependent canonical interest preserves the filer's supplied deductible fallback. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + deductible_mortgage_interest: 45_000 + dependent: + age: 20 + is_tax_unit_dependent: true + home_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [head, dependent] + state_code: HI + output: + # The head's supplied deduction belongs to the filer, and has already + # been limited. Neither the dependent's payment nor a second cap reduces it. + hi_mortgage_interest_deduction: 45_000 + +- name: Case 28, dependent-only supplied deductible interest cannot create a Hawaii filer deduction. + period: 2026 + input: + people: + head: + age: 45 + dependent: + age: 20 + is_tax_unit_dependent: true + deductible_mortgage_interest: 12_000 + tax_units: + tax_unit: + members: [head, dependent] + filing_status: SINGLE + first_home_mortgage_balance: 1_500_000 + first_home_mortgage_origination_year: 2023 + households: + household: + members: [head, dependent] + state_code: HI + output: + # Pub. 936 (2017), p. 2: the dependent's paid share is not the filer's. + hi_mortgage_interest_deduction: 0 diff --git a/policyengine_us/tests/test_federal_mortgage_interest_dependents.py b/policyengine_us/tests/test_federal_mortgage_interest_dependents.py new file mode 100644 index 00000000000..482af2a5688 --- /dev/null +++ b/policyengine_us/tests/test_federal_mortgage_interest_dependents.py @@ -0,0 +1,263 @@ +import random +from itertools import product + +import numpy as np + +from policyengine_us import Simulation + + +PERIOD = "2026" +MORTGAGE_CASES = ( + ("no-debt", 0, 2023), + ("post-tcja-uncapped", 600_000, 2023), + ("post-tcja-capped", 900_000, 2023), + ("pre-tcja-uncapped", 900_000, 2017), + ("pre-tcja-capped", 1_500_000, 2017), +) + + +def _add_tax_unit(situation, name, joint, filers, mortgage, dependent_interest): + members = [] + filer_names = [] + filer_indices = [] + dependent_indices = [] + + for index, interest in enumerate(filers): + person_name = f"{name}_filer_{index}" + members.append(person_name) + filer_names.append(person_name) + filer_indices.append(len(situation["people"])) + inputs = { + "age": 45 - index, + "is_tax_unit_head": index == 0, + "is_tax_unit_spouse": index == 1, + "is_tax_unit_dependent": False, + **interest, + } + situation["people"][person_name] = { + variable: {PERIOD: value} for variable, value in inputs.items() + } + + for index, interest in enumerate(dependent_interest): + person_name = f"{name}_dependent_{index}" + members.append(person_name) + dependent_indices.append(len(situation["people"])) + inputs = { + "age": 20 - index, + "is_tax_unit_head": False, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": True, + "home_mortgage_interest": interest, + } + situation["people"][person_name] = { + variable: {PERIOD: value} for variable, value in inputs.items() + } + situation["marital_units"][person_name] = {"members": [person_name]} + + situation["tax_units"][name] = { + "members": members, + "filing_status": {PERIOD: "JOINT" if joint else "SINGLE"}, + **{variable: {PERIOD: value} for variable, value in mortgage.items()}, + } + situation["households"][name] = { + "members": members, + "state_code": {PERIOD: "CA"}, + } + situation["families"][name] = {"members": members} + situation["spm_units"][name] = {"members": members} + situation["marital_units"][name] = {"members": filer_names} + return filer_indices, dependent_indices + + +def test_dependents_do_not_change_federal_mortgage_interest_or_allocations(): + """Matched returns conserve interest and allocate it only to filers. + + Varying dependents' own interest cannot change federal aggregates or + filer allocations. Canonical payments determine proportional shares; + zero filer payments use equal shares. Dataset-like structured totals track + all members' canonical payments, including dependents, without changing + filer aggregates. No deduction outputs are supplied. + + A fixed structured fallback is eligible only while nobody reports canonical + interest. Adding the first canonical payment intentionally changes its + eligibility; the structured pairs keep dependent canonical payments positive + on both sides, and YAML covers the eligible fallback separately. + """ + rng = random.Random(9950) + situation = { + entity: {} + for entity in ( + "people", + "tax_units", + "households", + "families", + "spm_units", + "marital_units", + ) + } + records = [] + + for joint, source, mortgage_case, dependent_count in product( + (False, True), + ( + "canonical", + "dataset-like-canonical", + "dataset-like-dependent-only", + "structured", + "none", + ), + MORTGAGE_CASES, + (1, 2, 3), + ): + debt_label, balance, origination_year = mortgage_case + filer_count = 2 if joint else 1 + filers = [ + {"home_mortgage_interest": rng.randint(1_000, 60_000)} + if source in ("canonical", "dataset-like-canonical") + else {} + for _ in range(filer_count) + ] + if ( + joint + and source in ("canonical", "dataset-like-canonical") + and dependent_count < 3 + ): + # Include head-only, spouse-only, and two-payer joint returns. + filers[dependent_count - 1] = {} + payments = np.array( + [filer.get("home_mortgage_interest", 0) for filer in filers] + ) + mortgage = { + "first_home_mortgage_balance": balance, + "first_home_mortgage_origination_year": origination_year, + } + if source == "structured": + mortgage["first_home_mortgage_interest"] = rng.randint(1_000, 120_000) + gross_interest = payments.sum() + expected_shares = ( + payments / payments.sum() + if payments.sum() > 0 + else np.full(filer_count, 1 / filer_count) + ) + label = ( + f"{'joint' if joint else 'single'}-{source}-{debt_label}" + f"-{dependent_count}-dependents" + ) + dependent_payments = [rng.randint(1, 200_000) for _ in range(dependent_count)] + before_dependent_payments = ( + [rng.randint(1, 1_000) for _ in range(dependent_count)] + if source in ("structured", "dataset-like-dependent-only") + else [0] * dependent_count + ) + for suffix, dependent_interest in ( + ("before", before_dependent_payments), + ("after", dependent_payments), + ): + name = f"{label}-{suffix}" + pair_mortgage = mortgage.copy() + if source.startswith("dataset-like"): + # Dataset exports duplicate the all-member canonical total. + # Its dependent component must not enter the filers' return. + all_member_interest = payments.sum() + sum(dependent_interest) + pair_mortgage["first_home_mortgage_interest"] = all_member_interest / 2 + pair_mortgage["second_home_mortgage_interest"] = all_member_interest / 2 + filer_indices, dependent_indices = _add_tax_unit( + situation, name, joint, filers, pair_mortgage, dependent_interest + ) + records.append( + ( + name, + filer_indices, + dependent_indices, + gross_interest, + expected_shares, + ) + ) + + # One model construction covers 150 matched pairs, including both vintages. + simulation = Simulation(situation=situation) + gross = simulation.calculate("home_mortgage_interest_tax_unit", PERIOD) + deductible = simulation.calculate("deductible_mortgage_interest_tax_unit", PERIOD) + non_deductible = simulation.calculate( + "non_deductible_mortgage_interest_tax_unit", PERIOD + ) + person_deductible = simulation.calculate("deductible_mortgage_interest", PERIOD) + person_non_deductible = simulation.calculate( + "non_deductible_mortgage_interest", PERIOD + ) + shares = simulation.calculate("home_mortgage_interest_share", PERIOD) + + for index, (name, filers, dependents, expected_gross, expected_shares) in enumerate( + records + ): + np.testing.assert_allclose( + gross[index], expected_gross, rtol=0, atol=0.01, err_msg=name + ) + np.testing.assert_allclose( + deductible[index] + non_deductible[index], + gross[index], + rtol=0, + atol=0.01, + err_msg=name, + ) + assert deductible[index] >= 0, name + assert non_deductible[index] >= 0, name + np.testing.assert_allclose( + shares[filers], expected_shares, rtol=0, atol=1e-7, err_msg=name + ) + np.testing.assert_allclose(shares[dependents], 0, rtol=0, atol=0, err_msg=name) + for person_values, tax_unit_values in ( + (person_deductible, deductible), + (person_non_deductible, non_deductible), + ): + np.testing.assert_allclose( + person_values[dependents], 0, rtol=0, atol=0, err_msg=name + ) + np.testing.assert_allclose( + person_values[filers].sum(), + tax_unit_values[index], + rtol=0, + atol=0.01, + err_msg=name, + ) + np.testing.assert_allclose( + person_values[filers], + expected_shares * tax_unit_values[index], + rtol=0, + atol=0.01, + err_msg=name, + ) + np.testing.assert_allclose( + person_deductible[filers] + person_non_deductible[filers], + expected_shares * expected_gross, + rtol=0, + atol=0.01, + err_msg=name, + ) + + for index in range(0, len(records), 2): + name, before_filers, *_ = records[index] + after_filers = records[index + 1][1] + for variable, values in ( + ("gross", gross), + ("deductible", deductible), + ("non-deductible", non_deductible), + ): + np.testing.assert_allclose( + values[index + 1], + values[index], + rtol=0, + atol=0.01, + err_msg=f"{name}: dependent inputs changed federal {variable}", + ) + for variable, values in ( + ("deductible", person_deductible), + ("non-deductible", person_non_deductible), + ): + np.testing.assert_allclose( + values[after_filers], + values[before_filers], + rtol=0, + atol=0.01, + err_msg=f"{name}: dependent inputs changed filer {variable}", + ) diff --git a/policyengine_us/tests/test_hi_mortgage_interest_dependents.py b/policyengine_us/tests/test_hi_mortgage_interest_dependents.py new file mode 100644 index 00000000000..14586538e91 --- /dev/null +++ b/policyengine_us/tests/test_hi_mortgage_interest_dependents.py @@ -0,0 +1,244 @@ +import random + +import numpy as np +import pytest + +from policyengine_us import Simulation + + +PERIOD = "2026" +MORTGAGE_INPUTS = ( + "home_mortgage_interest", + "mortgage_interest", + "deductible_mortgage_interest", + "non_deductible_mortgage_interest", +) +INTEREST_SOURCES = ( + "canonical", + "canonical_with_supplied_deduction", + "legacy", + "reconstructed", + "supplied_deduction", + "structured_with_filer_legacy", + "dataset_like", + "dataset_like_dependent_only", + "none", +) + + +def _filer_interest(source, amount): + if source in ( + "canonical", + "canonical_with_supplied_deduction", + "dataset_like", + ): + inputs = {"home_mortgage_interest": amount} + if source == "canonical_with_supplied_deduction": + inputs["deductible_mortgage_interest"] = amount / 2 + return inputs + if source == "legacy": + return {"mortgage_interest": amount} + if source == "reconstructed": + return { + "deductible_mortgage_interest": amount / 2, + "non_deductible_mortgage_interest": amount / 2, + } + if source == "supplied_deduction": + return {"deductible_mortgage_interest": amount} + return {} + + +def _add_tax_unit(situation, name, joint, filers, mortgage, dependents): + members = [] + filer_names = [] + for index, interest in enumerate(filers): + person_name = f"{name}_filer_{index}" + members.append(person_name) + filer_names.append(person_name) + inputs = { + "age": 45 - index, + "is_tax_unit_head": index == 0, + "is_tax_unit_spouse": index == 1, + "is_tax_unit_dependent": False, + **interest, + } + situation["people"][person_name] = { + variable: {PERIOD: value} for variable, value in inputs.items() + } + + for index, interest in enumerate(dependents): + person_name = f"{name}_dependent_{index}" + members.append(person_name) + inputs = { + "age": 15 - index, + "is_tax_unit_head": False, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": True, + **interest, + } + situation["people"][person_name] = { + variable: {PERIOD: value} for variable, value in inputs.items() + } + situation["marital_units"][person_name] = {"members": [person_name]} + + situation["tax_units"][name] = { + "members": members, + "filing_status": {PERIOD: "JOINT" if joint else "SINGLE"}, + **{variable: {PERIOD: value} for variable, value in mortgage.items()}, + } + situation["households"][name] = { + "members": members, + "state_code": {PERIOD: "HI"}, + } + situation["families"][name] = {"members": members} + situation["spm_units"][name] = {"members": members} + situation["marital_units"][name] = {"members": filer_names} + + +@pytest.mark.parametrize("reconstructed", (False, True)) +def test_dependent_mortgage_inputs_never_change_hawaii_deduction(reconstructed): + """Dependent payments cannot change deductions with filer payments fixed. + + Dataset-like structured inputs equal all members' canonical payments; + changing dependent payments and these matching totals leaves the filers' + deduction unchanged, including when dependent payments go from zero to + positive. Structured exports also have their totals explicitly attributed + to filers through legacy gross inputs, so disabling the federal structured + fallback cannot erase those filers' payments from the Hawaii deduction. + """ + rng = random.Random(9950) + situation = { + entity: {} + for entity in ( + "people", + "tax_units", + "households", + "families", + "spm_units", + "marital_units", + ) + } + labels = [] + expected = [] + sources = ( + ("reconstructed",) + if reconstructed + else tuple(source for source in INTEREST_SOURCES if source != "reconstructed") + ) + + for joint in (False, True): + for source in sources: + # Exercise each person input alone, then arbitrary combinations. + for sample in range(len(MORTGAGE_INPUTS) + 2): + filers = [ + _filer_interest(source, rng.randint(1_000, 150_000)) + for _ in range(2 if joint else 1) + ] + debt = (0, 550_000, 1_100_000, 1_500_000, 3_000_000)[sample % 5] + mortgage = { + "first_home_mortgage_balance": debt * 0.75, + "second_home_mortgage_balance": debt * 0.25, + "first_home_mortgage_origination_year": rng.choice( + (2010, 2017, 2023) + ), + "second_home_mortgage_origination_year": 2023, + } + if source == "structured_with_filer_legacy": + mortgage["first_home_mortgage_interest"] = rng.randint( + 1_000, 150_000 + ) + mortgage["second_home_mortgage_interest"] = rng.randint( + 1_000, 150_000 + ) + structured_total = ( + mortgage["first_home_mortgage_interest"] + + mortgage["second_home_mortgage_interest"] + ) + filers = [ + {"mortgage_interest": structured_total / len(filers)} + for _ in filers + ] + + variables = ( + (MORTGAGE_INPUTS[sample],) + if sample < len(MORTGAGE_INPUTS) + else MORTGAGE_INPUTS + ) + dependents = [ + {variable: rng.randint(1, 200_000) for variable in variables} + for _ in range(1 + sample % 3) + ] + if reconstructed: + # Supplying a variable for one person installs a complete + # input vector, defaulting omitted persons to zero. Keep + # mortgage_interest calculated in this separate batch. + for inputs in dependents: + legacy_gross = inputs.pop("mortgage_interest", None) + if legacy_gross is not None and len(inputs) == 0: + inputs["deductible_mortgage_interest"] = legacy_gross / 2 + inputs["non_deductible_mortgage_interest"] = ( + legacy_gross / 2 + ) + label = f"{'joint' if joint else 'single'}-{source}-{sample}" + labels.append(label) + before_dependents = [{} for _ in dependents] + + filer_total = sum(sum(inputs.values()) for inputs in filers) + if source == "canonical_with_supplied_deduction": + filer_total = sum( + inputs["home_mortgage_interest"] for inputs in filers + ) + if source == "structured_with_filer_legacy": + filer_total = ( + mortgage["first_home_mortgage_interest"] + + mortgage["second_home_mortgage_interest"] + ) + share = round(min(debt, 1_100_000) / debt, 3) if debt else 1 + expected.append( + filer_total + if source == "supplied_deduction" + else filer_total * share + ) + # Membership, filer inputs, and tax-unit debt remain identical. + # Dataset-like exports additionally mirror all members' + # canonical interest into the deprecated structured totals. + for suffix, dependent_inputs in ( + ("before", before_dependents), + ("after", dependents), + ): + tax_unit_inputs = mortgage.copy() + if source in ("dataset_like", "dataset_like_dependent_only"): + all_member_total = sum( + inputs.get("home_mortgage_interest", 0) + for inputs in filers + dependent_inputs + ) + tax_unit_inputs["first_home_mortgage_interest"] = ( + all_member_total * 0.75 + ) + tax_unit_inputs["second_home_mortgage_interest"] = ( + all_member_total * 0.25 + ) + _add_tax_unit( + situation, + f"{label}-{suffix}", + joint, + filers, + tax_unit_inputs, + dependent_inputs, + ) + + # One vectorized calculation keeps the generated cases inexpensive. + deduction = Simulation(situation=situation).calculate( + "hi_mortgage_interest_deduction", PERIOD + ) + before, after = deduction[::2], deduction[1::2] + failures = [ + f"{label}: {old} -> {new}" + for label, old, new in zip(labels, before, after) + if abs(old - new) > 0.01 + ] + np.testing.assert_allclose( + after, before, rtol=0, atol=0.01, err_msg="\n".join(failures) + ) + np.testing.assert_allclose(before, expected, rtol=0, atol=0.02) + np.testing.assert_allclose(after, expected, rtol=0, atol=0.02) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_interest_deduction.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_interest_deduction.py index 4a58a37629d..d453c8b53a3 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_interest_deduction.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_interest_deduction.py @@ -6,11 +6,12 @@ class hi_interest_deduction(Variable): entity = TaxUnit label = "Hawaii interest deduction" unit = USD - documentation = ( - "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=17\n" - "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=32" # total itemized deduction worksheet + documentation = "Worksheet A-3 line 14: home mortgage and investment interest." + reference = ( + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=17", + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34", ) definition_period = YEAR defined_for = StateCode.HI - adds = ["investment_interest_expense", "mortgage_interest"] + adds = ["hi_mortgage_interest_deduction", "investment_interest_expense"] 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 new file mode 100644 index 00000000000..732a8f60beb --- /dev/null +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_mortgage_interest_deduction.py @@ -0,0 +1,75 @@ +from policyengine_us.model_api import * + + +class hi_mortgage_interest_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Hawaii home mortgage interest deduction" + unit = USD + documentation = ( + "Home mortgage interest Hawaii allows on Worksheet A-3. Hawaii makes " + "IRC 163(h)(3)(F) inoperative, so the pre-TCJA home acquisition and " + "home equity debt limits apply regardless of when the debt arose." + ) + reference = ( + "https://data.capitol.hawaii.gov/hrscurrent/Vol04_Ch0201-0257/HRS0235/HRS_0235-0002_0004.htm", + "https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=20", + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=17", + "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=2", + "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=11", + ) + definition_period = YEAR + defined_for = StateCode.HI + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.hi.tax.income.deductions.itemized.interest + filing_status = tax_unit("filing_status", period) + # 2017 Publication 936, Table 1. Grandfathered debt from before + # October 14, 1987 (line 1) is not modeled, so every balance is home + # acquisition debt (line 2). + debt = add( + tax_unit, + period, + ["first_home_mortgage_balance", "second_home_mortgage_balance"], + ) + # Line 6. + acquisition_debt = min_(debt, p.home_acquisition_debt_limit[filing_status]) + # Line 7. Debt over the home acquisition debt limit may qualify as home + # equity debt. The fair market value limit on home equity debt is not + # modeled. + home_equity_debt = min_( + debt - acquisition_debt, p.home_equity_debt_limit[filing_status] + ) + # Line 8. + qualified_loan_limit = acquisition_debt + home_equity_debt + # Line 11. Without a reported balance, the interest is treated as + # within the limits, as in the federal calculation. + deductible_share = np.divide( + qualified_loan_limit, + debt, + out=np.ones_like(debt), + where=debt > 0, + ) + # Pub. 936 (2017), Table 1, line 11 rounds the ratio to three places. + deductible_share = np.round(deductible_share, 3) + # Line 10 uses total interest paid, before the federal debt cap. + # This also preserves the canonical filer input's priority over + # the deprecated first/second-home interest inputs. + gross_interest = tax_unit("home_mortgage_interest_tax_unit", period) + # These fallbacks already include only the filers' own payments. + legacy_interest = tax_unit_non_dep_add(tax_unit, period, ["mortgage_interest"]) + supplied_deduction = tax_unit_non_dep_add( + tax_unit, period, ["deductible_mortgage_interest"] + ) + # Legacy gross interest can be supplied directly or reconstructed + # from deductible and non-deductible components. Deductible interest + # alone has already been limited and must not receive a second cap. + has_gross_interest = (gross_interest > 0) | ( + legacy_interest > supplied_deduction + ) + interest = where(gross_interest > 0, gross_interest, legacy_interest) + # Line 12. Retain the supplied-deductible fallback when gross interest + # is unavailable, even if a mortgage balance has been reported. + return where( + has_gross_interest, interest * deductible_share, supplied_deduction + ) diff --git a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py index fed7ca75751..2af48bd9a2a 100644 --- a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py +++ b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py @@ -62,7 +62,8 @@ class first_home_mortgage_interest(Variable): documentation = ( "DEPRECATED (issue #9275): use the person-level home_mortgage_interest " "input instead; the deduction only ever uses the first+second sum, and " - "this input is read only when no person-level interest is reported. " + "this input is read only when no tax-unit member, including a " + "dependent, reports person-level home_mortgage_interest. " "Kept temporarily so existing datasets that supply it keep working; " "removal is scheduled once certified microdata stops exporting it." ) @@ -78,7 +79,8 @@ class second_home_mortgage_interest(Variable): documentation = ( "DEPRECATED (issue #9275): use the person-level home_mortgage_interest " "input instead; the deduction only ever uses the first+second sum, and " - "this input is read only when no person-level interest is reported. " + "this input is read only when no tax-unit member, including a " + "dependent, reports person-level home_mortgage_interest. " "Kept temporarily so existing datasets that supply it keep working; " "removal is scheduled once certified microdata stops exporting it." ) @@ -111,20 +113,30 @@ class home_mortgage_interest_tax_unit(Variable): unit = USD definition_period = YEAR documentation = ( - "Total home mortgage interest. The person-level home_mortgage_interest " + "Total home mortgage interest paid by the tax unit's head and spouse. " + "The person-level home_mortgage_interest " "input is canonical; the deprecated structured first/second interest " - "inputs are used only when no person-level interest is reported " + "inputs are used only when no tax-unit member, including a dependent, " + "reports person-level interest " "(existing datasets still supply them — see issue #9275)." ) + reference = "https://www.irs.gov/pub/irs-prior/p936--2017.pdf#page=2" + def formula(tax_unit, period, parameters): - reported_interest = add(tax_unit, period, ["home_mortgage_interest"]) + # Only filers' own interest belongs on their return (Pub. 936, p. 2). + all_member_reported_interest = add(tax_unit, period, ["home_mortgage_interest"]) + reported_interest = tax_unit_non_dep_add( + tax_unit, period, ["home_mortgage_interest"] + ) structured_interest = add( tax_unit, period, ["first_home_mortgage_interest", "second_home_mortgage_interest"], ) - return where(reported_interest > 0, reported_interest, structured_interest) + return where( + all_member_reported_interest > 0, reported_interest, structured_interest + ) class deductible_mortgage_interest_tax_unit(Variable): @@ -145,8 +157,8 @@ def formula(tax_unit, period, parameters): first_year = tax_unit("first_home_mortgage_origination_year", period) second_year = tax_unit("second_home_mortgage_origination_year", period) total_balance = first_balance + second_balance - # Falls back to reported person-level interest when the structured - # first/second inputs are absent. + # Canonical payments take priority; structured first/second interest + # is used only when no member reports person-level interest. total_interest = tax_unit("home_mortgage_interest_tax_unit", period) filing_status = tax_unit("filing_status", period)