From 747e8ef9c79a1723f86a22a310292991797d288e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 06:40:52 -0400 Subject: [PATCH 1/6] Apply Hawaii refundable credits before nonrefundable credits Form N-11 subtracts refundable credits (line 33) from tax (line 27) to get the adjusted tax liability (line 34), and limits nonrefundable credits (line 35) to it. The model capped nonrefundable credits at gross tax, which overstated the benefit when a filer had both kinds of credit. Co-Authored-By: Claude Opus 5.5 --- changelog.d/hi-credit-ordering.fixed.md | 1 + .../states/hi/tax/income/credits/hi_eitc.py | 17 ++++++--- .../tax/income/hi_adjusted_tax_liability.py | 35 +++++++++++++++++++ ...hi_income_tax_before_refundable_credits.py | 3 ++ .../tax/income/hi_non_refundable_credits.py | 8 ++++- 5 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 changelog.d/hi-credit-ordering.fixed.md create mode 100644 policyengine_us/variables/gov/states/hi/tax/income/hi_adjusted_tax_liability.py diff --git a/changelog.d/hi-credit-ordering.fixed.md b/changelog.d/hi-credit-ordering.fixed.md new file mode 100644 index 00000000000..c84dab26d2f --- /dev/null +++ b/changelog.d/hi-credit-ordering.fixed.md @@ -0,0 +1 @@ +Apply Hawaii refundable credits before nonrefundable credits, limiting nonrefundable credits (including the 2018-2022 Hawaii EITC) to the adjusted tax liability on Form N-11, line 34. diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py index 44a13b437cc..4c0e43f2d4b 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py @@ -11,17 +11,26 @@ class hi_eitc(Variable): defined_for = StateCode.HI unit = USD definition_period = YEAR - reference = "https://www.capitol.hawaii.gov/hrscurrent/Vol04_Ch0201-0257/HRS0235/HRS_0235-0055_0007_0005.htm" + reference = ( + "https://www.capitol.hawaii.gov/hrscurrent/Vol04_Ch0201-0257/HRS0235/HRS_0235-0055_0007_0005.htm", + # Form N-356, lines 10 to 13: the nonrefundable credit is limited to + # the adjusted tax liability on Form N-11, line 34. + "https://files.hawaii.gov/tax/forms/2018/n356_i.pdf#page=1", + "https://files.hawaii.gov/tax/forms/2022/n356_i.pdf#page=1", + ) def formula(tax_unit, period, parameters): - if period.start.year >= 2023: + ordered_credits = parameters( + period + ).gov.states.hi.tax.income.credits.non_refundable + if "hi_eitc" not in ordered_credits: return tax_unit("hi_eitc_potential", period) return applied_state_non_refundable_credit( tax_unit, period, - ["hi_eitc"], - "hi_income_tax_before_non_refundable_credits", + ordered_credits, + "hi_adjusted_tax_liability", "hi_eitc", "hi_eitc_potential", ) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/hi_adjusted_tax_liability.py b/policyengine_us/variables/gov/states/hi/tax/income/hi_adjusted_tax_liability.py new file mode 100644 index 00000000000..4309acdd398 --- /dev/null +++ b/policyengine_us/variables/gov/states/hi/tax/income/hi_adjusted_tax_liability.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * + + +class hi_adjusted_tax_liability(Variable): + value_type = float + entity = TaxUnit + label = "Hawaii adjusted tax liability" + unit = USD + definition_period = YEAR + defined_for = StateCode.HI + reference = ( + # Form N-11, line 34: line 27 (tax) minus line 33 (total refundable + # credits). The result can be negative. + "https://files.hawaii.gov/tax/forms/2018/n11_i.pdf#page=3", + "https://files.hawaii.gov/tax/forms/2025/n11_i.pdf#page=3", + # Nonrefundable credits cannot exceed line 34; none apply when line 34 + # is zero or less. + "https://files.hawaii.gov/tax/forms/2018/n11ins.pdf#page=25", + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=21", + # Act 115 refunds were issued to 2021 Form N-11 filers by the + # department, not claimed on the return. + "https://files.hawaii.gov/tax/news/announce/ann22-03.pdf#page=2", + ) + + def formula(tax_unit, period, parameters): + tax = tax_unit("hi_income_tax_before_non_refundable_credits", period) + refundable_credits = tax_unit("hi_refundable_credits", period) + # Lines 28 to 33 hold only refundable credits claimed on the return, + # so the Act 115 refund does not reduce the adjusted tax liability. + p = parameters(period).gov.states.hi.tax.income.credits + if "hi_act_115_rebate" in p.refundable: + refundable_credits = refundable_credits - tax_unit( + "hi_act_115_rebate", period + ) + return tax - refundable_credits diff --git a/policyengine_us/variables/gov/states/hi/tax/income/hi_income_tax_before_refundable_credits.py b/policyengine_us/variables/gov/states/hi/tax/income/hi_income_tax_before_refundable_credits.py index 4e6ba85a140..3d8675db524 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/hi_income_tax_before_refundable_credits.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/hi_income_tax_before_refundable_credits.py @@ -11,5 +11,8 @@ class hi_income_tax_before_refundable_credits(Variable): def formula(tax_unit, period, parameters): income_tax = tax_unit("hi_income_tax_before_non_refundable_credits", period) + # Nonrefundable credits are already limited to the tax left after + # refundable credits (Form N-11, line 34), so subtracting refundable + # credits from this amount gives the balance on line 36. non_refundable_credits = tax_unit("hi_non_refundable_credits", period) return max_(income_tax - non_refundable_credits, 0) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py b/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py index f9c03325e88..f97cf32e29e 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py @@ -11,6 +11,12 @@ class hi_non_refundable_credits(Variable): unit = USD definition_period = YEAR defined_for = StateCode.HI + reference = ( + # Form N-11 applies refundable credits first (line 33) and limits + # nonrefundable credits (line 35) to the adjusted tax on line 34. + "https://files.hawaii.gov/tax/forms/2025/n11_i.pdf#page=3", + "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=21", + ) def formula(tax_unit, period, parameters): ordered_credits = parameters( @@ -20,5 +26,5 @@ def formula(tax_unit, period, parameters): tax_unit, period, ordered_credits, - "hi_income_tax_before_non_refundable_credits", + "hi_adjusted_tax_liability", ) From c71d63a65b46c269b909b26d92f8f2191bde8bf7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 06:46:02 -0400 Subject: [PATCH 2/6] Test Hawaii credit ordering against hand-worked Form N-11 lines Co-Authored-By: Claude Opus 5.5 --- .../tax/income/hi_adjusted_tax_liability.yaml | 48 ++++++++ .../hi_credit_ordering_integration.yaml | 101 ++++++++++++++++ .../tax/income/hi_non_refundable_credits.yaml | 110 ++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_adjusted_tax_liability.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_credit_ordering_integration.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_non_refundable_credits.yaml diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_adjusted_tax_liability.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_adjusted_tax_liability.yaml new file mode 100644 index 00000000000..5490f1ba67b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_adjusted_tax_liability.yaml @@ -0,0 +1,48 @@ +# Form N-11, line 34: line 27 (tax) minus line 33 (total refundable credits). +# https://files.hawaii.gov/tax/forms/2022/n11_i.pdf#page=3 +# https://files.hawaii.gov/tax/forms/2025/n11_i.pdf#page=3 + +- name: Tax less refundable credits + period: 2022 + input: + state_code: HI + hi_income_tax_before_non_refundable_credits: 1_000 + hi_refundable_credits: 500 + output: + # Line 34 = 1,000 - 500. + hi_adjusted_tax_liability: 500 + +- name: Negative when refundable credits exceed tax + period: 2025 + input: + state_code: HI + hi_income_tax_before_non_refundable_credits: 300 + hi_refundable_credits: 500 + output: + # Line 34 = 300 - 500; the form allows a negative amount. + hi_adjusted_tax_liability: -200 + +- name: The 2021 Act 115 refund is not a credit on the return + period: 2021 + # Act 115 refunds were issued by the department to 2021 Form N-11 filers; + # they are not among lines 28 to 32. + # https://files.hawaii.gov/tax/news/announce/ann22-03.pdf#page=2 + input: + state_code: HI + hi_income_tax_before_non_refundable_credits: 1_000 + hi_refundable_credits: 410 + hi_act_115_rebate: 300 + output: + # Line 33 = 410 - 300 = 110; line 34 = 1,000 - 110. + hi_adjusted_tax_liability: 890 + +- name: The Act 115 refund is not subtracted in years it is not paid + period: 2022 + input: + state_code: HI + hi_income_tax_before_non_refundable_credits: 1_000 + hi_refundable_credits: 410 + hi_act_115_rebate: 300 + output: + # The refund is not in the 2022 refundable list: line 34 = 1,000 - 410. + hi_adjusted_tax_liability: 590 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_credit_ordering_integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_credit_ordering_integration.yaml new file mode 100644 index 00000000000..3e70172cf60 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_credit_ordering_integration.yaml @@ -0,0 +1,101 @@ +# A single filer aged 30 with $8,000 of wages and no other income, worked +# through Form N-11 lines 26 to 36 by hand. Refundable credits (line 33) come +# off the tax (line 27) first; nonrefundable credits (line 35) are limited to +# the adjusted tax liability that remains (line 34). +# https://files.hawaii.gov/tax/forms/2022/n11_i.pdf#page=3 +# https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=21 + +- name: 2022 nonrefundable EITC limited to the tax left after the food/excise credit + period: 2022 + input: + people: + head: + age: 30 + employment_income: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: HI + output: + # Line 26 = 8,000 - 2,200 standard deduction - 1,144 exemption. + hi_taxable_income: 4_656 + # Line 27 = 1.4% x 2,400 + 3.2% x (4,656 - 2,400) = 33.60 + 72.192. + hi_income_tax_before_non_refundable_credits: 105.792 + # Line 28, Form N-311: AGI from 5,000 to 9,999 gives 100 per exemption. + hi_food_excise_credit: 100 + hi_refundable_credits: 100 + # Line 34 = 105.792 - 100. + hi_adjusted_tax_liability: 5.792 + # Federal EITC = min(7.65% x 8,000, 560) = 560; Form N-356 line 3 = 20%. + hi_eitc_potential: 112 + # N-356 line 13 = min(112, 5.792); Schedule CR and line 35. + hi_eitc: 5.792 + hi_non_refundable_credits: 5.792 + # Line 36 = 5.792 - 5.792. + hi_income_tax: 0 + +- name: 2021 Act 115 refund paid on top of the return balance + period: 2021 + input: + people: + head: + age: 30 + employment_income: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: HI + output: + # Lines 26 to 28 as in 2022. + hi_income_tax_before_non_refundable_credits: 105.792 + hi_food_excise_credit: 100 + # One exemption at 300 for federal AGI under 100,000, issued by the + # department apart from the return. + # https://files.hawaii.gov/tax/news/announce/ann22-03.pdf#page=2 + hi_act_115_rebate: 300 + hi_refundable_credits: 400 + # Line 33 = 100 (the Act 115 refund is not on it); line 34 = 5.792. + hi_adjusted_tax_liability: 5.792 + # Federal EITC = 15.3% x 8,000 = 1,224 (2021 phase-in); 20% = 244.80. + hi_eitc_potential: 244.8 + hi_eitc: 5.792 + hi_non_refundable_credits: 5.792 + # Line 36 = 0, less the 300 Act 115 refund. + hi_income_tax: -300 + +- name: 2025 refundable credits paid in full beyond the tax + period: 2025 + input: + people: + head: + age: 30 + employment_income: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: HI + output: + # Line 26 = 8,000 - 4,400 standard deduction - 1,144 exemption. + hi_taxable_income: 2_456 + # Line 27 = 1.4% x 2,456. + hi_income_tax_before_non_refundable_credits: 34.384 + # Line 28, Form N-311: AGI under 15,000 gives 220 per exemption. + hi_food_excise_credit: 220 + # Federal EITC = 7.65% x 8,000 = 612; Hawaii EITC = 40% x 612 = 244.80, + # refundable on Schedule CR Part I (line 32). + hi_eitc: 244.8 + # Line 33 = 220 + 244.80. + hi_refundable_credits: 464.8 + # Line 34 = 34.384 - 464.80; line 35 = 0; line 36 = line 34. + hi_adjusted_tax_liability: -430.416 + hi_non_refundable_credits: 0 + hi_income_tax: -430.416 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_non_refundable_credits.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_non_refundable_credits.yaml new file mode 100644 index 00000000000..8b12267c784 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_non_refundable_credits.yaml @@ -0,0 +1,110 @@ +# Form N-11 applies refundable credits first: line 34 is line 27 (tax) minus +# line 33 (total refundable credits), and the nonrefundable credits on line 35 +# "cannot be greater than your adjusted tax liability. If line 34 is zero or +# less, no tax credit may be used." Line 36 is line 34 minus line 35. +# https://files.hawaii.gov/tax/forms/2022/n11_i.pdf#page=3 +# https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=21 +# Form N-356, lines 10 to 13, limits the 2018-2022 nonrefundable earned income +# tax credit to the same adjusted tax liability. +# https://files.hawaii.gov/tax/forms/2022/n356_i.pdf#page=1 + +- name: Nonrefundable credits are limited to tax less refundable credits + period: 2022 + input: + state_code: HI + hi_income_tax_before_non_refundable_credits: 1_000 + hi_refundable_credits: 500 + # Hawaii EITC = 20% x 4,000 = 800. + eitc: 4_000 + output: + # Line 27 = 1,000; line 33 = 500; line 34 = 500. + hi_adjusted_tax_liability: 500 + hi_eitc_potential: 800 + # N-356 line 13 = min(800, 500); line 35 = 500. + hi_eitc: 500 + hi_non_refundable_credits: 500 + hi_income_tax_before_refundable_credits: 500 + # Line 36 = 500 - 500. Capping at gross tax instead gave + # 1,000 - 800 - 500 = -300. + hi_income_tax: 0 + +- name: No nonrefundable credit when refundable credits exceed tax + period: 2022 + input: + state_code: HI + hi_income_tax_before_non_refundable_credits: 300 + hi_refundable_credits: 500 + eitc: 4_000 + output: + # Line 34 = 300 - 500 = -200, so line 35 = 0. + hi_eitc: 0 + hi_non_refundable_credits: 0 + hi_income_tax_before_refundable_credits: 300 + # Line 36 = -200: the refundable credits are paid in full. + hi_income_tax: -200 + +- name: No nonrefundable credit when refundable credits equal tax + period: 2022 + input: + state_code: HI + hi_income_tax_before_non_refundable_credits: 500 + hi_refundable_credits: 500 + eitc: 4_000 + output: + # Line 34 = 0. + hi_non_refundable_credits: 0 + hi_income_tax: 0 + +- name: Full nonrefundable credit when the adjusted tax covers it + period: 2022 + input: + state_code: HI + hi_income_tax_before_non_refundable_credits: 2_000 + hi_refundable_credits: 500 + eitc: 4_000 + output: + # Line 34 = 1,500; line 35 = 800; line 36 = 700. + hi_non_refundable_credits: 800 + hi_income_tax_before_refundable_credits: 1_200 + hi_income_tax: 700 + +- name: The 2025 earned income tax credit is refundable + period: 2025 + input: + state_code: HI + hi_income_tax_before_non_refundable_credits: 1_000 + # Hawaii EITC = 40% x 4,000 = 1,600, on Schedule CR Part I. + eitc: 4_000 + hi_food_excise_credit: 0 + hi_cdcc: 0 + hi_tax_credit_for_low_income_household_renters: 0 + output: + hi_refundable_credits: 1_600 + # Line 34 = 1,000 - 1,600; no nonrefundable credits in 2025. + hi_adjusted_tax_liability: -600 + hi_non_refundable_credits: 0 + hi_income_tax: -600 + +- name: A nonrefundable 2025 credit is limited to tax less refundable credits + period: 2025 + # Treat the 2025 earned income tax credit as nonrefundable, as it was + # before 2023, to apply the line 34 limit with 2025 parameters. + input: + gov.states.hi.tax.income.credits.non_refundable: [hi_eitc] + gov.states.hi.tax.income.credits.refundable: + [hi_food_excise_credit, hi_cdcc, hi_tax_credit_for_low_income_household_renters] + state_code: HI + hi_income_tax_before_non_refundable_credits: 1_000 + hi_food_excise_credit: 300 + hi_cdcc: 200 + hi_tax_credit_for_low_income_household_renters: 0 + # Hawaii EITC = 40% x 4,000 = 1,600. + eitc: 4_000 + output: + hi_refundable_credits: 500 + # Line 34 = 1,000 - 500; line 35 = min(1,600, 500). + hi_adjusted_tax_liability: 500 + hi_eitc: 500 + hi_non_refundable_credits: 500 + # Line 36 = 500 - 500. + hi_income_tax: 0 From d8e37f8efd6f5e5fe1c97f70b26287c43e068da5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 06:59:55 -0400 Subject: [PATCH 3/6] Add property tests for Hawaii's Form N-11 credit ordering Co-Authored-By: Claude Opus 5.5 --- .../tests/test_hi_credit_ordering.py | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 policyengine_us/tests/test_hi_credit_ordering.py diff --git a/policyengine_us/tests/test_hi_credit_ordering.py b/policyengine_us/tests/test_hi_credit_ordering.py new file mode 100644 index 00000000000..5700e5b455c --- /dev/null +++ b/policyengine_us/tests/test_hi_credit_ordering.py @@ -0,0 +1,301 @@ +"""Property tests: Hawaii applies refundable credits before nonrefundable ones. + +Form N-11, lines 27 to 36, in every year from 2018 to 2025: + +- line 33 totals the refundable credits claimed on the return; +- line 34, the adjusted tax liability, is line 27 (tax) minus line 33; +- line 35 totals the nonrefundable credits, and the instructions say "the + total of the nonrefundable credits used cannot be greater than your + adjusted tax liability. If line 34 is zero or less, nonrefundable tax + credits may not be used"; +- line 36 is line 34 minus line 35. + +Form N-356, lines 10 to 13, limits the 2018 to 2022 nonrefundable earned +income tax credit (20% of the federal credit) to line 34. The Act 115 refund +of 2021 was issued apart from the return, so it is not on line 33. + +A reference written from those lines is compared with the model. For tax T, +refundable credits R (of which the Act 115 refund is A in 2021), and a +nonrefundable potential N (the earned income tax credit while it is +nonrefundable, zero from 2023): + +1. hi_adjusted_tax_liability = T - (R - A). +2. hi_non_refundable_credits = min(N, max(T - (R - A), 0)). +3. hi_income_tax = line 36 - A, and also equals + hi_income_tax_before_refundable_credits - hi_refundable_credits, the + identity the household-level state lists rely on. +4. Refundable credits are paid in full: when R - A >= T, no nonrefundable + credit applies and hi_income_tax = T - R. +5. min(T - R, -A) <= hi_income_tax <= T - R. +6. Raising R by d lowers hi_income_tax by between 0 and d, and raising the + federal earned income tax credit never raises it. + +The model computes in single precision, so comparisons allow one cent or +eight float32 spacings at the largest amount, whichever is larger. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEARS = [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025] +# Years in which the Hawaii earned income tax credit is nonrefundable and the +# Act 115 refund is paid. +NON_REFUNDABLE_EITC_YEARS = range(2018, 2023) +ACT_115_YEAR = 2021 +EITC_MATCH = 0.2 +MAX_AMOUNT = 20_000 + +OUTPUTS = [ + "hi_adjusted_tax_liability", + "hi_non_refundable_credits", + "hi_income_tax_before_refundable_credits", + "hi_income_tax", + "state_income_tax", + "state_non_refundable_credits", +] + + +def tolerance(*amounts): + scale = np.float32(max(np.max(np.abs(a)) for a in amounts)) + return max(0.01, 8 * float(np.spacing(scale))) + + +def act_115_paid(unit, year): + return unit["act_115"] if year == ACT_115_YEAR else 0 + + +def calculate(units, year): + """One Hawaii tax unit per household. Each unit sets its tax, refundable + credits, Act 115 refund and federal earned income tax credit.""" + people, tax_units, households = {}, {}, {} + for i, unit in enumerate(units): + people[f"p{i}"] = {"age": {year: 30}} + tax_units[f"t{i}"] = { + "members": [f"p{i}"], + "hi_income_tax_before_non_refundable_credits": {year: unit["tax"]}, + "hi_refundable_credits": {year: unit["refundable"]}, + # The refund is paid only in 2021. (The model's 2021 refundable + # list also reaches back to 2015-2020; a zero refund keeps those + # years to the law.) + "hi_act_115_rebate": {year: act_115_paid(unit, year)}, + "eitc": {year: unit["eitc"]}, + } + households[f"h{i}"] = {"members": [f"p{i}"], "state_code": {year: "HI"}} + sim = Simulation( + situation={"people": people, "tax_units": tax_units, "households": households} + ) + return {name: sim.calculate(name, year) for name in OUTPUTS} + + +def reference(units, year): + tax = np.array([u["tax"] for u in units], dtype=float) + refundable = np.array([u["refundable"] for u in units], dtype=float) + act_115 = np.array([act_115_paid(u, year) for u in units], dtype=float) + eitc = np.array([u["eitc"] for u in units], dtype=float) + potential = EITC_MATCH * eitc if year in NON_REFUNDABLE_EITC_YEARS else 0 * eitc + line_34 = tax - (refundable - act_115) + line_35 = np.minimum(potential, np.maximum(line_34, 0)) + return tax, refundable, act_115, line_34, line_35 + + +def check(units, year): + result = calculate(units, year) + tax, refundable, act_115, line_34, line_35 = reference(units, year) + tol = tolerance(tax, refundable, line_34) + income_tax = result["hi_income_tax"] + # 1. Line 34. + np.testing.assert_allclose(result["hi_adjusted_tax_liability"], line_34, atol=tol) + # 2. Line 35. + np.testing.assert_allclose(result["hi_non_refundable_credits"], line_35, atol=tol) + # 3. Line 36, less the Act 115 refund, and the household identity. + np.testing.assert_allclose(income_tax, line_34 - line_35 - act_115, atol=tol) + np.testing.assert_allclose( + income_tax, + result["hi_income_tax_before_refundable_credits"] - refundable, + atol=tol, + ) + np.testing.assert_allclose(result["state_income_tax"], income_tax, atol=tol) + np.testing.assert_allclose( + result["state_non_refundable_credits"], + result["hi_non_refundable_credits"], + atol=tol, + ) + # 4. Refundable credits are paid in full. + covered = line_34 <= 0 + assert (result["hi_non_refundable_credits"][covered] <= tol).all() + np.testing.assert_allclose( + income_tax[covered], (tax - refundable)[covered], atol=tol + ) + # 5. Bounds. + assert (income_tax <= tax - refundable + tol).all() + assert (income_tax >= np.minimum(tax - refundable, -act_115) - tol).all() + return result + + +def check_monotonic(units, year, refundable_step, eitc_step): + """6. A second copy of every unit with more refundable credits, and a + third with a larger federal earned income tax credit.""" + more_refundable = [ + {**u, "refundable": u["refundable"] + refundable_step} for u in units + ] + more_eitc = [{**u, "eitc": u["eitc"] + eitc_step} for u in units] + result = check(units + more_refundable + more_eitc, year) + n = len(units) + income_tax = result["hi_income_tax"] + base, raised_r, raised_e = ( + income_tax[:n], + income_tax[n : 2 * n], + income_tax[2 * n :], + ) + tol = tolerance(income_tax) + change = base - raised_r + assert (change >= -tol).all() + assert (change <= refundable_step + tol).all() + assert (raised_e <= base + tol).all() + + +amount = st.one_of(st.just(0), st.integers(1, MAX_AMOUNT)) +unit_strategy = st.builds( + lambda tax, refundable, act_115_share, eitc: { + "tax": tax, + "refundable": refundable, + # The Act 115 refund is part of the refundable credits. + "act_115": act_115_share * refundable // 100, + "eitc": eitc, + }, + amount, + amount, + st.integers(0, 100), + amount, +) + +# Boundaries: no tax, tax equal to the refundable credits, tax equal to the +# refundable credits plus the nonrefundable credit, and either side of each. +GRID = [ + {"tax": tax, "refundable": refundable, "act_115": act_115, "eitc": eitc} + for refundable in (0, 300, 1_000) + for act_115 in sorted({0, refundable // 3}) + for eitc in (0, 2_500) + for tax in sorted( + { + 0, + max(refundable - act_115 - 1, 0), + refundable - act_115, + refundable - act_115 + 1, + refundable - act_115 + 250, + refundable - act_115 + 500, + refundable - act_115 + 501, + 5_000, + } + ) +] + + +@pytest.mark.parametrize("year", YEARS) +def test_grid_follows_form_n11(year): + check_monotonic(GRID, year, refundable_step=200, eitc_step=1_000) + + +@settings( + max_examples=10, + deadline=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given( + st.lists(unit_strategy, min_size=1, max_size=20), + st.sampled_from(YEARS), + st.integers(1, 2_000), + st.integers(1, 5_000), +) +def test_random_units_follow_form_n11(units, year, refundable_step, eitc_step): + check_monotonic(units, year, refundable_step, eitc_step) + + +def test_grid_binds_each_case(): + # The 2022 grid has units with line 34 at or below zero, units whose + # nonrefundable credit is cut to line 34, and units that use it in full. + _, _, _, line_34, line_35 = reference(GRID, 2022) + potential = EITC_MATCH * np.array([u["eitc"] for u in GRID]) + assert (line_34 <= 0).any() + assert ((line_35 > 0) & (line_35 < potential)).any() + assert ((line_35 > 0) & (line_35 == potential)).any() + + +def calculate_households(households, year): + """Hawaii households whose credits the model computes: a head (and a + spouse) with wages and rent, and children aged 5.""" + people, tax_units, households_out = {}, {}, {} + for i, h in enumerate(households): + members = [f"head{i}"] + people[f"head{i}"] = { + "age": {year: 30}, + "employment_income": {year: h["wages"]}, + "rent": {year: h["rent"]}, + } + if h["married"]: + members.append(f"spouse{i}") + people[f"spouse{i}"] = { + "age": {year: 30}, + "employment_income": {year: h["spouse_wages"]}, + } + for c in range(h["children"]): + members.append(f"child{i}_{c}") + people[f"child{i}_{c}"] = {"age": {year: 5}} + tax_units[f"t{i}"] = {"members": members} + households_out[f"h{i}"] = {"members": members, "state_code": {year: "HI"}} + sim = Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "households": households_out, + } + ) + names = OUTPUTS + [ + "hi_income_tax_before_non_refundable_credits", + "hi_refundable_credits", + "hi_act_115_rebate", + "hi_eitc_potential", + ] + return {name: sim.calculate(name, year) for name in names} + + +household_strategy = st.fixed_dictionaries( + { + "married": st.booleans(), + "children": st.integers(0, 3), + "wages": st.one_of(st.just(0), st.integers(1, 60_000)), + "spouse_wages": st.one_of(st.just(0), st.integers(1, 30_000)), + "rent": st.one_of(st.just(0), st.integers(1, 24_000)), + } +) + + +@settings( + max_examples=10, + deadline=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given( + st.lists(household_strategy, min_size=1, max_size=20), + st.sampled_from([2021, 2022, 2025]), +) +def test_computed_credits_follow_form_n11(households, year): + # Properties 1 to 3 with the tax and credits the model computes. + r = calculate_households(households, year) + tax = r["hi_income_tax_before_non_refundable_credits"] + refundable = r["hi_refundable_credits"] + act_115 = r["hi_act_115_rebate"] if year == ACT_115_YEAR else 0 * tax + potential = r["hi_eitc_potential"] if year in NON_REFUNDABLE_EITC_YEARS else 0 * tax + line_34 = tax - (refundable - act_115) + line_35 = np.minimum(potential, np.maximum(line_34, 0)) + tol = tolerance(tax, refundable) + np.testing.assert_allclose(r["hi_adjusted_tax_liability"], line_34, atol=tol) + np.testing.assert_allclose(r["hi_non_refundable_credits"], line_35, atol=tol) + np.testing.assert_allclose( + r["hi_income_tax"], line_34 - line_35 - act_115, atol=tol + ) + np.testing.assert_allclose(r["state_income_tax"], r["hi_income_tax"], atol=tol) From f8b79a4572ff3e652deb7572737f426f1ef7a80c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 07:04:47 -0400 Subject: [PATCH 4/6] Cite HRS 235-91.5 for Hawaii's refundable-first credit order Co-Authored-By: Claude Opus 5.5 --- .../gov/states/hi/tax/income/hi_adjusted_tax_liability.py | 3 +++ .../gov/states/hi/tax/income/hi_non_refundable_credits.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/hi_adjusted_tax_liability.py b/policyengine_us/variables/gov/states/hi/tax/income/hi_adjusted_tax_liability.py index 4309acdd398..280df2f8e7b 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/hi_adjusted_tax_liability.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/hi_adjusted_tax_liability.py @@ -9,6 +9,9 @@ class hi_adjusted_tax_liability(Variable): definition_period = YEAR defined_for = StateCode.HI reference = ( + # HRS 235-91.5: refundable credits are used first, followed by + # nonrefundable credits. + "https://www.capitol.hawaii.gov/hrscurrent/Vol04_Ch0201-0257/HRS0235/HRS_0235-0091_0005.htm", # Form N-11, line 34: line 27 (tax) minus line 33 (total refundable # credits). The result can be negative. "https://files.hawaii.gov/tax/forms/2018/n11_i.pdf#page=3", diff --git a/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py b/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py index f97cf32e29e..6ff82ed82d4 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py @@ -12,6 +12,9 @@ class hi_non_refundable_credits(Variable): definition_period = YEAR defined_for = StateCode.HI reference = ( + # HRS 235-91.5: refundable credits are used first, followed by + # nonrefundable credits. + "https://www.capitol.hawaii.gov/hrscurrent/Vol04_Ch0201-0257/HRS0235/HRS_0235-0091_0005.htm", # Form N-11 applies refundable credits first (line 33) and limits # nonrefundable credits (line 35) to the adjusted tax on line 34. "https://files.hawaii.gov/tax/forms/2025/n11_i.pdf#page=3", From 4ef0258fa356225909c9ca67603c3691ef1001c0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 11:00:10 -0400 Subject: [PATCH 5/6] Address review of the Hawaii credit ordering fix Replace the dead 2018 Form N-356 link with its archived copy and register the dead 2018-2021 N-356 links; cite the 2018 and 2022 N-11 in hi_non_refundable_credits; move the household cases into integration.yaml and say that line 27 comes from the rate schedule; guard against a credit in both Hawaii lists; add a computed wage ladder that must reach the line 34 limit. Co-Authored-By: Claude Opus 5.5 --- .../known_dead_reference_urls/west.toml | 20 ++++ .../hi_credit_ordering_integration.yaml | 101 ----------------- .../gov/states/hi/tax/income/integration.yaml | 103 ++++++++++++++++++ .../tests/test_hi_credit_ordering.py | 56 +++++++++- .../states/hi/tax/income/credits/hi_eitc.py | 6 +- ...hi_income_tax_before_refundable_credits.py | 3 +- .../tax/income/hi_non_refundable_credits.py | 6 +- 7 files changed, 185 insertions(+), 110 deletions(-) delete mode 100644 policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_credit_ordering_integration.yaml diff --git a/policyengine_us/tests/code_health/known_dead_reference_urls/west.toml b/policyengine_us/tests/code_health/known_dead_reference_urls/west.toml index cdb9aecf815..1c9e487de81 100644 --- a/policyengine_us/tests/code_health/known_dead_reference_urls/west.toml +++ b/policyengine_us/tests/code_health/known_dead_reference_urls/west.toml @@ -177,3 +177,23 @@ replacement = "https://www.idsaves.org/tax-benefits/" prefix = "https://www.srca.nm.gov/parts/title08/08.401.0002.html" verified = "2026-10-01" replacement = "https://prod-rf-lambda.rtssaas.com/PublicFiles/d89c47bd0d70402dba89b03a22bda6d1/45b8668e-460f-45cd-804f-92ee3d442343/8.401.2.html" + +[[dead]] +prefix = "https://files.hawaii.gov/tax/forms/2018/n356_i.pdf" +verified = "2026-10-13" +replacement = "https://web.archive.org/web/20190819110547/https://files.hawaii.gov/tax/forms/2018/n356_i.pdf" + +[[dead]] +prefix = "https://files.hawaii.gov/tax/forms/2019/n356_i.pdf" +verified = "2026-10-13" +replacement = "https://web.archive.org/web/20201129163620/https://files.hawaii.gov/tax/forms/2019/n356_i.pdf" + +[[dead]] +prefix = "https://files.hawaii.gov/tax/forms/2020/n356_i.pdf" +verified = "2026-10-13" +replacement = "https://web.archive.org/web/20210624115312/https://files.hawaii.gov/tax/forms/2020/n356_i.pdf" + +[[dead]] +prefix = "https://files.hawaii.gov/tax/forms/2021/n356_i.pdf" +verified = "2026-10-13" +replacement = "https://web.archive.org/web/20220526233754/https://files.hawaii.gov/tax/forms/2021/n356_i.pdf" diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_credit_ordering_integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_credit_ordering_integration.yaml deleted file mode 100644 index 3e70172cf60..00000000000 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_credit_ordering_integration.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# A single filer aged 30 with $8,000 of wages and no other income, worked -# through Form N-11 lines 26 to 36 by hand. Refundable credits (line 33) come -# off the tax (line 27) first; nonrefundable credits (line 35) are limited to -# the adjusted tax liability that remains (line 34). -# https://files.hawaii.gov/tax/forms/2022/n11_i.pdf#page=3 -# https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=21 - -- name: 2022 nonrefundable EITC limited to the tax left after the food/excise credit - period: 2022 - input: - people: - head: - age: 30 - employment_income: 8_000 - tax_units: - tax_unit: - members: [head] - households: - household: - members: [head] - state_code: HI - output: - # Line 26 = 8,000 - 2,200 standard deduction - 1,144 exemption. - hi_taxable_income: 4_656 - # Line 27 = 1.4% x 2,400 + 3.2% x (4,656 - 2,400) = 33.60 + 72.192. - hi_income_tax_before_non_refundable_credits: 105.792 - # Line 28, Form N-311: AGI from 5,000 to 9,999 gives 100 per exemption. - hi_food_excise_credit: 100 - hi_refundable_credits: 100 - # Line 34 = 105.792 - 100. - hi_adjusted_tax_liability: 5.792 - # Federal EITC = min(7.65% x 8,000, 560) = 560; Form N-356 line 3 = 20%. - hi_eitc_potential: 112 - # N-356 line 13 = min(112, 5.792); Schedule CR and line 35. - hi_eitc: 5.792 - hi_non_refundable_credits: 5.792 - # Line 36 = 5.792 - 5.792. - hi_income_tax: 0 - -- name: 2021 Act 115 refund paid on top of the return balance - period: 2021 - input: - people: - head: - age: 30 - employment_income: 8_000 - tax_units: - tax_unit: - members: [head] - households: - household: - members: [head] - state_code: HI - output: - # Lines 26 to 28 as in 2022. - hi_income_tax_before_non_refundable_credits: 105.792 - hi_food_excise_credit: 100 - # One exemption at 300 for federal AGI under 100,000, issued by the - # department apart from the return. - # https://files.hawaii.gov/tax/news/announce/ann22-03.pdf#page=2 - hi_act_115_rebate: 300 - hi_refundable_credits: 400 - # Line 33 = 100 (the Act 115 refund is not on it); line 34 = 5.792. - hi_adjusted_tax_liability: 5.792 - # Federal EITC = 15.3% x 8,000 = 1,224 (2021 phase-in); 20% = 244.80. - hi_eitc_potential: 244.8 - hi_eitc: 5.792 - hi_non_refundable_credits: 5.792 - # Line 36 = 0, less the 300 Act 115 refund. - hi_income_tax: -300 - -- name: 2025 refundable credits paid in full beyond the tax - period: 2025 - input: - people: - head: - age: 30 - employment_income: 8_000 - tax_units: - tax_unit: - members: [head] - households: - household: - members: [head] - state_code: HI - output: - # Line 26 = 8,000 - 4,400 standard deduction - 1,144 exemption. - hi_taxable_income: 2_456 - # Line 27 = 1.4% x 2,456. - hi_income_tax_before_non_refundable_credits: 34.384 - # Line 28, Form N-311: AGI under 15,000 gives 220 per exemption. - hi_food_excise_credit: 220 - # Federal EITC = 7.65% x 8,000 = 612; Hawaii EITC = 40% x 612 = 244.80, - # refundable on Schedule CR Part I (line 32). - hi_eitc: 244.8 - # Line 33 = 220 + 244.80. - hi_refundable_credits: 464.8 - # Line 34 = 34.384 - 464.80; line 35 = 0; line 36 = line 34. - hi_adjusted_tax_liability: -430.416 - hi_non_refundable_credits: 0 - hi_income_tax: -430.416 diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/integration.yaml index 146fde13f04..740a39764fc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/integration.yaml @@ -311,3 +311,106 @@ # Total = $268.80 + $307.20 + $528.00 + $614.40 + $1,632.00 + $1,728.00 # + $11,704.00 + $7,900.00 + $4,125.00 = $28,807.40 hi_income_tax_before_non_refundable_credits: 28_807.40 + +# Credit ordering: a single filer aged 30 with $8,000 of wages and no other +# income, through Form N-11 lines 26 to 36. Refundable credits (line 33) come +# off the tax (line 27) first; nonrefundable credits (line 35) are limited to +# the adjusted tax liability that remains (line 34). HRS 235-91.5. +# https://files.hawaii.gov/tax/forms/2022/n11_i.pdf#page=3 +# https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=21 + +- name: 2022 nonrefundable EITC limited to the tax left after the food/excise credit + period: 2022 + input: + people: + head: + age: 30 + employment_income: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: HI + output: + # Line 26 = 8,000 - 2,200 standard deduction - 1,144 exemption. + hi_taxable_income: 4_656 + # Line 27 by the rate schedule (the model does not use the Tax Table): + # 1.4% x 2,400 + 3.2% x (4,656 - 2,400) = 33.60 + 72.192. + hi_income_tax_before_non_refundable_credits: 105.792 + # Line 28, Form N-311: AGI from 5,000 to 9,999 gives 100 per exemption. + hi_food_excise_credit: 100 + hi_refundable_credits: 100 + # Line 34 = 105.792 - 100. + hi_adjusted_tax_liability: 5.792 + # Federal EITC = min(7.65% x 8,000, 560) = 560; Form N-356 line 3 = 20%. + hi_eitc_potential: 112 + # N-356 line 13 = min(112, 5.792); Schedule CR and line 35. + hi_eitc: 5.792 + hi_non_refundable_credits: 5.792 + # Line 36 = 5.792 - 5.792. + hi_income_tax: 0 + +- name: 2021 Act 115 refund paid on top of the return balance + period: 2021 + input: + people: + head: + age: 30 + employment_income: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: HI + output: + # Lines 26 to 28 as in 2022. + hi_income_tax_before_non_refundable_credits: 105.792 + hi_food_excise_credit: 100 + # One exemption at 300 for federal AGI under 100,000, issued by the + # department apart from the return. + # https://files.hawaii.gov/tax/news/announce/ann22-03.pdf#page=2 + hi_act_115_rebate: 300 + hi_refundable_credits: 400 + # Line 33 = 100 (the Act 115 refund is not on it); line 34 = 5.792. + hi_adjusted_tax_liability: 5.792 + # Federal EITC = 15.3% x 8,000 = 1,224 (2021 phase-in); 20% = 244.80. + hi_eitc_potential: 244.8 + hi_eitc: 5.792 + hi_non_refundable_credits: 5.792 + # Line 36 = 0, less the 300 Act 115 refund. + hi_income_tax: -300 + +- name: 2025 refundable credits paid in full beyond the tax + period: 2025 + input: + people: + head: + age: 30 + employment_income: 8_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: HI + output: + # Line 26 = 8,000 - 4,400 standard deduction - 1,144 exemption. + hi_taxable_income: 2_456 + # Line 27 by the rate schedule: 1.4% x 2,456. + hi_income_tax_before_non_refundable_credits: 34.384 + # Line 28, Form N-311: AGI under 15,000 gives 220 per exemption. + hi_food_excise_credit: 220 + # Federal EITC = 7.65% x 8,000 = 612; Hawaii EITC = 40% x 612 = 244.80, + # refundable on Schedule CR Part I (line 32). + hi_eitc: 244.8 + # Line 33 = 220 + 244.80. + hi_refundable_credits: 464.8 + # Line 34 = 34.384 - 464.80; line 35 = 0; line 36 = line 34. + hi_adjusted_tax_liability: -430.416 + hi_non_refundable_credits: 0 + hi_income_tax: -430.416 diff --git a/policyengine_us/tests/test_hi_credit_ordering.py b/policyengine_us/tests/test_hi_credit_ordering.py index 5700e5b455c..6054369692a 100644 --- a/policyengine_us/tests/test_hi_credit_ordering.py +++ b/policyengine_us/tests/test_hi_credit_ordering.py @@ -78,9 +78,8 @@ def calculate(units, year): "members": [f"p{i}"], "hi_income_tax_before_non_refundable_credits": {year: unit["tax"]}, "hi_refundable_credits": {year: unit["refundable"]}, - # The refund is paid only in 2021. (The model's 2021 refundable - # list also reaches back to 2015-2020; a zero refund keeps those - # years to the law.) + # The refund is paid only in 2021; a zero refund in other years + # keeps the reference to the law whatever the parameters list. "hi_act_115_rebate": {year: act_115_paid(unit, year)}, "eitc": {year: unit["eitc"]}, } @@ -111,8 +110,12 @@ def check(units, year): np.testing.assert_allclose(result["hi_adjusted_tax_liability"], line_34, atol=tol) # 2. Line 35. np.testing.assert_allclose(result["hi_non_refundable_credits"], line_35, atol=tol) - # 3. Line 36, less the Act 115 refund, and the household identity. + # 3. Line 36, less the Act 115 refund. This also shows that the floor in + # hi_income_tax_before_refundable_credits never binds. np.testing.assert_allclose(income_tax, line_34 - line_35 - act_115, atol=tol) + # The identities the household-level lists rely on. They hold by the + # current definitions of hi_income_tax and state_income_tax; these + # guard them against a later change to either. np.testing.assert_allclose( income_tax, result["hi_income_tax_before_refundable_credits"] - refundable, @@ -299,3 +302,48 @@ def test_computed_credits_follow_form_n11(households, year): r["hi_income_tax"], line_34 - line_35 - act_115, atol=tol ) np.testing.assert_allclose(r["state_income_tax"], r["hi_income_tax"], atol=tol) + + +def test_credit_lists_do_not_overlap(): + # A credit in both lists would make hi_non_refundable_credits depend on + # itself through line 34 (hi_adjusted_tax_liability). + from policyengine_us.system import system + + for year in range(2015, 2036): + credits = system.parameters(f"{year}-01-01").gov.states.hi.tax.income.credits + overlap = set(credits.refundable) & set(credits.non_refundable) + assert not overlap, (year, overlap) + + +@pytest.mark.parametrize("year", [2021, 2022]) +def test_computed_wage_ladder_reaches_the_line_34_limit(year): + # Single filers and married filers with two children, $0 to $40,000 of + # wages in $500 steps: low earners have refundable credits above the tax, + # then the nonrefundable credit is cut to line 34, then used in full. + households = [ + { + "married": married, + "children": 2 if married else 0, + "wages": wages, + "spouse_wages": 0, + "rent": 0, + } + for married in (False, True) + for wages in range(0, 40_001, 500) + ] + r = calculate_households(households, year) + tax = r["hi_income_tax_before_non_refundable_credits"] + refundable = r["hi_refundable_credits"] + act_115 = r["hi_act_115_rebate"] if year == ACT_115_YEAR else 0 * tax + potential = r["hi_eitc_potential"] + line_34 = tax - (refundable - act_115) + line_35 = np.minimum(potential, np.maximum(line_34, 0)) + tol = tolerance(tax, refundable) + np.testing.assert_allclose(r["hi_non_refundable_credits"], line_35, atol=tol) + np.testing.assert_allclose( + r["hi_income_tax"], line_34 - line_35 - act_115, atol=tol + ) + applies = potential > 0 + assert (applies & (line_34 <= 0)).any() + assert (applies & (line_34 > 0) & (line_35 < potential - tol)).any() + assert (applies & (line_35 >= potential - tol)).any() diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py index 4c0e43f2d4b..fc871601d8d 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py @@ -14,8 +14,10 @@ class hi_eitc(Variable): reference = ( "https://www.capitol.hawaii.gov/hrscurrent/Vol04_Ch0201-0257/HRS0235/HRS_0235-0055_0007_0005.htm", # Form N-356, lines 10 to 13: the nonrefundable credit is limited to - # the adjusted tax liability on Form N-11, line 34. - "https://files.hawaii.gov/tax/forms/2018/n356_i.pdf#page=1", + # the adjusted tax liability, "the amount from Form N-11, line 34" + # (2018-2019 wording; 2020-2022 forms name the line "Adjusted Tax + # Liability"). The 2018 form is no longer on files.hawaii.gov. + "https://web.archive.org/web/20190819110547/https://files.hawaii.gov/tax/forms/2018/n356_i.pdf#page=1", "https://files.hawaii.gov/tax/forms/2022/n356_i.pdf#page=1", ) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/hi_income_tax_before_refundable_credits.py b/policyengine_us/variables/gov/states/hi/tax/income/hi_income_tax_before_refundable_credits.py index 3d8675db524..5431a1d24f3 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/hi_income_tax_before_refundable_credits.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/hi_income_tax_before_refundable_credits.py @@ -13,6 +13,7 @@ def formula(tax_unit, period, parameters): income_tax = tax_unit("hi_income_tax_before_non_refundable_credits", period) # Nonrefundable credits are already limited to the tax left after # refundable credits (Form N-11, line 34), so subtracting refundable - # credits from this amount gives the balance on line 36. + # credits from this amount gives the balance on line 36 (less the + # 2021 Act 115 refund, which was paid apart from the return). non_refundable_credits = tax_unit("hi_non_refundable_credits", period) return max_(income_tax - non_refundable_credits, 0) diff --git a/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py b/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py index 6ff82ed82d4..21b4e3d9521 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/hi_non_refundable_credits.py @@ -17,8 +17,10 @@ class hi_non_refundable_credits(Variable): "https://www.capitol.hawaii.gov/hrscurrent/Vol04_Ch0201-0257/HRS0235/HRS_0235-0091_0005.htm", # Form N-11 applies refundable credits first (line 33) and limits # nonrefundable credits (line 35) to the adjusted tax on line 34. - "https://files.hawaii.gov/tax/forms/2025/n11_i.pdf#page=3", - "https://files.hawaii.gov/tax/forms/2025/n11ins.pdf#page=21", + "https://files.hawaii.gov/tax/forms/2018/n11_i.pdf#page=3", + "https://files.hawaii.gov/tax/forms/2018/n11ins.pdf#page=25", + "https://files.hawaii.gov/tax/forms/2022/n11_i.pdf#page=3", + "https://files.hawaii.gov/tax/forms/2022/n11ins.pdf#page=21", ) def formula(tax_unit, period, parameters): From 517208806e0bfc03fa2adb76655ffa14da399424 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 13 Oct 2026 14:32:59 -0400 Subject: [PATCH 6/6] Cite the archived 2019 Form N-356, whose capture loads reliably The 2019 Wayback capture of the 2018 N-356 now redirects to a 2023 404 capture, so cite the 2019 form (same line 10 wording) and drop the 2018 entry from the dead-URL registry. Co-Authored-By: Claude Opus 5.5 --- .../tests/code_health/known_dead_reference_urls/west.toml | 5 ----- policyengine_us/tests/test_hi_credit_ordering.py | 5 +++-- .../variables/gov/states/hi/tax/income/credits/hi_eitc.py | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/policyengine_us/tests/code_health/known_dead_reference_urls/west.toml b/policyengine_us/tests/code_health/known_dead_reference_urls/west.toml index 1c9e487de81..dc1f3852ea6 100644 --- a/policyengine_us/tests/code_health/known_dead_reference_urls/west.toml +++ b/policyengine_us/tests/code_health/known_dead_reference_urls/west.toml @@ -178,11 +178,6 @@ prefix = "https://www.srca.nm.gov/parts/title08/08.401.0002.html" verified = "2026-10-01" replacement = "https://prod-rf-lambda.rtssaas.com/PublicFiles/d89c47bd0d70402dba89b03a22bda6d1/45b8668e-460f-45cd-804f-92ee3d442343/8.401.2.html" -[[dead]] -prefix = "https://files.hawaii.gov/tax/forms/2018/n356_i.pdf" -verified = "2026-10-13" -replacement = "https://web.archive.org/web/20190819110547/https://files.hawaii.gov/tax/forms/2018/n356_i.pdf" - [[dead]] prefix = "https://files.hawaii.gov/tax/forms/2019/n356_i.pdf" verified = "2026-10-13" diff --git a/policyengine_us/tests/test_hi_credit_ordering.py b/policyengine_us/tests/test_hi_credit_ordering.py index 6054369692a..a80ecab9b47 100644 --- a/policyengine_us/tests/test_hi_credit_ordering.py +++ b/policyengine_us/tests/test_hi_credit_ordering.py @@ -305,8 +305,9 @@ def test_computed_credits_follow_form_n11(households, year): def test_credit_lists_do_not_overlap(): - # A credit in both lists would make hi_non_refundable_credits depend on - # itself through line 34 (hi_adjusted_tax_liability). + # A credit in both lists would be counted twice or create a cycle: the + # nonrefundable Hawaii EITC reads line 34 (hi_adjusted_tax_liability), + # which reads the refundable credits. from policyengine_us.system import system for year in range(2015, 2036): diff --git a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py index fc871601d8d..1ffe9a8b9e5 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/credits/hi_eitc.py @@ -16,8 +16,8 @@ class hi_eitc(Variable): # Form N-356, lines 10 to 13: the nonrefundable credit is limited to # the adjusted tax liability, "the amount from Form N-11, line 34" # (2018-2019 wording; 2020-2022 forms name the line "Adjusted Tax - # Liability"). The 2018 form is no longer on files.hawaii.gov. - "https://web.archive.org/web/20190819110547/https://files.hawaii.gov/tax/forms/2018/n356_i.pdf#page=1", + # Liability"). files.hawaii.gov no longer serves the 2018-2021 forms. + "https://web.archive.org/web/20201129163620/https://files.hawaii.gov/tax/forms/2019/n356_i.pdf#page=1", "https://files.hawaii.gov/tax/forms/2022/n356_i.pdf#page=1", )