From d0d844addb8f468e76a163637dab173e8c1ea4ff Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 16:12:44 -0400 Subject: [PATCH 1/5] Fix Hawaii itemized deduction limitation thresholds and unlimited case hi_itemized_deductions picked the reduced amount using the federal 2009 personal exemption phase-out thresholds (250,200 joint, 208,500 head of household, 125,100 separate) instead of Hawaii's 166,800 / 83,400, and returned 0 when medical, investment interest and casualty deductions made up the whole total. Replace hi_reduced_itemized_deductions with hi_itemized_deductions_reduction (worksheet lines 2-10) on Hawaii-owned parameters for the 3% and 80% rates and the excluded deductions. Co-Authored-By: Claude Opus 5.5 --- .../itemized/limitation/agi_rate.yaml | 19 +++ .../limitation/excluded_deductions.yaml | 20 +++ .../limitation/itemized_deduction_rate.yaml | 19 +++ .../itemized/threshold/reduction.yaml | 16 ++- .../tax/income/deductions/hi_deductions.yaml | 10 +- .../itemized/hi_itemized_deductions.yaml | 123 +++++++++++++++++- .../hi_itemized_deductions_reduction.yaml | 114 ++++++++++++++++ .../hi_reduced_itemized_deductions.yaml | 69 ---------- .../itemized/hi_itemized_deductions.py | 28 ++-- .../hi_itemized_deductions_reduction.py | 39 ++++++ .../hi_reduced_itemized_deductions.py | 56 -------- 11 files changed, 351 insertions(+), 162 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/agi_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/excluded_deductions.yaml create mode 100644 policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/itemized_deduction_rate.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions_reduction.yaml delete mode 100644 policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_reduced_itemized_deductions.yaml create mode 100644 policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions_reduction.py delete mode 100644 policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_reduced_itemized_deductions.py diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/agi_rate.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/agi_rate.yaml new file mode 100644 index 00000000000..36855e20ef1 --- /dev/null +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/agi_rate.yaml @@ -0,0 +1,19 @@ +description: Hawaii reduces itemized deductions by this share of Hawaii adjusted gross income above the itemized deductions reduction threshold, under the overall limitation on itemized deductions. +values: + 2021-01-01: 0.03 +metadata: + unit: /1 + period: year + label: Hawaii itemized deductions limitation adjusted gross income rate + reference: + # Section 68 stays operative for Hawaii "in the form that it existed as of + # December 31, 2024" (Act 35, SLH 2026, for taxable years beginning after + # December 31, 2025); section 68(a)(1) sets the 3 percent rate. + - title: Act 35, SLH 2026 (H.B. 2329, C.D. 1), Section 3, amending HRS 235-2.4(b) + href: https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=18 + - title: 26 U.S. Code 68(a)(1) (2024 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapB-partI-sec68.htm + - title: 2021 Hawaii N-11 Instructions, Total Itemized Deductions Worksheet, line 9 + href: https://files.hawaii.gov/tax/forms/2021/n11ins.pdf#page=32 + - title: 2025 Hawaii N-11 Instructions, Total Itemized Deductions Worksheet, line 9 + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34 diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/excluded_deductions.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/excluded_deductions.yaml new file mode 100644 index 00000000000..1f8d53c4c3a --- /dev/null +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/excluded_deductions.yaml @@ -0,0 +1,20 @@ +description: Hawaii excludes these deductions from the itemized deductions that the 80 percent ceiling of the overall limitation on itemized deductions applies to. +values: + # Section 68(c) excludes medical expenses, investment interest, and + # casualty, theft and wagering losses. PolicyEngine's Hawaii itemized + # deductions do not include wagering losses. + 2021-01-01: + - hi_medical_expense_deduction + - investment_interest_expense + - hi_casualty_loss_deduction +metadata: + unit: list + period: year + label: Hawaii itemized deductions excluded from the limitation ceiling + reference: + - title: 26 U.S. Code 68(c) (2024 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapB-partI-sec68.htm + - title: 2021 Hawaii N-11 Instructions, Total Itemized Deductions Worksheet, line 2 + href: https://files.hawaii.gov/tax/forms/2021/n11ins.pdf#page=32 + - title: 2025 Hawaii N-11 Instructions, Total Itemized Deductions Worksheet, line 2 + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34 diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/itemized_deduction_rate.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/itemized_deduction_rate.yaml new file mode 100644 index 00000000000..f0b32eeed0c --- /dev/null +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/limitation/itemized_deduction_rate.yaml @@ -0,0 +1,19 @@ +description: Hawaii limits the reduction of itemized deductions to this share of the itemized deductions other than the excluded deductions, under the overall limitation on itemized deductions. +values: + 2021-01-01: 0.8 +metadata: + unit: /1 + period: year + label: Hawaii itemized deductions limitation deduction rate + reference: + # Section 68 stays operative for Hawaii "in the form that it existed as of + # December 31, 2024" (Act 35, SLH 2026, for taxable years beginning after + # December 31, 2025); section 68(a)(2) sets the 80 percent rate. + - title: Act 35, SLH 2026 (H.B. 2329, C.D. 1), Section 3, amending HRS 235-2.4(b) + href: https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=18 + - title: 26 U.S. Code 68(a)(2) (2024 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapB-partI-sec68.htm + - title: 2021 Hawaii N-11 Instructions, Total Itemized Deductions Worksheet, line 5 + href: https://files.hawaii.gov/tax/forms/2021/n11ins.pdf#page=32 + - title: 2025 Hawaii N-11 Instructions, Total Itemized Deductions Worksheet, line 5 + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34 diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/reduction.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/reduction.yaml index d92dbf65907..8f0582eb8f5 100644 --- a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/reduction.yaml +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/threshold/reduction.yaml @@ -1,4 +1,4 @@ -description: Hawaii reduces the total itemized deductions amount for filers with federal adjusted gross income below this amount, based on filing status. +description: Hawaii reduces itemized deductions for filers with Hawaii adjusted gross income above this amount, based on filing status, under the overall limitation on itemized deductions. SINGLE: @@ -28,9 +28,15 @@ metadata: href: https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=32 - title: 2021 Hawaii Resident Income Tax Instructions href: https://files.hawaii.gov/tax/forms/2021/n11ins.pdf#page=32 - # The legal code specifies a reduction threshold aligning with the IRS threshold in 2009 - # However, the tax forms and instructions only reflect the 2009 threshold for single filers + # HRS 235-2.4 (subsection (c) before 2026, (b) from 2026 under Act 35, + # SLH 2026) uses the section 68 thresholds "operative for federal tax year + # 2009": $166,800 for every filing status except married filing + # separately ($83,400). - title: Hawaii Income Tax Law, Chapter 235, Section 235-2.4, (c)(1) href: https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=11 - - title: 2025 Hawaii N-11 Instructions Itemized Deductions - href: https://files.hawaii.gov/tax/forms/current/n11ins.pdf#page=32 + - title: Act 35, SLH 2026 (H.B. 2329, C.D. 1), Section 3, amending HRS 235-2.4(b) + href: https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=18 + - title: 2009 Instructions for Schedule A (Form 1040), Itemized Deductions Worksheet, line 6 + href: https://www.irs.gov/pub/irs-prior/i1040sca--2009.pdf#page=11 + - title: 2025 Hawaii N-11 Instructions, Total Itemized Deductions Worksheet, line 7 + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml index fee63c3b063..2e5ad95b36d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml @@ -6,7 +6,7 @@ tax_unit_earned_income: 1_000 hi_agi: 1_000 hi_total_itemized_deductions: 5_000 - hi_reduced_itemized_deductions: 1_000 + hi_itemized_deductions_reduction: 0 head_is_dependent_elsewhere: false output: hi_deductions: 5_000 @@ -19,7 +19,7 @@ tax_unit_earned_income: 1_000_000 hi_agi: 1_000_000 hi_total_itemized_deductions: 5_000 - hi_reduced_itemized_deductions: 1_000 + hi_itemized_deductions_reduction: 4_000 head_is_dependent_elsewhere: false output: hi_deductions: 4_400 @@ -32,7 +32,7 @@ tax_unit_earned_income: 1_000 hi_agi: 1_000 hi_total_itemized_deductions: 1_000 - hi_reduced_itemized_deductions: 1_000 + hi_itemized_deductions_reduction: 0 head_is_dependent_elsewhere: false output: hi_deductions: 4_400 @@ -45,7 +45,7 @@ tax_unit_earned_income: 1_000_000 hi_agi: 1_000_000 hi_total_itemized_deductions: 5_000 - hi_reduced_itemized_deductions: 1_000 + hi_itemized_deductions_reduction: 4_000 head_is_dependent_elsewhere: false output: hi_deductions: 2_200 @@ -58,7 +58,7 @@ tax_unit_earned_income: 1_000_000 hi_agi: 1_000_000 hi_total_itemized_deductions: 5_000 - hi_reduced_itemized_deductions: 4_500 + hi_itemized_deductions_reduction: 500 head_is_dependent_elsewhere: false output: hi_deductions: 4_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions.yaml index 5a44ba7f696..b03767807d5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions.yaml @@ -1,33 +1,142 @@ -- name: Total deduction amount, single +- name: Case 1, single filer below the reduction threshold keeps the total. period: 2022 input: state_code: HI filing_status: SINGLE hi_agi: 166_779 hi_total_itemized_deductions: 5_000 - hi_reduced_itemized_deductions: 1_000 output: hi_itemized_deductions: 5_000 - -- name: Total deduction amount joint +- name: Case 2, joint filer below the reduction threshold keeps the total. period: 2022 input: state_code: HI filing_status: JOINT hi_agi: 166_779 hi_total_itemized_deductions: 5_000 - hi_reduced_itemized_deductions: 4_500 output: hi_itemized_deductions: 5_000 -- name: Separate, have reduced deductions +- name: Case 3, total less the reduction. period: 2022 input: state_code: HI filing_status: SEPARATE hi_agi: 166_880 hi_total_itemized_deductions: 5_000 - hi_reduced_itemized_deductions: 4_500 + hi_itemized_deductions_reduction: 500 output: hi_itemized_deductions: 4_500 + +- name: Case 4, joint filer above $166,800 is limited in 2026. + period: 2026 + input: + state_code: HI + filing_status: JOINT + hi_agi: 200_000 + hi_total_itemized_deductions: 30_000 + output: + # Worksheet line 9: (200,000 - 166,800) * 3% = 996, below line 5 (24,000). + # 30,000 - 996 = 29,004. + hi_itemized_deductions: 29_004 + +- name: Case 5, head of household above $166,800 is limited in 2026. + period: 2026 + input: + state_code: HI + filing_status: HEAD_OF_HOUSEHOLD + hi_agi: 200_000 + hi_total_itemized_deductions: 30_000 + output: + hi_itemized_deductions: 29_004 + +- name: Case 6, surviving spouse above $166,800 is limited in 2026. + period: 2026 + input: + state_code: HI + filing_status: SURVIVING_SPOUSE + hi_agi: 200_000 + hi_total_itemized_deductions: 30_000 + output: + hi_itemized_deductions: 29_004 + +- name: Case 7, separate filer above $83,400 is limited in 2026. + period: 2026 + input: + state_code: HI + filing_status: SEPARATE + hi_agi: 100_000 + hi_total_itemized_deductions: 30_000 + output: + # (100,000 - 83,400) * 3% = 498. 30,000 - 498 = 29,502. + hi_itemized_deductions: 29_502 + +- name: Case 8, single filer above $166,800 is limited in 2026. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 200_000 + hi_total_itemized_deductions: 30_000 + output: + hi_itemized_deductions: 29_004 + +- name: Case 9, only medical expenses, deduction is not limited. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 300_000 + hi_medical_expense_deduction: 20_000 + hi_total_itemized_deductions: 20_000 + output: + # Worksheet line 4: the excluded deductions are not less than the total, + # so the deduction is not limited. + hi_itemized_deductions: 20_000 + +- name: Case 10, the 80% ceiling applies to deductions other than medical expenses. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 1_000_000 + hi_medical_expense_deduction: 45_000 + hi_total_itemized_deductions: 50_000 + output: + # Line 5: (50,000 - 45,000) * 80% = 4,000. + # Line 9: (1,000,000 - 166,800) * 3% = 24,996. + # 50,000 - 4,000 = 46,000. + hi_itemized_deductions: 46_000 + +- name: Case 11, joint filer exactly at the threshold keeps the total. + period: 2026 + input: + state_code: HI + filing_status: JOINT + hi_agi: 166_800 + hi_total_itemized_deductions: 30_000 + output: + hi_itemized_deductions: 30_000 + +- name: Case 12, the same limit applies in 2025. + period: 2025 + input: + state_code: HI + filing_status: JOINT + hi_agi: 200_000 + hi_total_itemized_deductions: 30_000 + output: + hi_itemized_deductions: 29_004 + +- name: Case 13, high-income joint filer in 2026 loses 80% of limited deductions. + period: 2026 + input: + state_code: HI + filing_status: JOINT + hi_agi: 1_000_000 + hi_total_itemized_deductions: 30_000 + output: + # The federal 2/37 limit under P.L. 119-21 does not apply in Hawaii. + # 30,000 - min(24,000, 24,996) = 6,000. + hi_itemized_deductions: 6_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions_reduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions_reduction.yaml new file mode 100644 index 00000000000..7fd0864660f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions_reduction.yaml @@ -0,0 +1,114 @@ +- name: Case 1, separate filer above the threshold, reduction is 3% of the excess AGI. + period: 2022 + input: + people: + person1: + investment_interest_expense: 100 + person2: + investment_interest_expense: 100 + tax_units: + tax_unit: + filing_status: SEPARATE + hi_medical_expense_deduction: 300 + hi_casualty_loss_deduction: 400 + hi_agi: 200_000 + members: [person1, person2] + hi_total_itemized_deductions: 10_000 + households: + household: + members: [person1, person2] + state_code: HI + output: + # Line 5: (10,000 - 900) * 80% = 7,280. + # Line 9: (200,000 - 83,400) * 3% = 3,498. + hi_itemized_deductions_reduction: 3_498 + +- name: Case 2, Hawaii AGI below the threshold, no reduction. + period: 2022 + input: + people: + person1: + investment_interest_expense: 100 + person2: + investment_interest_expense: 100 + tax_units: + tax_unit: + filing_status: JOINT + hi_medical_expense_deduction: 300 + hi_casualty_loss_deduction: 400 + hi_agi: 166_779 + members: [person1, person2] + hi_total_itemized_deductions: 10_000 + households: + household: + members: [person1, person2] + state_code: HI + output: + hi_itemized_deductions_reduction: 0 + +- name: Case 3, excluded deductions equal the total, no reduction. + period: 2022 + input: + people: + person1: + investment_interest_expense: 100 + person2: + investment_interest_expense: 100 + tax_units: + tax_unit: + filing_status: JOINT + hi_medical_expense_deduction: 300 + hi_casualty_loss_deduction: 400 + hi_agi: 166_880 + members: [person1, person2] + hi_total_itemized_deductions: 900 + households: + household: + members: [person1, person2] + state_code: HI + output: + hi_itemized_deductions_reduction: 0 + +- name: Case 4, joint filer in 2026 uses the $166,800 threshold, not a joint federal amount. + period: 2026 + input: + state_code: HI + filing_status: JOINT + hi_agi: 200_000 + hi_total_itemized_deductions: 30_000 + output: + # Line 5: 30,000 * 80% = 24,000. + # Line 9: (200,000 - 166,800) * 3% = 996. + hi_itemized_deductions_reduction: 996 + +- name: Case 5, Hawaii AGI exactly at the threshold, no reduction. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 166_800 + hi_total_itemized_deductions: 30_000 + output: + hi_itemized_deductions_reduction: 0 + +- name: Case 6, the 80% ceiling binds at high income in 2026, not the federal 2/37 rule. + period: 2026 + input: + state_code: HI + filing_status: JOINT + hi_agi: 1_000_000 + hi_total_itemized_deductions: 30_000 + output: + # Line 5: 30,000 * 80% = 24,000. + # Line 9: (1,000,000 - 166,800) * 3% = 24,996. + hi_itemized_deductions_reduction: 24_000 + +- name: Case 7, negative Hawaii AGI, no reduction. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: -50_000 + hi_total_itemized_deductions: 30_000 + output: + hi_itemized_deductions_reduction: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_reduced_itemized_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_reduced_itemized_deductions.yaml deleted file mode 100644 index 912c803624b..00000000000 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_reduced_itemized_deductions.yaml +++ /dev/null @@ -1,69 +0,0 @@ -- name: base test, separate filers - period: 2022 - input: - people: - person1: - investment_interest_expense: 100 - person2: - investment_interest_expense: 100 - tax_units: - tax_unit: - filing_status: SEPARATE - hi_medical_expense_deduction: 300 - hi_casualty_loss_deduction: 400 - hi_agi: 200_000 - members: [person1, person2] - hi_total_itemized_deductions: 10_000 - households: - household: - members: [person1, person2] - state_code: HI - output: - hi_reduced_itemized_deductions: 6_502 - - -- name: AGI limit not met - period: 2022 - input: - people: - person1: - investment_interest_expense: 100 - person2: - investment_interest_expense: 100 - tax_units: - tax_unit: - filing_status: JOINT - hi_medical_expense_deduction: 300 - hi_casualty_loss_deduction: 400 - hi_agi: 166_779 - members: [person1, person2] - hi_total_itemized_deductions: 10_000 - households: - household: - members: [person1, person2] - state_code: HI - output: - hi_reduced_itemized_deductions: 0 - -- name: Reduced itemized deductions same as total - period: 2022 - input: - people: - person1: - investment_interest_expense: 100 - person2: - investment_interest_expense: 100 - tax_units: - tax_unit: - filing_status: JOINT - hi_medical_expense_deduction: 300 - hi_casualty_loss_deduction: 400 - hi_agi: 166_880 - members: [person1, person2] - hi_total_itemized_deductions: 900 - households: - household: - members: [person1, person2] - state_code: HI - output: - hi_reduced_itemized_deductions: 0 diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions.py index 2358892da1a..566bd073d05 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions.py @@ -6,27 +6,15 @@ class hi_itemized_deductions(Variable): entity = TaxUnit label = "Hawaii itemized deduction" unit = USD - documentation = ( - "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=15\n" - "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=32" # total itemized deduction worksheet - ) definition_period = YEAR defined_for = StateCode.HI + reference = ( + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=20", + # Total Itemized Deductions Worksheet, line 11. + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34", + ) def formula(tax_unit, period, parameters): - # Hawaii applies an federal AGI limit which has been introduced in 2009 - p = parameters(f"2009-01-01").gov.irs.deductions.itemized.limitation - - # Hawaii did not suspend the overall limitation on itemized deductions - # You may not be able to deduct all of your itemized deductions if agi reach the cap - # need to calculate the reduced itemized deductions - hi_agi = tax_unit("hi_agi", period) - filing_status = tax_unit("filing_status", period) - total_itemized_deduction_eligibility = ( - hi_agi < p.applicable_amount[filing_status] - ) - return where( - total_itemized_deduction_eligibility, - tax_unit("hi_total_itemized_deductions", period), - tax_unit("hi_reduced_itemized_deductions", period), - ) + total_deductions = tax_unit("hi_total_itemized_deductions", period) + reduction = tax_unit("hi_itemized_deductions_reduction", period) + return max_(0, total_deductions - reduction) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions_reduction.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions_reduction.py new file mode 100644 index 00000000000..6c503d3bace --- /dev/null +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_itemized_deductions_reduction.py @@ -0,0 +1,39 @@ +from policyengine_us.model_api import * + + +class hi_itemized_deductions_reduction(Variable): + value_type = float + entity = TaxUnit + label = "Hawaii itemized deductions reduction" + unit = USD + definition_period = YEAR + defined_for = StateCode.HI + reference = ( + # HRS 235-2.4 keeps IRC section 68 operative with the 2009 thresholds; + # Act 35, SLH 2026 keeps it "in the form that it existed as of + # December 31, 2024" for taxable years beginning after 2025. + "https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=18", + "https://www.govinfo.gov/content/pkg/USCODE-2024-title26/html/USCODE-2024-title26-subtitleA-chap1-subchapB-partI-sec68.htm", + # Total Itemized Deductions Worksheet, lines 2 to 10. + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34", + ) + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.hi.tax.income.deductions.itemized + total_deductions = tax_unit("hi_total_itemized_deductions", period) + # Lines 2 to 5: 80% of the deductions other than medical expenses, + # investment interest and casualty losses. If those deductions are not + # less than the total, the deduction is not limited. + excluded_deductions = add(tax_unit, period, p.limitation.excluded_deductions) + limited_deductions = max_(0, total_deductions - excluded_deductions) + deduction_based_reduction = ( + limited_deductions * p.limitation.itemized_deduction_rate + ) + # Lines 6 to 9: 3% of Hawaii AGI above the threshold. If Hawaii AGI is + # not above the threshold, the deduction is not limited. + hi_agi = tax_unit("hi_agi", period) + filing_status = tax_unit("filing_status", period) + agi_excess = max_(0, hi_agi - p.threshold.reduction[filing_status]) + agi_based_reduction = agi_excess * p.limitation.agi_rate + # Line 10: the smaller of the two. + return min_(deduction_based_reduction, agi_based_reduction) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_reduced_itemized_deductions.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_reduced_itemized_deductions.py deleted file mode 100644 index c68fabcfe61..00000000000 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_reduced_itemized_deductions.py +++ /dev/null @@ -1,56 +0,0 @@ -from policyengine_us.model_api import * - - -class hi_reduced_itemized_deductions(Variable): - value_type = float - entity = TaxUnit - label = "Hawaii reduced itemized deduction" - unit = USD - documentation = ( - "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=15\n" - "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=32" # total itemized deduction worksheet - ) - definition_period = YEAR - defined_for = StateCode.HI - - # If the state AGI of the filer exceeds a certain amount, only partial itemized deductions - # can be deducted. - def formula(tax_unit, period, parameters): - total_deductions = tax_unit("hi_total_itemized_deductions", period) - partial_deductions = add( - tax_unit, - period, - [ - "hi_medical_expense_deduction", - "investment_interest_expense", - "hi_casualty_loss_deduction", - ], - ) - - # eligible check 1: deduction_difference need to be greater than 0 to have reduced deduction - partial_deductions_less_than_total = partial_deductions < total_deductions - total_less_partial_ded_amount = total_deductions - partial_deductions - # Take a percentage of the difference between the total and partial deductions - # Hawaii applies federal limits which have been revoked in 2018 - p_2017 = parameters(f"2017-01-01").gov.irs.deductions.itemized.limitation - total_less_partial_ded_percentage = ( - total_less_partial_ded_amount * p_2017.itemized_deduction_rate - ) - # eligible check 2: actual AGI need to be smaller than AGI cap - hi_agi = tax_unit("hi_agi", period) - filing_status = tax_unit("filing_status", period) - # Hawaii applies an federal AGI limit as of 2009. - p_hi = parameters(period).gov.states.hi.tax.income.deductions.itemized.threshold - agi_threshold = p_hi.reduction[filing_status] - agi_over_threshold = agi_threshold < hi_agi - # If the AGI is over a threshold, the AGI amount is reduced by the threshold and multiplied - # by a rate - reduced_agi = hi_agi - agi_threshold - reduced_agi_percentage = reduced_agi * p_2017.agi_rate - - smaller_reduced_ded = min_( - total_less_partial_ded_percentage, reduced_agi_percentage - ) - reduced_deductions = max_(0, total_deductions - smaller_reduced_ded) - eligible = partial_deductions_less_than_total & agi_over_threshold - return eligible * reduced_deductions From 0361f8a5df3df84062a57002e11ff6436ae3c140 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 16:23:20 -0400 Subject: [PATCH 2/5] Add Hawaii's Act 35 qualified tips and non-itemizer charitable deductions Act 35, SLH 2026 conforms Hawaii to the Internal Revenue Code as of December 31, 2025 without making section 224 (qualified tips) or 170(p) (charitable deduction for non-itemizers) inoperative. Add hi_tip_income_deduction for 2026-2028, phased out on Hawaii AGI and denied to separate filers (224(f)), and give Hawaii non-itemizers the federal 170(p) amount on top of the standard deduction (also restores the 2020 Act 13 and 2021 Act 89 amounts). Co-Authored-By: Claude Opus 5.5 --- .../non_itemizer_charitable/in_effect.yaml | 22 +++ .../deductions/tip_income/in_effect.yaml | 22 +++ .../tax/income/deductions/hi_deductions.yaml | 100 ++++++++++++++ .../deductions/hi_tip_income_deduction.yaml | 125 ++++++++++++++++++ .../hi/tax/income/hi_taxable_income.yaml | 50 +++++++ .../hi/tax/income/deductions/hi_deductions.py | 26 +++- .../deductions/hi_tip_income_deduction.py | 45 +++++++ 7 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/hi/tax/income/deductions/non_itemizer_charitable/in_effect.yaml create mode 100644 policyengine_us/parameters/gov/states/hi/tax/income/deductions/tip_income/in_effect.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_tip_income_deduction.yaml create mode 100644 policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_tip_income_deduction.py diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/non_itemizer_charitable/in_effect.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/non_itemizer_charitable/in_effect.yaml new file mode 100644 index 00000000000..60a89aacf8f --- /dev/null +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/non_itemizer_charitable/in_effect.yaml @@ -0,0 +1,22 @@ +description: Hawaii allows the federal charitable deduction for taxpayers who do not itemize if this is true. +values: + # Act 13, SLH 2020 adopted the 2020 deduction, which reduced federal + # adjusted gross income for taxpayers who did not itemize on their federal + # return; PolicyEngine applies it to Hawaii non-itemizers below the line. + # Act 89, SLH 2021 adopted section 170(p) for 2021. The federal amount is + # zero for 2022 to 2025. Act 35, SLH 2026 conforms to section 170(p) as + # amended by P.L. 119-21 for taxable years beginning after December 31, 2025. + 2020-01-01: true +metadata: + unit: bool + period: year + label: Hawaii non-itemizer charitable deduction in effect + reference: + - title: 2020 Hawaii N-11 Instructions, Changes to Note + href: https://files.hawaii.gov/tax/forms/2020/n11ins.pdf#page=2 + - title: 2021 Hawaii N-11 Instructions, Line 23 + href: https://files.hawaii.gov/tax/forms/2021/n11ins.pdf#page=20 + - title: Hawaii Department of Taxation Announcement No. 2026-06, Act 35 + href: https://files.hawaii.gov/tax/news/announce/ann26-06.pdf#page=2 + - title: 26 U.S. Code 170(p) + href: https://www.law.cornell.edu/uscode/text/26/170#p diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/tip_income/in_effect.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/tip_income/in_effect.yaml new file mode 100644 index 00000000000..ac7fb267fb4 --- /dev/null +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/tip_income/in_effect.yaml @@ -0,0 +1,22 @@ +description: Hawaii allows the federal deduction for qualified tips if this is true. +values: + # For 2025, HRS 235-2.3(a) conforms to the Internal Revenue Code as amended + # as of December 31, 2024, before P.L. 119-21 added section 224. + 2025-01-01: false + # Act 35, SLH 2026 conforms to the Internal Revenue Code as amended as of + # December 31, 2025 and does not make section 224 inoperative. + 2026-01-01: true + # Section 224(h) allows no deduction for taxable years beginning after + # December 31, 2028. + 2029-01-01: false +metadata: + unit: bool + period: year + label: Hawaii qualified tips deduction in effect + reference: + - title: Act 35, SLH 2026 (H.B. 2329, C.D. 1), Section 2, amending HRS 235-2.3 + href: https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=1 + - title: Hawaii Department of Taxation Announcement No. 2026-06, Act 35 + href: https://files.hawaii.gov/tax/news/announce/ann26-06.pdf#page=3 + - title: 26 U.S. Code 224(h) + href: https://www.law.cornell.edu/uscode/text/26/224#h diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml index 2e5ad95b36d..80adfded752 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_deductions.yaml @@ -71,3 +71,103 @@ employment_income: 62_000 output: hi_deductions: 8_800 + +- name: Case 7, non-itemizer adds the charitable deduction to the standard deduction in 2026. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 60_000 + hi_total_itemized_deductions: 0 + charitable_cash_donations: 1_500 + head_is_dependent_elsewhere: false + output: + # 8,000 standard deduction + min(1,000, 1,500) = 9,000. + hi_deductions: 9_000 + +- name: Case 8, joint non-itemizers get up to $2,000 in 2026. + period: 2026 + input: + state_code: HI + filing_status: JOINT + hi_agi: 90_000 + hi_total_itemized_deductions: 0 + charitable_cash_donations: 3_000 + head_is_dependent_elsewhere: false + output: + # 16,000 + 2,000 = 18,000. + hi_deductions: 18_000 + +- name: Case 9, no non-itemizer charitable deduction in 2025. + period: 2025 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 60_000 + hi_total_itemized_deductions: 0 + charitable_cash_donations: 1_500 + head_is_dependent_elsewhere: false + output: + hi_deductions: 4_400 + +- name: Case 10, Act 89 allowed up to $300 for non-itemizers in 2021. + period: 2021 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 60_000 + hi_total_itemized_deductions: 0 + charitable_cash_donations: 500 + head_is_dependent_elsewhere: false + output: + # 2,200 + 300 = 2,500. + hi_deductions: 2_500 + +- name: Case 11, itemizers do not get the non-itemizer charitable deduction. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 60_000 + hi_itemized_deductions: 20_000 + charitable_cash_donations: 1_500 + head_is_dependent_elsewhere: false + output: + hi_deductions: 20_000 + +- name: Case 12, itemizer is better off taking the standard and charitable deductions. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 60_000 + hi_itemized_deductions: 8_500 + charitable_cash_donations: 1_500 + head_is_dependent_elsewhere: false + output: + # max(8,500, 8,000 + 1,000) = 9,000. + hi_deductions: 9_000 + +- name: Case 13, the tips deduction is added for itemizers. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 60_000 + hi_itemized_deductions: 20_000 + hi_tip_income_deduction: 5_000 + head_is_dependent_elsewhere: false + output: + hi_deductions: 25_000 + +- name: Case 14, the tips deduction is added for non-itemizers. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 60_000 + hi_total_itemized_deductions: 0 + hi_tip_income_deduction: 5_000 + head_is_dependent_elsewhere: false + output: + hi_deductions: 13_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_tip_income_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_tip_income_deduction.yaml new file mode 100644 index 00000000000..845419ff689 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/hi_tip_income_deduction.yaml @@ -0,0 +1,125 @@ +- name: Case 1, tipped worker deducts qualified tips in 2026. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + tip_income_deduction_ssn_requirement_met: true + treasury_tipped_occupation_code: 102 + tip_income: 10_000 + hi_agi: 50_000 + output: + hi_tip_income_deduction: 10_000 + +- name: Case 2, no Hawaii tips deduction in 2025. + period: 2025 + input: + state_code: HI + filing_status: SINGLE + tip_income_deduction_ssn_requirement_met: true + treasury_tipped_occupation_code: 102 + tip_income: 10_000 + hi_agi: 50_000 + output: + # HRS 235-2.3(a) uses the Internal Revenue Code as of December 31, 2024. + hi_tip_income_deduction: 0 + +- name: Case 3, the deduction ends after 2028. + period: 2029 + input: + state_code: HI + filing_status: SINGLE + tip_income_deduction_ssn_requirement_met: true + treasury_tipped_occupation_code: 102 + tip_income: 10_000 + hi_agi: 50_000 + output: + hi_tip_income_deduction: 0 + +- name: Case 4, the deduction is capped at $25,000. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + tip_income_deduction_ssn_requirement_met: true + treasury_tipped_occupation_code: 102 + tip_income: 30_000 + hi_agi: 50_000 + output: + hi_tip_income_deduction: 25_000 + +- name: Case 5, the phase-out uses Hawaii AGI, not federal AGI. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + tip_income_deduction_ssn_requirement_met: true + treasury_tipped_occupation_code: 102 + tip_income: 10_000 + adjusted_gross_income: 250_000 + hi_agi: 200_000 + output: + # (200,000 - 150,000) * 10% = 5,000. 10,000 - 5,000 = 5,000. + hi_tip_income_deduction: 5_000 + +- name: Case 6, joint filers phase out above $300,000. + period: 2026 + input: + state_code: HI + filing_status: JOINT + tip_income_deduction_ssn_requirement_met: true + treasury_tipped_occupation_code: 102 + tip_income: 10_000 + hi_agi: 320_000 + output: + # (320,000 - 300,000) * 10% = 2,000. 10,000 - 2,000 = 8,000. + hi_tip_income_deduction: 8_000 + +- name: Case 7, married filing separately gets no deduction. + period: 2026 + input: + state_code: HI + filing_status: SEPARATE + tip_income_deduction_ssn_requirement_met: true + treasury_tipped_occupation_code: 102 + tip_income: 10_000 + hi_agi: 50_000 + output: + # Section 224(f) requires married individuals to file a joint return. + hi_tip_income_deduction: 0 + +- name: Case 8, tips outside a listed tipped occupation do not qualify. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + tip_income_deduction_ssn_requirement_met: true + treasury_tipped_occupation_code: 0 + tip_income: 10_000 + hi_agi: 50_000 + output: + hi_tip_income_deduction: 0 + +- name: Case 9, no deduction without a valid social security number. + period: 2026 + input: + state_code: HI + filing_status: SINGLE + tip_income_deduction_ssn_requirement_met: false + treasury_tipped_occupation_code: 102 + tip_income: 10_000 + hi_agi: 50_000 + output: + hi_tip_income_deduction: 0 + +- name: Case 10, fully phased out. + period: 2026 + input: + state_code: HI + filing_status: HEAD_OF_HOUSEHOLD + tip_income_deduction_ssn_requirement_met: true + treasury_tipped_occupation_code: 102 + tip_income: 10_000 + hi_agi: 260_000 + output: + # (260,000 - 150,000) * 10% = 11,000, more than the 10,000 of tips. + hi_tip_income_deduction: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_taxable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_taxable_income.yaml index 057dce05319..b04035e3dc9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_taxable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_taxable_income.yaml @@ -17,3 +17,53 @@ state_code: HI output: hi_taxable_income: 0 + +- name: Case 3, tipped worker deducts qualified tips from Hawaii taxable income in 2026. + period: 2026 + input: + people: + person1: + age: 30 + employment_income: 40_000 + tip_income: 8_000 + treasury_tipped_occupation_code: 102 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: HI + output: + hi_agi: 40_000 + hi_tip_income_deduction: 8_000 + # 8,000 standard deduction + 8,000 tips deduction. + hi_deductions: 16_000 + hi_exemptions: 1_144 + # 40,000 - 16,000 - 1,144 = 22,856. + hi_taxable_income: 22_856 + +- name: Case 4, joint itemizers above $166,800 have limited itemized deductions in 2026. + period: 2026 + input: + people: + person1: + age: 40 + person2: + age: 40 + tax_units: + tax_unit: + members: [person1, person2] + hi_agi: 200_000 + hi_total_itemized_deductions: 30_000 + households: + household: + members: [person1, person2] + state_code: HI + output: + # 30,000 - (200,000 - 166,800) * 3% = 29,004. + hi_itemized_deductions: 29_004 + hi_deductions: 29_004 + hi_exemptions: 2_288 + # 200,000 - 29,004 - 2,288 = 168,708. + hi_taxable_income: 168_708 diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py index 5e84412ac6c..52bedf8d748 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_deductions.py @@ -10,11 +10,17 @@ class hi_deductions(Variable): "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=15\n" # Itemized Deduction "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=20" # Standard Deduction ) + reference = ( + # Act 35, SLH 2026 conforms to sections 170(p) and 224 from 2026. + "https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=1", + "https://files.hawaii.gov/tax/news/announce/ann26-06.pdf#page=2", + ) definition_period = YEAR defined_for = StateCode.HI def formula(tax_unit, period, parameters): - p = parameters(period).gov.states.hi.tax.income.deductions.itemized + p_deductions = parameters(period).gov.states.hi.tax.income.deductions + p = p_deductions.itemized standard_deduction = tax_unit("hi_standard_deduction", period) total_itemized_deduction = tax_unit("hi_itemized_deductions", period) @@ -35,4 +41,20 @@ def formula(tax_unit, period, parameters): itemized_deduction = where( itemized_deductions_eligible, total_itemized_deduction, 0 ) - return max_(itemized_deduction, standard_deduction) + # Section 170(p) lets taxpayers who do not itemize deduct some cash + # charitable contributions on top of the standard deduction. + non_itemizer_charitable_deduction = ( + tax_unit("charitable_deduction_for_non_itemizers", period) + if p_deductions.non_itemizer_charitable.in_effect + else 0 + ) + # Section 224 allows the qualified tips deduction whether or not the + # taxpayer itemizes. + tip_income_deduction = tax_unit("hi_tip_income_deduction", period) + return ( + max_( + itemized_deduction, + standard_deduction + non_itemizer_charitable_deduction, + ) + + tip_income_deduction + ) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_tip_income_deduction.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_tip_income_deduction.py new file mode 100644 index 00000000000..902af35be78 --- /dev/null +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/hi_tip_income_deduction.py @@ -0,0 +1,45 @@ +from policyengine_us.model_api import * + + +class hi_tip_income_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Hawaii qualified tips deduction" + unit = USD + definition_period = YEAR + defined_for = StateCode.HI + reference = ( + "https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=1", + "https://files.hawaii.gov/tax/news/announce/ann26-06.pdf#page=3", + "https://www.law.cornell.edu/uscode/text/26/224", + ) + + def formula(tax_unit, period, parameters): + if not parameters( + period + ).gov.states.hi.tax.income.deductions.tip_income.in_effect: + return 0 + p = parameters(period).gov.irs.deductions.tip_income + person = tax_unit.members + tip_income = person("tip_income", period) + occupation_requirement_met = person( + "tip_income_deduction_occupation_requirement_met", period + ) + qualified_tips = tax_unit.sum(tip_income * occupation_requirement_met) + capped_tips = min_(p.cap, qualified_tips) + # Section 224(b)(2) phases the deduction out with modified adjusted + # gross income. HRS 235-1 defines adjusted gross income as determined + # under the Internal Revenue Code as modified by chapter 235, so the + # phase-out uses Hawaii adjusted gross income. + hi_agi = tax_unit("hi_agi", period) + filing_status = tax_unit("filing_status", period) + agi_excess = max_(0, hi_agi - p.phase_out.start[filing_status]) + phase_out_amount = agi_excess * p.phase_out.rate + deduction = max_(0, capped_tips - phase_out_amount) + # Section 224(e) requires a social security number, and section 224(f) + # requires married individuals to file a joint return. + ssn_requirement_met = tax_unit( + "tip_income_deduction_ssn_requirement_met", period + ) + separate = filing_status == filing_status.possible_values.SEPARATE + return where(ssn_requirement_met & ~separate, deduction, 0) From 7fbb180e9caa5d09449c29a299470423df0a83ee Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 16:51:20 -0400 Subject: [PATCH 3/5] Model Hawaii miscellaneous deductions through 2025 and use Hawaii AGI for charitable limits Hawaii kept the 2% miscellaneous itemized deductions for 2018-2025 (it never adopted the section 67(g) suspension); Act 35, SLH 2026 ends them from 2026. Add hi_misc_deduction gated by a Hawaii parameter. The N-11 instructions state the section 170(b) limits as a share of Hawaii AGI, and Act 35 conforms to the 0.5% floor from 2026. Move the federal charitable_deduction formula into a helper that takes the contribution base, unchanged for the federal deduction, and add hi_charitable_deduction on Hawaii AGI. Co-Authored-By: Claude Opus 5.5 --- .../deductions/itemized/misc/in_effect.yaml | 22 ++++++ .../income/deductions/itemized/sources.yaml | 7 +- .../itemized/hi_charitable_deduction.yaml | 73 ++++++++++++++++++ .../itemized/hi_misc_deduction.yaml | 77 +++++++++++++++++++ .../_charitable_deduction_helpers.py | 77 +++++++++++++++++++ .../itemizing/charitable_deduction.py | 66 ++-------------- .../itemized/hi_charitable_deduction.py | 29 +++++++ .../deductions/itemized/hi_misc_deduction.py | 26 +++++++ 8 files changed, 314 insertions(+), 63 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/misc/in_effect.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_charitable_deduction.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_misc_deduction.yaml create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/_charitable_deduction_helpers.py create mode 100644 policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_charitable_deduction.py create mode 100644 policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_misc_deduction.py diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/misc/in_effect.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/misc/in_effect.yaml new file mode 100644 index 00000000000..dcfc1ece0cd --- /dev/null +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/misc/in_effect.yaml @@ -0,0 +1,22 @@ +description: Hawaii allows miscellaneous itemized deductions above the adjusted gross income floor if this is true. +values: + # HRS 235-2.4 made section 67 operative except for the section 67(g) + # suspension for 2018 through 2025. Act 35, SLH 2026 strikes that exception + # for taxable years beginning after December 31, 2025. + 2021-01-01: true + 2026-01-01: false +metadata: + unit: bool + period: year + label: Hawaii miscellaneous itemized deductions in effect + reference: + - title: 2021 Hawaii N-11 Instructions, Worksheet A-6, Miscellaneous Deductions + href: https://files.hawaii.gov/tax/forms/2021/n11ins.pdf#page=32 + - title: 2025 Hawaii N-11 Instructions, Line 21f, Miscellaneous Deductions + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=19 + - title: 2025 Hawaii N-11 Instructions, Worksheet A-6, Miscellaneous Deductions + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34 + - title: Act 35, SLH 2026 (H.B. 2329, C.D. 1), Section 3, amending HRS 235-2.4 + href: https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=17 + - title: Hawaii Department of Taxation Announcement No. 2026-06, Act 35 + href: https://files.hawaii.gov/tax/news/announce/ann26-06.pdf#page=2 diff --git a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/sources.yaml b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/sources.yaml index 20406023cc9..176083a7938 100644 --- a/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/sources.yaml +++ b/policyengine_us/parameters/gov/states/hi/tax/income/deductions/itemized/sources.yaml @@ -1,11 +1,12 @@ description: Hawaii deducts the following itemized deductions. values: 2018-01-01: - - charitable_deduction + - hi_charitable_deduction - hi_medical_expense_deduction - hi_interest_deduction - hi_casualty_loss_deduction - hi_salt_deduction + - hi_misc_deduction metadata: unit: list period: year @@ -24,4 +25,6 @@ metadata: - title: Hawaii Income Tax Law, Chapter 235, Section 235-2.4 href: https://files.hawaii.gov/tax/legal/hrs/hrs_235.pdf#page=10 - title: 2025 Hawaii N-11 Instructions Itemized Deductions - href: https://files.hawaii.gov/tax/forms/current/n11ins.pdf#page=17 + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=17 + - title: 2025 Hawaii N-11 Instructions, Itemized Deductions Worksheet, line 32 + href: https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_charitable_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_charitable_deduction.yaml new file mode 100644 index 00000000000..62e339f2de4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_charitable_deduction.yaml @@ -0,0 +1,73 @@ +- name: Case 1, the 2026 floor is 0.5% of Hawaii AGI, not federal AGI. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: HI + filing_status: SINGLE + adjusted_gross_income: 110_600 + hi_agi: 50_000 + charitable_cash_donations: 20_000 + output: + # 20,000 - 0.5% * 50,000 = 19,750, below 60% of Hawaii AGI (30,000). + hi_charitable_deduction: 19_750 + +- name: Case 2, the 60% ceiling is a share of Hawaii AGI. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: HI + filing_status: SINGLE + adjusted_gross_income: 110_600 + hi_agi: 50_000 + charitable_cash_donations: 40_000 + output: + # 60% * 50,000 = 30,000. + hi_charitable_deduction: 30_000 + +- name: Case 3, no floor in 2025. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: HI + filing_status: SINGLE + hi_agi: 100_000 + charitable_cash_donations: 5_000 + output: + hi_charitable_deduction: 5_000 + +- name: Case 4, the floor applies to non-cash gifts first in 2026. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: HI + filing_status: JOINT + hi_agi: 100_000 + charitable_cash_donations: 1_000 + charitable_non_cash_donations: 300 + output: + # Floor 500: non-cash 300 is used up, then 200 of the cash. + # 0 + (1,000 - 200) = 800. + hi_charitable_deduction: 800 + +- name: Case 5, the 50% ceiling on non-cash gifts uses Hawaii AGI. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: HI + filing_status: JOINT + hi_agi: 100_000 + charitable_non_cash_donations: 60_000 + output: + # (60,000 - 500) capped at 50% * 100,000 = 50,000. + hi_charitable_deduction: 50_000 + +- name: Case 6, negative Hawaii AGI allows no deduction. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: HI + filing_status: SINGLE + hi_agi: -10_000 + charitable_cash_donations: 1_000 + output: + hi_charitable_deduction: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_misc_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_misc_deduction.yaml new file mode 100644 index 00000000000..7a67ab6af1c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/deductions/itemized/hi_misc_deduction.yaml @@ -0,0 +1,77 @@ +- name: Case 1, expenses above 2% of Hawaii AGI are deductible in 2025. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: HI + unreimbursed_business_employee_expenses: 5_000 + tax_preparation_fees: 500 + hi_agi: 50_000 + output: + # 5,500 - 2% * 50,000 = 4,500. + hi_misc_deduction: 4_500 + +- name: Case 2, Act 35 ends the deduction from 2026. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: HI + unreimbursed_business_employee_expenses: 5_000 + tax_preparation_fees: 500 + hi_agi: 50_000 + output: + hi_misc_deduction: 0 + +- name: Case 3, expenses below the floor give no deduction. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: HI + tax_preparation_fees: 800 + hi_agi: 50_000 + output: + hi_misc_deduction: 0 + +- name: Case 4, the floor is zero when Hawaii AGI is negative. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: HI + tax_preparation_fees: 800 + hi_agi: -20_000 + output: + hi_misc_deduction: 800 + +- name: Case 5, both spouses' expenses count on a joint return. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + unreimbursed_business_employee_expenses: 3_000 + person2: + age: 40 + tax_preparation_fees: 1_000 + tax_units: + tax_unit: + members: [person1, person2] + hi_agi: 100_000 + households: + household: + members: [person1, person2] + state_code: HI + output: + # 4,000 - 2% * 100,000 = 2,000. + hi_misc_deduction: 2_000 + +- name: Case 6, the deduction is part of Hawaii total itemized deductions. + period: 2025 + absolute_error_margin: 0.01 + input: + state_code: HI + unreimbursed_business_employee_expenses: 5_000 + tax_preparation_fees: 500 + hi_agi: 50_000 + hi_salt_deduction: 0 + output: + hi_total_itemized_deductions: 4_500 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/_charitable_deduction_helpers.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/_charitable_deduction_helpers.py new file mode 100644 index 00000000000..640bd4ac891 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/_charitable_deduction_helpers.py @@ -0,0 +1,77 @@ +from policyengine_us.model_api import * + + +def charitable_deduction_given_contribution_base( + tax_unit, period, parameters, contribution_base +): + """Itemized charitable deduction under 26 USC 170(b) for a given contribution base. + + The federal deduction uses positive federal adjusted gross income as the + contribution base. States that measure the section 170(b) percentage + limits and floor against their own adjusted gross income pass that + instead. + """ + # TODO: 26 USC 170(b)(1) defines 4 contribution categories with + # separate AGI caps: + # (G) Cash to 50% limit orgs: 60% AGI (post-TCJA) + # (B) Cash to non-50% limit orgs: 30% AGI + # (C) Capital gain property to 50% limit orgs: 30% AGI + # (or 50% with basis-reduction election) + # (D) Capital gain property to non-50% limit orgs: 20% AGI + # Currently we model two non-cash categories (50% vs 30% cap) + # and treat all cash as subject to the overall cap only. + # The cash split and the 20% category are not yet modeled. + cash_donations = add(tax_unit, period, ["charitable_cash_donations"]) + non_cash_donations = add(tax_unit, period, ["charitable_non_cash_donations"]) + non_cash_to_non_50_pct = add( + tax_unit, + period, + ["charitable_non_cash_donations_non_50_pct_orgs"], + ) + non_cash_to_50_pct = non_cash_donations - non_cash_to_non_50_pct + p = parameters(period).gov.irs.deductions.itemized.charity + + # Non-cash to 50% limit orgs capped at 50% of AGI. + # Note: this models the basis-reduction election under + # 170(b)(1)(C)(iii); the default cap for capital gain + # property is 30% per 170(b)(1)(C)(i). + capped_non_cash_50 = min_( + non_cash_to_50_pct, p.ceiling.non_cash * contribution_base + ) + # Non-cash to non-50% limit orgs capped at 30% of AGI + # per 170(b)(1)(B). Note: the stricter 20% cap for + # capital gain property under 170(b)(1)(D) is not yet + # modeled separately. + capped_non_cash_non_50 = min_( + non_cash_to_non_50_pct, + p.ceiling.non_cash_to_non_50_pct_org * contribution_base, + ) + capped_non_cash = capped_non_cash_50 + capped_non_cash_non_50 + + total_cap = p.ceiling.all * contribution_base + if p.floor.applies: + deduction_floor = p.floor.amount * contribution_base + reduced_non_cash_50 = max_(non_cash_to_50_pct - deduction_floor, 0) + capped_reduced_non_cash_50 = min_( + reduced_non_cash_50, + p.ceiling.non_cash * contribution_base, + ) + + remaining_floor_after_50 = max_(deduction_floor - non_cash_to_50_pct, 0) + reduced_non_cash_non_50 = max_( + non_cash_to_non_50_pct - remaining_floor_after_50, 0 + ) + capped_reduced_non_cash_non_50 = min_( + reduced_non_cash_non_50, + p.ceiling.non_cash_to_non_50_pct_org * contribution_base, + ) + + remaining_floor = max_(remaining_floor_after_50 - non_cash_to_non_50_pct, 0) + reduced_cash_donations = max_(cash_donations - remaining_floor, 0) + return min_( + capped_reduced_non_cash_50 + + capped_reduced_non_cash_non_50 + + reduced_cash_donations, + total_cap, + ) + return min_(capped_non_cash + cash_donations, total_cap) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction.py index 96b9285f230..5ea445bb17c 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/charitable_deduction.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.income.taxable_income.deductions.itemizing._charitable_deduction_helpers import ( + charitable_deduction_given_contribution_base, +) class charitable_deduction(Variable): @@ -11,66 +14,7 @@ class charitable_deduction(Variable): reference = "https://www.law.cornell.edu/uscode/text/26/170" def formula(tax_unit, period, parameters): - # TODO: 26 USC 170(b)(1) defines 4 contribution categories with - # separate AGI caps: - # (G) Cash to 50% limit orgs: 60% AGI (post-TCJA) - # (B) Cash to non-50% limit orgs: 30% AGI - # (C) Capital gain property to 50% limit orgs: 30% AGI - # (or 50% with basis-reduction election) - # (D) Capital gain property to non-50% limit orgs: 20% AGI - # Currently we model two non-cash categories (50% vs 30% cap) - # and treat all cash as subject to the overall cap only. - # The cash split and the 20% category are not yet modeled. - cash_donations = add(tax_unit, period, ["charitable_cash_donations"]) - non_cash_donations = add(tax_unit, period, ["charitable_non_cash_donations"]) - non_cash_to_non_50_pct = add( - tax_unit, - period, - ["charitable_non_cash_donations_non_50_pct_orgs"], - ) - non_cash_to_50_pct = non_cash_donations - non_cash_to_non_50_pct positive_agi = tax_unit("positive_agi", period) - p = parameters(period).gov.irs.deductions.itemized.charity - - # Non-cash to 50% limit orgs capped at 50% of AGI. - # Note: this models the basis-reduction election under - # 170(b)(1)(C)(iii); the default cap for capital gain - # property is 30% per 170(b)(1)(C)(i). - capped_non_cash_50 = min_(non_cash_to_50_pct, p.ceiling.non_cash * positive_agi) - # Non-cash to non-50% limit orgs capped at 30% of AGI - # per 170(b)(1)(B). Note: the stricter 20% cap for - # capital gain property under 170(b)(1)(D) is not yet - # modeled separately. - capped_non_cash_non_50 = min_( - non_cash_to_non_50_pct, - p.ceiling.non_cash_to_non_50_pct_org * positive_agi, + return charitable_deduction_given_contribution_base( + tax_unit, period, parameters, positive_agi ) - capped_non_cash = capped_non_cash_50 + capped_non_cash_non_50 - - total_cap = p.ceiling.all * positive_agi - if p.floor.applies: - deduction_floor = p.floor.amount * positive_agi - reduced_non_cash_50 = max_(non_cash_to_50_pct - deduction_floor, 0) - capped_reduced_non_cash_50 = min_( - reduced_non_cash_50, - p.ceiling.non_cash * positive_agi, - ) - - remaining_floor_after_50 = max_(deduction_floor - non_cash_to_50_pct, 0) - reduced_non_cash_non_50 = max_( - non_cash_to_non_50_pct - remaining_floor_after_50, 0 - ) - capped_reduced_non_cash_non_50 = min_( - reduced_non_cash_non_50, - p.ceiling.non_cash_to_non_50_pct_org * positive_agi, - ) - - remaining_floor = max_(remaining_floor_after_50 - non_cash_to_non_50_pct, 0) - reduced_cash_donations = max_(cash_donations - remaining_floor, 0) - return min_( - capped_reduced_non_cash_50 - + capped_reduced_non_cash_non_50 - + reduced_cash_donations, - total_cap, - ) - return min_(capped_non_cash + cash_donations, total_cap) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_charitable_deduction.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_charitable_deduction.py new file mode 100644 index 00000000000..6df5025408f --- /dev/null +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_charitable_deduction.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.income.taxable_income.deductions.itemizing._charitable_deduction_helpers import ( + charitable_deduction_given_contribution_base, +) + + +class hi_charitable_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Hawaii charitable deduction" + unit = USD + definition_period = YEAR + defined_for = StateCode.HI + reference = ( + # Line 21d and the limits stated as a share of Hawaii AGI. + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=17", + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=18", + # Act 35, SLH 2026 conforms to the section 170(b)(1)(I) floor. + "https://files.hawaii.gov/tax/news/announce/ann26-06.pdf#page=2", + ) + + def formula(tax_unit, period, parameters): + # The N-11 instructions measure the section 170(b) limits against + # Hawaii adjusted gross income, so Hawaii AGI is the contribution + # base for the ceilings and for the 0.5% floor from 2026. + hi_agi = tax_unit("hi_agi", period) + return charitable_deduction_given_contribution_base( + tax_unit, period, parameters, max_(hi_agi, 0) + ) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_misc_deduction.py b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_misc_deduction.py new file mode 100644 index 00000000000..c7c67665d7a --- /dev/null +++ b/policyengine_us/variables/gov/states/hi/tax/income/deductions/itemized/hi_misc_deduction.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class hi_misc_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Hawaii miscellaneous itemized deduction" + unit = USD + definition_period = YEAR + defined_for = StateCode.HI + reference = ( + # Worksheet A-6, lines 23 to 29. + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=34", + "https://data.capitol.hawaii.gov/sessions/session2026/bills/HB2329_CD1_.pdf#page=17", + ) + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.hi.tax.income.deductions.itemized.misc + if not p.in_effect: + return 0 + # Hawaii applies the section 67(a) floor to Hawaii AGI. + floor_rate = parameters(period).gov.irs.deductions.itemized.misc.floor + expenses = tax_unit("total_misc_deductions", period) + hi_agi = tax_unit("hi_agi", period) + floor = max_(0, hi_agi * floor_rate) + return max_(0, expenses - floor) From a4e95d6e3817a390e5cd7ad003dfd075463376f5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 16:57:09 -0400 Subject: [PATCH 4/5] Add Act 35 changelog fragment and a 2026 senior and overtime guard case Co-Authored-By: Claude Opus 5.5 --- .../hi-act-35-2026-conformity.fixed.md | 1 + .../hi/tax/income/hi_taxable_income.yaml | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 changelog.d/hi-act-35-2026-conformity.fixed.md diff --git a/changelog.d/hi-act-35-2026-conformity.fixed.md b/changelog.d/hi-act-35-2026-conformity.fixed.md new file mode 100644 index 00000000000..fe06de0019b --- /dev/null +++ b/changelog.d/hi-act-35-2026-conformity.fixed.md @@ -0,0 +1 @@ +Update Hawaii's income tax for Act 35, SLH 2026 (2025 Internal Revenue Code conformity): apply the overall itemized deduction limitation at Hawaii's $166,800 ($83,400 separate) threshold for every filing status and keep the full deduction when only excluded deductions are claimed; add the 2026-2028 qualified tips deduction and the non-itemizer charitable deduction; end the 2% miscellaneous itemized deductions after 2025; and measure the charitable deduction's limits and 2026 floor against Hawaii AGI. diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_taxable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_taxable_income.yaml index b04035e3dc9..18b820eba7c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_taxable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_taxable_income.yaml @@ -67,3 +67,29 @@ hi_exemptions: 2_288 # 200,000 - 29,004 - 2,288 = 168,708. hi_taxable_income: 168_708 + +- name: Case 5, Hawaii does not adopt the federal senior or overtime deductions in 2026. + period: 2026 + input: + people: + person1: + age: 70 + employment_income: 50_000 + fsla_overtime_premium: 5_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: HI + output: + # The federal return gets both deductions. + additional_senior_deduction: 6_000 + overtime_income_deduction: 5_000 + # Act 35 keeps sections 151 and 225 inoperative for Hawaii. + hi_deductions: 8_000 + # Regular plus aged exemption: 2 * 1,144 = 2,288. + hi_exemptions: 2_288 + # 50,000 - 8,000 - 2,288 = 39,712. + hi_taxable_income: 39_712 From ebc228561571bea54c98c3ec8da50029d0afa2c2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 17:05:23 -0400 Subject: [PATCH 5/5] Add property tests for Hawaii's itemized deduction limitation and tips deduction Checks the reduction against an independent restatement of the Total Itemized Deductions Worksheet, its bounds, monotonicity, identical 2025 and 2026 results under Act 35, and the tips deduction's bounds and years. Co-Authored-By: Claude Opus 5.5 --- ...temized_deduction_limitation_properties.py | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 policyengine_us/tests/core/test_hi_itemized_deduction_limitation_properties.py diff --git a/policyengine_us/tests/core/test_hi_itemized_deduction_limitation_properties.py b/policyengine_us/tests/core/test_hi_itemized_deduction_limitation_properties.py new file mode 100644 index 00000000000..a3e662572af --- /dev/null +++ b/policyengine_us/tests/core/test_hi_itemized_deduction_limitation_properties.py @@ -0,0 +1,294 @@ +"""Invariants of Hawaii's overall limitation on itemized deductions and tips deduction. + +HRS 235-2.4 keeps section 68 of the Internal Revenue Code operative with the +thresholds "operative for federal tax year 2009" ($166,800, or $83,400 for a +married individual filing separately). Act 35, SLH 2026 keeps section 68 "in +the form that it existed as of December 31, 2024" for taxable years beginning +after December 31, 2025, so the 3 percent and 80 percent rates and the section +68(c) exclusions (medical expenses, investment interest, casualty losses) do +not change in 2026. The Total Itemized Deductions Worksheet in the Form N-11 +instructions implements this. + +Act 35 also conforms Hawaii to the section 224 deduction for qualified tips +from 2026 (section 224(h) ends it after 2028). + +Batches of tax units share one simulation per example. The tests check: + +1. hi_itemized_deductions_reduction matches an independent restatement of + worksheet lines 1-10, and hi_itemized_deductions equals line 11. +2. The reduction lies between 0 and both the 3 percent amount and 80 percent + of the deductions that section 68(c) does not exclude, so the excluded + deductions are never reduced. +3. The 2025 and 2026 results are identical for the same inputs. +4. Raising an excluded deduction leaves the reduction unchanged; raising any + deduction never lowers the allowed amount; raising Hawaii AGI never lowers + the reduction. +5. The tips deduction lies between 0 and the smaller of $25,000 and the + qualified tips, never rises with Hawaii AGI, is zero for married filing + separately, and is zero in 2025 and 2029. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +STATUSES = ("SINGLE", "JOINT", "SEPARATE", "HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE") +# Worksheet line 7: "Enter $166,800 ($83,400 if married filing separately)". +THRESHOLD = 166_800 +SEPARATE_THRESHOLD = 83_400 + +# Components of hi_total_itemized_deductions, split by whether section 68(c) +# excludes them. Investment interest and mortgage interest are person inputs +# that hi_interest_deduction adds up. +EXCLUDED = ("hi_medical_expense_deduction", "hi_casualty_loss_deduction") +EXCLUDED_PERSON = ("investment_interest_expense",) +OTHER = ("hi_charitable_deduction", "hi_salt_deduction", "hi_misc_deduction") +OTHER_PERSON = ("mortgage_interest",) +TAX_UNIT_COMPONENTS = EXCLUDED + OTHER +PERSON_COMPONENTS = EXCLUDED_PERSON + OTHER_PERSON +ALL_COMPONENTS = TAX_UNIT_COMPONENTS + PERSON_COMPONENTS +# PolicyEngine computes in float32, whose spacing near $1 million is 6 cents. +RTOL = 1e-6 +ATOL = 0.01 + + +def _slack(scale): + return ATOL + RTOL * np.abs(scale) + + +def _close(actual, desired, scale, message=""): + gap = np.abs(np.asarray(actual, dtype=float) - desired) + worst = int(np.argmax(gap - _slack(scale))) + assert np.all(gap <= _slack(scale)), ( + f"{message} unit {worst}: {actual[worst]} vs {desired[worst]}" + ) + + +money = st.one_of(st.just(0.0), st.integers(1, 120_000).map(float)) + + +@st.composite +def tax_units(draw): + unit = { + "filing_status": draw(st.sampled_from(STATUSES)), + # Hawaii AGI near the thresholds, far above them, and negative. + "hi_agi": float( + draw( + st.one_of( + st.integers(-50_000, 1_500_000), + st.integers(SEPARATE_THRESHOLD - 2, SEPARATE_THRESHOLD + 2), + st.integers(THRESHOLD - 2, THRESHOLD + 2), + ) + ) + ), + } + only_excluded = draw(st.booleans()) + for name in ALL_COMPONENTS: + excluded = name in EXCLUDED + EXCLUDED_PERSON + unit[name] = draw(money) if excluded or not only_excluded else 0.0 + return unit + + +def _arrays(units): + return {key: np.array([unit[key] for unit in units]) for key in units[0]} + + +def _simulate(year, units, outputs): + period = str(year) + people, groups, households = {}, {}, {} + for i, unit in enumerate(units): + person = f"p{i}" + people[person] = {"age": {period: 45}} + for name in PERSON_COMPONENTS: + people[person][name] = {period: unit[name]} + groups[f"t{i}"] = { + "members": [person], + "filing_status": {period: unit["filing_status"]}, + "hi_agi": {period: unit["hi_agi"]}, + **{name: {period: unit[name]} for name in TAX_UNIT_COMPONENTS}, + } + households[f"h{i}"] = {"members": [person], "state_code": {period: "HI"}} + simulation = Simulation( + situation={"people": people, "tax_units": groups, "households": households} + ) + return {name: simulation.calculate(name, period) for name in outputs} + + +def _thresholds(statuses): + return np.where(statuses == "SEPARATE", SEPARATE_THRESHOLD, THRESHOLD) + + +def _split(units): + excluded = sum(units[name] for name in EXCLUDED + EXCLUDED_PERSON) + other = sum(units[name] for name in OTHER + OTHER_PERSON) + return excluded, other + + +def _worksheet_line_10(units): + """Total Itemized Deductions Worksheet, lines 1-10, restated.""" + excluded, other = _split(units) + line_1 = excluded + other + line_3 = excluded + line_4 = np.where(line_3 < line_1, line_1 - line_3, 0) + line_5 = line_4 * 0.8 + line_6 = units["hi_agi"] + line_7 = _thresholds(units["filing_status"]) + line_8 = np.where(line_7 < line_6, line_6 - line_7, 0) + line_9 = line_8 * 0.03 + return np.minimum(line_5, line_9) + + +LIMITATION_OUTPUTS = ( + "hi_total_itemized_deductions", + "hi_itemized_deductions_reduction", + "hi_itemized_deductions", +) + +# A batch's cost is mostly per-variable overhead, so each example is a large +# batch and there are few examples. +SETTINGS = dict( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.lists(tax_units(), min_size=20, max_size=60)) +def test_limitation_matches_the_worksheet_in_2025_and_2026(units): + arrays = _arrays(units) + excluded, other = _split(arrays) + scale = excluded + other + np.abs(arrays["hi_agi"]) + line_10 = _worksheet_line_10(arrays) + results = {} + for year in (2025, 2026): + run = _simulate(year, units, LIMITATION_OUTPUTS) + results[year] = run + total = run["hi_total_itemized_deductions"] + reduction = run["hi_itemized_deductions_reduction"] + allowed = run["hi_itemized_deductions"] + _close(total, excluded + other, scale, f"{year} total") + # 1. Worksheet lines 10 and 11. + _close(reduction, line_10, scale, f"{year} line 10") + _close(allowed, excluded + other - line_10, scale, f"{year} line 11") + # 2. Bounds; excluded deductions are never reduced. + agi_excess = np.maximum( + 0, arrays["hi_agi"] - _thresholds(arrays["filing_status"]) + ) + assert np.all(reduction >= 0) + assert np.all(reduction <= 0.03 * agi_excess + _slack(scale)) + assert np.all(reduction <= 0.8 * other + _slack(scale)) + assert np.all(allowed >= excluded + 0.2 * other - _slack(scale)) + # 3. Act 35 leaves the limitation unchanged in 2026. + for name in LIMITATION_OUTPUTS: + np.testing.assert_array_equal(results[2025][name], results[2026][name]) + + +def test_limitation_responds_to_components_and_income(): + rng = np.random.default_rng(2026) + n = 200 + units = [] + for i in range(n): + unit = { + "filing_status": str(STATUSES[i % len(STATUSES)]), + "hi_agi": float(np.round(rng.uniform(-20_000, 800_000), 2)), + } + for name in ALL_COMPONENTS: + amount = float(np.round(rng.uniform(0, 60_000), 2)) + unit[name] = amount if rng.random() < 0.6 else 0.0 + units.append(unit) + outputs = ("hi_itemized_deductions_reduction", "hi_itemized_deductions") + base = _simulate(2026, units, outputs) + arrays = _arrays(units) + excluded, other = _split(arrays) + scale = excluded + other + np.abs(arrays["hi_agi"]) + 10_000 + assert np.any(base["hi_itemized_deductions_reduction"] > 0) + for name in ALL_COMPONENTS: + raised = [dict(unit, **{name: unit[name] + 1_000}) for unit in units] + run = _simulate(2026, raised, outputs) + # 4. More of any deduction never lowers the allowed amount. + assert np.all( + run["hi_itemized_deductions"] + >= base["hi_itemized_deductions"] - _slack(scale) + ), name + if name in EXCLUDED + EXCLUDED_PERSON: + # Excluded deductions pass through without reduction. + _close( + run["hi_itemized_deductions_reduction"], + base["hi_itemized_deductions_reduction"], + scale, + name, + ) + _close( + run["hi_itemized_deductions"], + base["hi_itemized_deductions"] + 1_000, + scale, + name, + ) + richer = [dict(unit, hi_agi=unit["hi_agi"] + 10_000) for unit in units] + richer_reduction = _simulate(2026, richer, outputs)[ + "hi_itemized_deductions_reduction" + ] + assert np.all( + richer_reduction >= base["hi_itemized_deductions_reduction"] - _slack(scale) + ) + + +def _tips_situation(units, period): + people, groups, households = {}, {}, {} + for i, unit in enumerate(units): + person = f"p{i}" + people[person] = { + "age": {period: 30}, + "tip_income": {period: unit["tip_income"]}, + "treasury_tipped_occupation_code": {period: unit["occupation_code"]}, + } + groups[f"t{i}"] = { + "members": [person], + "filing_status": {period: unit["filing_status"]}, + "hi_agi": {period: unit["hi_agi"]}, + } + households[f"h{i}"] = {"members": [person], "state_code": {period: "HI"}} + return {"people": people, "tax_units": groups, "households": households} + + +def test_tips_deduction_bounds_and_years(): + rng = np.random.default_rng(224) + n = 250 + units = [ + { + "filing_status": str(STATUSES[i % len(STATUSES)]), + "hi_agi": float(np.round(rng.uniform(-10_000, 600_000), 2)), + "tip_income": float(np.round(rng.uniform(0, 40_000), 2)), + "occupation_code": int(rng.choice([0, 102])), + } + for i in range(n) + ] + arrays = _arrays(units) + qualified = arrays["tip_income"] * (arrays["occupation_code"] > 0) + separate = arrays["filing_status"] == "SEPARATE" + for year in (2025, 2029): + period = str(year) + deduction = Simulation(situation=_tips_situation(units, period)).calculate( + "hi_tip_income_deduction", period + ) + # 5. No deduction before Act 35 or after section 224(h) ends it. + assert np.all(deduction == 0), year + for year in (2026, 2028): + period = str(year) + base = Simulation(situation=_tips_situation(units, period)).calculate( + "hi_tip_income_deduction", period + ) + richer_units = [dict(unit, hi_agi=unit["hi_agi"] + 5_000) for unit in units] + richer = Simulation(situation=_tips_situation(richer_units, period)).calculate( + "hi_tip_income_deduction", period + ) + scale = qualified + np.abs(arrays["hi_agi"]) + assert np.all(base >= 0) + assert np.all(base <= np.minimum(25_000, qualified) + _slack(scale)) + assert np.all(base[separate] == 0) + assert np.all(richer <= base + _slack(scale)) + assert np.any(base > 0)