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/tests/code_health/known_dead_reference_urls/west.toml b/policyengine_us/tests/code_health/known_dead_reference_urls/west.toml index cdb9aecf815..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 @@ -177,3 +177,18 @@ 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/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_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_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 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 new file mode 100644 index 00000000000..a80ecab9b47 --- /dev/null +++ b/policyengine_us/tests/test_hi_credit_ordering.py @@ -0,0 +1,350 @@ +"""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; 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"]}, + } + 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. 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, + 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) + + +def test_credit_lists_do_not_overlap(): + # 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): + 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 44a13b437cc..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 @@ -11,17 +11,28 @@ 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, "the amount from Form N-11, line 34" + # (2018-2019 wording; 2020-2022 forms name the line "Adjusted Tax + # 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", + ) 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..280df2f8e7b --- /dev/null +++ b/policyengine_us/variables/gov/states/hi/tax/income/hi_adjusted_tax_liability.py @@ -0,0 +1,38 @@ +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 = ( + # 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", + "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..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 @@ -11,5 +11,9 @@ 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 (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 f9c03325e88..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 @@ -11,6 +11,17 @@ class hi_non_refundable_credits(Variable): unit = USD 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/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): ordered_credits = parameters( @@ -20,5 +31,5 @@ def formula(tax_unit, period, parameters): tax_unit, period, ordered_credits, - "hi_income_tax_before_non_refundable_credits", + "hi_adjusted_tax_liability", )