From edd51ad1ae97d49affd2973a0012a98da14ebd50 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 17:50:05 -0400 Subject: [PATCH 01/13] test: add Arkansas, Kentucky, and West Virginia statute witnesses --- changelog.d/statute-bugs-ar-ky-wv.fixed.md | 1 + ..._wv_senior_citizen_disability_deduction.py | 37 ++- policyengine_us/tests/fixtures/ar_indexing.py | 54 ++++ ...x_before_non_refundable_credits_indiv.yaml | 37 +++ .../dependent_care_credit/ky_cdcc.yaml | 245 ++++++++++++++++++ ...r_citizen_disability_deduction_person.yaml | 88 ++++++- 6 files changed, 430 insertions(+), 32 deletions(-) create mode 100644 changelog.d/statute-bugs-ar-ky-wv.fixed.md create mode 100644 policyengine_us/tests/fixtures/ar_indexing.py diff --git a/changelog.d/statute-bugs-ar-ky-wv.fixed.md b/changelog.d/statute-bugs-ar-ky-wv.fixed.md new file mode 100644 index 00000000000..3c56954340b --- /dev/null +++ b/changelog.d/statute-bugs-ar-ky-wv.fixed.md @@ -0,0 +1 @@ +Fix Arkansas income-tax indexing projections, Kentucky's 2021 dependent care credit conformity, and West Virginia's treatment of 529 contributions in senior and disability modifications. diff --git a/policyengine_us/tests/core/test_wv_senior_citizen_disability_deduction.py b/policyengine_us/tests/core/test_wv_senior_citizen_disability_deduction.py index 359eed4a6d0..3a13b5d2841 100644 --- a/policyengine_us/tests/core/test_wv_senior_citizen_disability_deduction.py +++ b/policyengine_us/tests/core/test_wv_senior_citizen_disability_deduction.py @@ -1,16 +1,17 @@ """Hypothesis coverage of Schedule M's person-level subtraction limits. The 2025 IT-140 Schedule M line 47, boxes (c) and (d), first caps the income -remaining after lines 35-46 at $8,000, then subtracts the person's own -lines 29-34 modifications. These properties exercise combinations that the -worked YAML examples do not enumerate, using the real model and independent +remaining after the modified income receipts on lines 35-46 at $8,000, then +subtracts the person's own lines 29-34 modifications. These properties +exercise combinations that the worked YAML examples do not enumerate, +using the real model and independent conservation and spouse-isolation checks. -The form interpretation includes line 39's deductible 529 contribution in -the excluded Schedule M amounts: it removes income already deducted on that -line from the remainder in box (c), although a contribution is not an income -receipt. Compatibility with the old formula therefore means no excluded -Schedule M amounts, including no deductible 529 contribution. +Only income receipts enter the excluded Schedule M amounts. Section +11-21-12a independently authorizes the line 39 contribution deduction in +addition to the section 11-21-12(c) modifications. Contributions are outside the income-conservation property below; their +independence from the senior modification is covered by the variable YAML +tests. Fresh two- or four-person simulations reuse the package's reference model; they do not share simulation caches, mutate parameters, or load datasets. @@ -33,7 +34,6 @@ EXCLUDED_SOURCES = ( "military_service_income", "salt_refund_income", - "investment_in_529_plan_indv", ) AGIS = st.integers(min_value=-20_000, max_value=60_000) @@ -44,7 +44,7 @@ COUPLE = st.tuples(PERSON, PERSON) PROPERTY_SETTINGS = settings(max_examples=50, deadline=None, derandomize=True) ZERO_MODIFICATIONS = (0, 0, 0, 0) -ZERO_EXCLUSIONS = (0, 0, 0) +ZERO_EXCLUSIONS = (0, 0) LOW_INCOME_COUPLE = ( (6_000, ZERO_MODIFICATIONS, ZERO_EXCLUSIONS), (6_000, ZERO_MODIFICATIONS, ZERO_EXCLUSIONS), @@ -118,19 +118,13 @@ def test_without_excluded_amounts_matches_previous_formula(agis, modifications): @given(couple=COUPLE) @example( couple=( - (6_000, ZERO_MODIFICATIONS, (0, 2_000, 0)), + (6_000, ZERO_MODIFICATIONS, (0, 2_000)), (6_000, ZERO_MODIFICATIONS, ZERO_EXCLUSIONS), ) ) @example( couple=( - (6_000, ZERO_MODIFICATIONS, (2_000, 0, 0)), - (6_000, ZERO_MODIFICATIONS, ZERO_EXCLUSIONS), - ) -) -@example( - couple=( - (6_000, ZERO_MODIFICATIONS, (0, 0, 2_000)), + (6_000, ZERO_MODIFICATIONS, (2_000, 0)), (6_000, ZERO_MODIFICATIONS, ZERO_EXCLUSIONS), ) ) @@ -138,8 +132,9 @@ def test_senior_and_other_subtractions_do_not_exceed_income_or_other_subtraction couple, ): deduction = _simulation(couple).calculate(DEDUCTION, YEAR) - # Sum each person's independently supplied lines 29-46 amounts, without - # using the deduction formula or its intermediate calculated totals. + # Sum each person's independently supplied modified income receipts, + # without using the deduction formula or its intermediate totals. + # A 529 contribution is a payment, so it does not enter this bound. subtractions = np.array( [ sum(modifications) + sum(exclusions) @@ -161,8 +156,6 @@ def test_senior_and_other_subtractions_do_not_exceed_income_or_other_subtraction @example(couple=LOW_INCOME_COUPLE, changed_person=1, source_index=0, increase=2_000) @example(couple=LOW_INCOME_COUPLE, changed_person=0, source_index=1, increase=2_000) @example(couple=LOW_INCOME_COUPLE, changed_person=1, source_index=1, increase=2_000) -@example(couple=LOW_INCOME_COUPLE, changed_person=0, source_index=2, increase=2_000) -@example(couple=LOW_INCOME_COUPLE, changed_person=1, source_index=2, increase=2_000) def test_changing_one_spouses_excluded_amount_preserves_the_others_modification( couple, changed_person, source_index, increase ): diff --git a/policyengine_us/tests/fixtures/ar_indexing.py b/policyengine_us/tests/fixtures/ar_indexing.py new file mode 100644 index 00000000000..87a7f6be8a1 --- /dev/null +++ b/policyengine_us/tests/fixtures/ar_indexing.py @@ -0,0 +1,54 @@ +"""Complete synthetic CPI windows for Arkansas indexing mechanism tests.""" + +from policyengine_core.model_api import Reform +from policyengine_core.parameters import Parameter +from policyengine_core.parameters.operations.uprate_parameters import uprate_parameters +from policyengine_core.periods import instant + +from policyengine_us.parameters.uprating_extensions import set_all_uprating_parameters + + +def install_monthly_window(parameter, first_month, value): + """Replace all twelve observations, including any missing baseline month.""" + start = instant(first_month) + for month in range(12): + parameter.update(period=f"month:{start.offset(month, 'month')}:1", value=value) + + +def _synthetic_indexing(parameters, cpi_u_factor, chained_factor): + cpi = parameters.gov.bls.cpi + install_monthly_window(cpi.cpi_u, "2024-09-01", 100) + install_monthly_window(cpi.cpi_u, "2025-09-01", 100 * cpi_u_factor) + # Preserve the shared IRS index's existing August-July window. This + # makes its growth intentionally differ from Arkansas's CPI-U growth. + install_monthly_window(cpi.c_cpi_u, "2024-08-01", 100) + install_monthly_window(cpi.c_cpi_u, "2025-08-01", 100 * chained_factor) + + main = parameters.gov.states.ar.tax.income.rates.main + for parameter in main.get_descendants(): + if isinstance(parameter, Parameter) and parameter.metadata.get("uprating"): + parameter.update( + start=instant("2026-01-01"), + value=parameter("2026-01-01"), + remove_after=True, + ) + + set_all_uprating_parameters(parameters) + return uprate_parameters(parameters) + + +def _indexing_reform(cpi_u_factor, chained_factor): + class reform(Reform): + def apply(self): + self.modify_parameters( + lambda parameters: _synthetic_indexing( + parameters, cpi_u_factor, chained_factor + ) + ) + + return reform + + +ar_cpi_four_percent_chained_one_percent = _indexing_reform(1.04, 1.01) +ar_cpi_one_percent_chained_four_percent = _indexing_reform(1.01, 1.04) +ar_cpi_minus_one_percent_chained_four_percent = _indexing_reform(0.99, 1.04) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml index b0791e64e0a..dc467c8ae23 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml @@ -797,3 +797,40 @@ output: # "For $50,000 and over" (p.6): 50,000 x 7% - 949.24 = 2,550.76 ar_income_tax_before_non_refundable_credits_indiv: 2_550.76 + +# A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2, PDF page 3: +# https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=3 +# These complete synthetic windows test the indexing rule, not an actual +# 2027 coefficient: the October 2025 CPI observation was not published. +- name: Arkansas caps synthetic four percent CPI-U growth at three percent + period: 2027 + absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent + input: + state_code: AR + ar_taxable_income_indiv: 150_000 + output: + # $4,700 * 1.03 rounds to $4,800; $96 + $145,200 * 3.7%. + ar_income_tax_before_non_refundable_credits_indiv: 5_468.40 + +- name: Arkansas uses synthetic CPI-U growth rather than chained growth + period: 2027 + absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_one_percent_chained_four_percent + input: + state_code: AR + ar_taxable_income_indiv: 150_000 + output: + # $4,700 * 1.01 rounds to $4,700; $94 + $145,300 * 3.7%. + ar_income_tax_before_non_refundable_credits_indiv: 5_470.10 + +- name: Arkansas does not lower boundaries after a synthetic CPI-U decrease + period: 2027 + absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_minus_one_percent_chained_four_percent + input: + state_code: AR + ar_taxable_income_indiv: 150_000 + output: + # Negative CPI-U growth leaves the $4,700 boundary unchanged. + ar_income_tax_before_non_refundable_credits_indiv: 5_470.10 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/credits/dependent_care_credit/ky_cdcc.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/credits/dependent_care_credit/ky_cdcc.yaml index 0c2c44f6451..38b61cf979b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/credits/dependent_care_credit/ky_cdcc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/credits/dependent_care_credit/ky_cdcc.yaml @@ -247,3 +247,248 @@ output: cdcc: 200 ky_cdcc_potential: 40 + +# Kentucky's December 31, 2018 IRC reference excludes ARPA's 2021 expansion. +# 2021 Ky. Acts ch. 31, section 1 (KRS 141.010(21)(b)), PDF page 3: +# https://apps.legislature.ky.gov/law/acts/21RS/documents/0031.pdf#page=3 +# Form 2441-K pages 1-3 confirms the conformed cap, rate, benefits, and +# federal liability limit before Kentucky's 20% match: +# https://revenue.ky.gov/Forms/2441-K-2021.pdf#page=1 + +- name: 2021 Kentucky retains the pre-ARPA expense cap and rate + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 50_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 2 + cdcc_qualifying_childcare_expenses: 10_000 + care_expenses: 0 + min_head_spouse_earned: 50_000 + dependent_care_employer_benefits: 0 + cdcc_credit_limit: 5_000 + output: + # 20% match x 20% rate x $6,000 conformed expense cap. + ky_cdcc_potential: 240 + # The federal ARPA calculation continues to use 50% of $10,000. + cdcc: 5_000 + +- name: 2021 Kentucky does not import federal CDCC refundability + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 10_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 3_000 + care_expenses: 0 + min_head_spouse_earned: 10_000 + dependent_care_employer_benefits: 0 + cdcc_credit_limit: 0 + output: + # Kentucky matches min(35% x $3,000, $0), while federal ARPA is refundable. + ky_cdcc_potential: 0 + cdcc: 1_500 + +- name: 2021 Kentucky retains credit after ARPA's high-income phaseout + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 450_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 3_000 + care_expenses: 0 + min_head_spouse_earned: 450_000 + dependent_care_employer_benefits: 0 + cdcc_credit_limit: 100_000 + output: + # The conformed 20% rate has no second phaseout: 20% x 20% x $3,000. + ky_cdcc_potential: 120 + cdcc: 0 + +- name: 2021 Kentucky applies the federal liability limit before its match + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 50_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 2 + cdcc_qualifying_childcare_expenses: 6_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 500 + output: + # 20% x min(20% x $6,000, $500). + ky_cdcc_potential: 100 + +- name: 2021 Kentucky retains the pre-ARPA dependent-care benefit exclusion cap + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: JOINT + adjusted_gross_income: 50_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 2 + cdcc_qualifying_childcare_expenses: 10_000 + min_head_spouse_earned: 25_000 + dependent_care_employer_benefits: 6_000 + cdcc_credit_limit: 5_000 + output: + # Exclusion=min($6,000,$5,000)=$5,000; remaining cap=$1,000. + # 20% match x 20% rate x min($10,000-$5,000,$6,000-$5,000). + ky_cdcc_potential: 40 + +- name: 2021 Kentucky excludes reimbursed expenses below the remaining cap + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 50_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 2 + cdcc_qualifying_childcare_expenses: 3_000 + min_head_spouse_earned: 50_000 + dependent_care_employer_benefits: 1_000 + cdcc_credit_limit: 5_000 + output: + # Expenses=$3,000-$1,000=$2,000, below the $5,000 remaining cap. + ky_cdcc_potential: 80 + +- name: 2021 Kentucky limits the benefit exclusion to earned income + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: JOINT + adjusted_gross_income: 10_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 3_000 + min_head_spouse_earned: 2_000 + dependent_care_employer_benefits: 5_000 + cdcc_credit_limit: 5_000 + output: + # Exclusion=min($5,000,$5,000,$2,000,$3,000)=$2,000. + # 20% match x 35% rate x min($3,000-$2,000,$3,000-$2,000,$2,000). + ky_cdcc_potential: 70 + +- name: 2021 Kentucky caps expenses at the lesser of the spouses' earnings + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: JOINT + adjusted_gross_income: 50_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 2 + cdcc_qualifying_childcare_expenses: 6_000 + min_head_spouse_earned: 1_000 + cdcc_credit_limit: 5_000 + output: + ky_cdcc_potential: 40 + +- name: 2021 Kentucky excludes separate filers not treated as unmarried + period: 2021 + input: + state_code: KY + filing_status: SEPARATE + cdcc_treated_as_unmarried: false + adjusted_gross_income: 50_000 + capped_count_cdcc_eligible: 2 + cdcc_qualifying_childcare_expenses: 6_000 + min_head_spouse_earned: 50_000 + dependent_care_employer_benefits: 1_000 + cdcc_credit_limit: 5_000 + output: + ky_cdcc_potential: 0 + +- name: 2021 Kentucky uses the unmarried benefit cap for a qualifying separate filer + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SEPARATE + cdcc_treated_as_unmarried: true + adjusted_gross_income: 50_000 + capped_count_cdcc_eligible: 2 + cdcc_qualifying_childcare_expenses: 8_000 + min_head_spouse_earned: 50_000 + dependent_care_employer_benefits: 4_000 + cdcc_credit_limit: 5_000 + output: + # The full $4,000 is excluded, leaving a $2,000 creditable expense cap. + ky_cdcc_potential: 80 + +- name: 2021 Kentucky includes care expenses for a qualifying adult + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 50_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 0 + care_expenses: 3_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 5_000 + output: + ky_cdcc_potential: 120 + +- name: 2021 Kentucky applies each started two-thousand-dollar rate increment + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: [15_000, 15_001, 17_000, 17_001, 43_000, 43_001] + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 3_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 5_000 + output: + # 20% match x $3,000 x [35%,34%,34%,33%,21%,20%]. + ky_cdcc_potential: [210, 204, 204, 198, 126, 120] + +- name: 2020 Kentucky preserves the federal credit computation + period: 2020 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 50_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 2 + cdcc_qualifying_childcare_expenses: 10_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 5_000 + output: + cdcc: 1_200 + ky_cdcc_potential: 240 + +- name: 2022 Kentucky preserves the federal credit computation + period: 2022 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 50_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 2 + cdcc_qualifying_childcare_expenses: 10_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 5_000 + output: + cdcc: 1_200 + ky_cdcc_potential: 240 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.yaml index 0db631f2408..4a5445d7194 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.yaml @@ -142,7 +142,7 @@ wv_senior_citizen_disability_deduction: 10_000 wv_subtractions: 12_000 -- name: A line 39 contribution reduces only the contributor's remaining income. +- name: A 529 contribution preserves both spouses' senior modifications. period: 2025 input: people: @@ -164,11 +164,11 @@ members: [head, spouse] state_code: WV output: - # The contribution deducts 2,000 on a line within the excluded range. - # Head: min(6,000 - 2,000, 8,000); spouse: min(6,000, 8,000). + # Section 11-21-12a allows the contribution in addition to both + # senior modifications: 6,000 + 6,000 + 2,000 = 14,000. wv_529_plan_deduction: 2_000 - wv_senior_citizen_disability_deduction_person: [4_000, 6_000] - wv_subtractions: 12_000 + wv_senior_citizen_disability_deduction_person: [6_000, 6_000] + wv_subtractions: 14_000 - name: Income still above the cap after exclusions leaves the modification unchanged. period: 2025 @@ -181,7 +181,7 @@ wv_senior_citizen_disability_deduction_total_modifications: 3_000 state_code: WV output: - # min(20,000 - 4,000, 8,000) - 3,000 = 5,000, as before. + # min(20,000 - 3,000, 8,000) - 3,000 = 5,000, as before. wv_senior_citizen_disability_deduction_person: 5_000 - name: Box d is subtracted after excluding refunds and applying the cap. @@ -231,7 +231,7 @@ output: wv_senior_citizen_disability_deduction_person: 0 -- name: A qualifying disabled person also excludes a line 39 contribution. +- name: A qualifying disabled person retains the additional 529 deduction. period: 2025 input: age: 50 @@ -240,10 +240,10 @@ investment_in_529_plan_indv: 1_000 state_code: WV output: - # min(6,000 - 1,000, 8,000) - 0 = 5,000. - wv_senior_citizen_disability_deduction_person: 5_000 + # min(6,000, 8,000) - 0 = 6,000, plus the separate contribution. + wv_senior_citizen_disability_deduction_person: 6_000 wv_529_plan_deduction: 1_000 - wv_subtractions: 6_000 + wv_subtractions: 7_000 - name: Each spouse's $8,000 limit uses their own AGI, after their own IRA deduction. period: 2024 @@ -268,3 +268,71 @@ # W. Va. Code 11-21-12(c): up to $8,000 of each eligible individual's # federal AGI. Before: [5_000, 8_000]. wv_senior_citizen_disability_deduction_person: [4_000, 8_000] + +# W. Va. Code sections 11-21-12(c)(9) and 11-21-12a: contribution payments +# are deductible separately, rather than income excluded from box (c). +- name: A senior's contribution does not reduce the senior modification. + period: 2025 + input: + state_code: WV + age: 66 + is_tax_unit_head: true + adjusted_gross_income_person: 6_000 + investment_in_529_plan_indv: 2_000 + wv_senior_citizen_disability_deduction_total_modifications: 0 + output: + wv_senior_citizen_disability_deduction_person: 6_000 + wv_529_plan_deduction: 2_000 + +- name: The 529 contribution is independent from the senior income cap from 2014. + period: 2014 + input: + wv_senior_citizen_disability_deduction_eligible_person: true + adjusted_gross_income_person: 6_000 + investment_in_529_plan_indv: 2_000 + wv_senior_citizen_disability_deduction_total_modifications: 0 + output: + wv_senior_citizen_disability_deduction_person: 6_000 + +- name: Refund exclusions and modifications still apply independently of contributions. + period: 2025 + input: + state_code: WV + age: 66 + adjusted_gross_income_person: 6_000 + investment_in_529_plan_indv: 2_000 + salt_refund_income: 1_000 + wv_senior_citizen_disability_deduction_total_modifications: 500 + output: + # Preserve Schedule M base: min(6,000 - 1,000, 8,000) - 500. + wv_senior_citizen_disability_deduction_person: 4_500 + +- name: A senior's additional 529 deduction remains available on a joint return. + period: 2025 + input: + people: + head: + age: 66 + adjusted_gross_income_person: 6_000 + investment_in_529_plan_indv: 2_000 + wv_senior_citizen_disability_deduction_total_modifications: 0 + spouse: + age: 60 + adjusted_gross_income_person: 40_000 + tax_units: + unit: + members: [head, spouse] + filing_status: JOINT + adjusted_gross_income: 46_000 + wv_additions: 0 + wv_low_income_earned_income_exclusion: 0 + households: + household: + members: [head, spouse] + state_code: WV + output: + wv_senior_citizen_disability_deduction_person: [6_000, 0] + wv_529_plan_deduction: 2_000 + wv_subtractions: 8_000 + wv_agi: 38_000 + wv_taxable_income: 34_000 From c37ff78a7f06a6f785984f8235b5502ca2deb53d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 18:21:57 -0400 Subject: [PATCH 02/13] Fix West Virginia 529 coordination with senior modifications --- .../excluded_income_sources.yaml | 12 ++++++++---- .../wv_senior_citizen_disability_deduction_person.py | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/policyengine_us/parameters/gov/states/wv/tax/income/subtractions/senior_citizen_disability_deduction/excluded_income_sources.yaml b/policyengine_us/parameters/gov/states/wv/tax/income/subtractions/senior_citizen_disability_deduction/excluded_income_sources.yaml index 63c9c52dc00..9d50f180dc4 100644 --- a/policyengine_us/parameters/gov/states/wv/tax/income/subtractions/senior_citizen_disability_deduction/excluded_income_sources.yaml +++ b/policyengine_us/parameters/gov/states/wv/tax/income/subtractions/senior_citizen_disability_deduction/excluded_income_sources.yaml @@ -8,10 +8,8 @@ values: # Line numbers below are from the 2025 form. - military_service_income # Line 36 - salt_refund_income # Line 38 - # Line 39 is a contribution deduction, not an income receipt. Its amount - # removes income within the form's excluded range. Use the contributor's - # own amount rather than projecting the tax-unit 529 deduction to spouses. - - investment_in_529_plan_indv # Line 39 + # Section 11-21-12a separately deducts 529 contribution payments; + # those payments are not income excluded from the senior income cap. metadata: unit: list @@ -26,3 +24,9 @@ metadata: href: https://tax.wv.gov/Documents/TaxForms/2018/it140.booklet.pdf#page=5 - title: West Virginia Personal Income Tax Forms and Instructions 2014, Schedule M line 52 href: https://tax.wv.gov/Documents/TaxForms/2014/it140.booklet.pdf#page=9 + - title: W. Va. Code section 11-21-12(c)(9) + href: https://code.wvlegislature.gov/11-21-12/ + - title: W. Va. Code section 11-21-12a, additional college-savings modification + href: https://code.wvlegislature.gov/11-21-12a/ + - title: West Virginia 2000 HB4679, section 11-21-12a + href: https://code.wvlegislature.gov/signed_bills/2000/2000-RS-HB4679-SUB%20ENR_signed.pdf#page=3 diff --git a/policyengine_us/variables/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.py b/policyengine_us/variables/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.py index a13eabbf8cb..5a55696ff95 100644 --- a/policyengine_us/variables/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.py +++ b/policyengine_us/variables/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.py @@ -9,6 +9,7 @@ class wv_senior_citizen_disability_deduction_person(Variable): definition_period = YEAR reference = ( "https://code.wvlegislature.gov/11-21-12/", + "https://code.wvlegislature.gov/11-21-12a/", "https://tax.wv.gov/Documents/PIT/2025/it140.PersonalIncomeTaxFormsAndInstructions.2025.pdf#page=6", "https://tax.wv.gov/Documents/PIT/2024/it140.PersonalIncomeTaxFormsAndInstructions.2024.pdf#page=6", "https://tax.wv.gov/Documents/TaxForms/2018/it140.booklet.pdf#page=5", @@ -21,8 +22,9 @@ def formula(person, period, parameters): ).gov.states.wv.tax.income.subtractions.senior_citizen_disability_deduction agi_person = person("adjusted_gross_income_person", period) - # Box (c) excludes the person's own amounts on lines 35-46 before - # applying the cap, including income already deducted on line 39. + # Box (c) excludes the person's own modified income receipts on + # lines 35-46 before applying the cap. The separate section 12a + # deduction for 529 contribution payments does not exclude income. excluded_amounts = add(person, period, p.excluded_income_sources) capped_income = min_(p.cap, agi_person - excluded_amounts) # Box (d) then subtracts the person's own lines 29-34 modifications. From 56cf8e8bd23c08129e99cda0dc3b6fe3415fc530 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 18:26:23 -0400 Subject: [PATCH 03/13] Compute Kentucky 2021 dependent care credit under static conformity --- .../ky_cdcc_potential.py | 76 ++++++++++++++++++- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/policyengine_us/variables/gov/states/ky/tax/income/credits/dependent_care_service/ky_cdcc_potential.py b/policyengine_us/variables/gov/states/ky/tax/income/credits/dependent_care_service/ky_cdcc_potential.py index 73f62508423..c708cea4a3e 100644 --- a/policyengine_us/variables/gov/states/ky/tax/income/credits/dependent_care_service/ky_cdcc_potential.py +++ b/policyengine_us/variables/gov/states/ky/tax/income/credits/dependent_care_service/ky_cdcc_potential.py @@ -5,11 +5,28 @@ class ky_cdcc_potential(Variable): value_type = float entity = TaxUnit label = "Kentucky household and dependent care service credit" + documentation = ( + "Kentucky matches the federal credit allowed under its conformed " + "Internal Revenue Code. For 2021, the December 31, 2018 reference " + "excludes ARPA's expanded expense limits, rates, benefit exclusion " + "and refundability. The credit is recomputed under that Code before " + "applying Kentucky's match." + ) unit = USD definition_period = YEAR reference = ( "https://apps.legislature.ky.gov/law/statutes/statute.aspx?id=29058", "https://apps.legislature.ky.gov/law/statutes/statute.aspx?id=57913#page=3", + # 2021 KRS 141.010(21)(b): December 31, 2018 conformity. + "https://apps.legislature.ky.gov/law/acts/21RS/documents/0031.pdf#page=3", + # The 2022 amendment advances conformity for taxable years from 2022. + "https://apps.legislature.ky.gov/law/acts/22RS/documents/0212.pdf#page=62", + # PDF pages 1-3: IRC 21(a)-(d) cap, rate, earnings limits, and + # amendment history confirming these provisions were unchanged in 2018. + "https://www.govinfo.gov/content/pkg/USCODE-2020-title26/pdf/USCODE-2020-title26-subtitleA-chap1-subchapA-partIV-subpartA-sec21.pdf#page=1", + # PDF pages 1-3: conformed credit computation, section 129 benefit + # reconciliation, and federal liability limit; instructions exclude ARPA. + "https://revenue.ky.gov/Forms/2441-K-2021.pdf#page=1", ) defined_for = StateCode.KY @@ -21,12 +38,63 @@ def formula(tax_unit, period, parameters): # date. Where the reference date predates the One Big Beautiful Bill # Act, Kentucky's section 21 excludes the Act's rate increase. From # 2026 the reference date is December 31, 2025, which includes it. - # The 2019-2021 reference date (December 31, 2018) also excludes the - # American Rescue Plan Act's 2021 section 21 expansion (2021 Form - # 2441-K); the model does not apply that yet, so 2021 reads the live - # federal credit. if p.irc_conformity.obbba: federal_credit = tax_unit("cdcc", period) else: federal_credit = tax_unit("pre_obbba_cdcc", period) return federal_credit * p.credits.dependent_care_service.match + + def formula_2021_01_01(tax_unit, period, parameters): + # The December 31, 2018 Code excludes both ARPA's section 21 expansion + # and section 129 exclusion increase. The 2020 parameters preserve + # these unchanged pre-ARPA provisions without changing federal 2021. + frozen = parameters("2020-01-01").gov.irs + p = frozen.credits.cdcc + childcare = tax_unit("cdcc_qualifying_childcare_expenses", period) + adult_care = add(tax_unit, period, ["care_expenses"]) + expenses = max_(childcare + adult_care, 0) + earnings = max_(tax_unit("min_head_spouse_earned", period), 0) + + # Form 2441-K Part III lines 18-29 limit excluded/deductible benefits + # to benefits received, qualifying expenses, earnings, and $5,000 + # ($2,500 for a married separate filer not treated as unmarried). + benefits = max_(add(tax_unit, period, ["dependent_care_employer_benefits"]), 0) + exclusion_caps = frozen.gross_income.dependent_care_assistance_programs + filing_status = tax_unit("filing_status", period) + treated_as_unmarried = tax_unit("cdcc_treated_as_unmarried", period) + exclusion_cap = where( + treated_as_unmarried, + exclusion_caps.reduction_amount["SINGLE"], + exclusion_caps.reduction_amount[filing_status], + ) + exclusion = min_(benefits, min_(expenses, min_(earnings, exclusion_cap))) + + # Lines 28-32 reduce both the conformed $3,000/$6,000 expense cap + # and qualified expenses by these excluded/deductible benefits. + qualifying_count = min_( + tax_unit("capped_count_cdcc_eligible", period), p.eligibility.max + ) + expense_limit = max_(p.max * qualifying_count - exclusion, 0) + creditable_expenses = min_( + expense_limit, min_(max_(expenses - exclusion, 0), earnings) + ) + + # Part II line 8: 35%, less one point for every started $2,000 over + # $15,000, floored at 20%. The conformed Code has no second phaseout. + agi = tax_unit("adjusted_gross_income", period) + increments = np.ceil(max_(agi - p.phase_out.start, 0) / p.phase_out.increment) + rate = max_(p.phase_out.min, p.phase_out.max - increments * p.phase_out.rate) + eligible = tax_unit("cdcc_filing_status_eligible", period) + potential = eligible * creditable_expenses * rate + # Part II lines 10-12: apply the federal liability limit first, then + # Kentucky's 20% match. ARPA's federal refundability does not apply. + federal_credit = min_(potential, tax_unit("cdcc_credit_limit", period)) + match = parameters( + period + ).gov.states.ky.tax.income.credits.dependent_care_service.match + return federal_credit * match + + def formula_2022_01_01(tax_unit, period, parameters): + # ARPA's temporary expansion has ended; retain the ordinary + # computation and subsequent conformity changes from 2022 onward. + return ky_cdcc_potential.formula(tax_unit, period, parameters) From 964ac71341c607fb28cce7ee45d760288ddccf8d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 18:31:38 -0400 Subject: [PATCH 04/13] Apply capped CPI-U indexing to Arkansas income tax boundaries --- .../main/high_income/bracket_adjustment.yaml | 62 ++++----- .../income/rates/main/high_income/rate.yaml | 4 +- .../rates/main/high_income/threshold.yaml | 4 +- .../states/ar/tax/income/rates/main/rate.yaml | 14 +- .../gov/states/ar/tax/income/uprating.yaml | 13 ++ .../states/ar/tax/income/uprating_cap.yaml | 10 ++ .../parameters/uprating_extensions.py | 43 ++++++ .../core/test_state_uprating_rounding.py | 14 +- .../tests/test_ar_income_tax_indexing.py | 122 ++++++++++++++++++ .../tests/test_ar_income_tax_schedule.py | 4 +- 10 files changed, 244 insertions(+), 46 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml create mode 100644 policyengine_us/parameters/gov/states/ar/tax/income/uprating_cap.yaml create mode 100644 policyengine_us/tests/test_ar_income_tax_indexing.py diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/bracket_adjustment.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/bracket_adjustment.yaml index 0a2268342f7..0ca022e7897 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/bracket_adjustment.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/bracket_adjustment.yaml @@ -5,6 +5,8 @@ metadata: amount_unit: currency-USD label: Arkansas high-income bracket adjustment reference: + - title: A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2 (annual CPI-U indexing capped at three percent) + href: https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=3 - title: A.C.A. § 26-51-201(a)(4)(C), as amended by Act 1 of 2026 (1st Extraordinary Session), § 1 href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=2 - title: A.C.A. § 26-51-201(d)(1), as amended by Act 532 of 2023, § 2 (bracket amounts indexed annually, rounded to the nearest $100) @@ -22,7 +24,7 @@ brackets: 2026-01-01: 94_700 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -33,7 +35,7 @@ brackets: 2026-01-01: 94_800 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -44,7 +46,7 @@ brackets: 2026-01-01: 94_900 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -55,7 +57,7 @@ brackets: 2026-01-01: 95_000 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -66,7 +68,7 @@ brackets: 2026-01-01: 95_100 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -77,7 +79,7 @@ brackets: 2026-01-01: 95_200 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -88,7 +90,7 @@ brackets: 2026-01-01: 95_300 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -99,7 +101,7 @@ brackets: 2026-01-01: 95_400 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -110,7 +112,7 @@ brackets: 2026-01-01: 95_500 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -121,7 +123,7 @@ brackets: 2026-01-01: 95_600 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -132,7 +134,7 @@ brackets: 2026-01-01: 95_700 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -143,7 +145,7 @@ brackets: 2026-01-01: 95_800 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -154,7 +156,7 @@ brackets: 2026-01-01: 95_900 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -165,7 +167,7 @@ brackets: 2026-01-01: 96_000 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -176,7 +178,7 @@ brackets: 2026-01-01: 96_100 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -187,7 +189,7 @@ brackets: 2026-01-01: 96_200 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -198,7 +200,7 @@ brackets: 2026-01-01: 96_300 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -209,7 +211,7 @@ brackets: 2026-01-01: 96_400 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -220,7 +222,7 @@ brackets: 2026-01-01: 96_500 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -231,7 +233,7 @@ brackets: 2026-01-01: 96_600 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -242,7 +244,7 @@ brackets: 2026-01-01: 96_700 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -253,7 +255,7 @@ brackets: 2026-01-01: 96_800 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -264,7 +266,7 @@ brackets: 2026-01-01: 96_900 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -275,7 +277,7 @@ brackets: 2026-01-01: 97_000 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -286,7 +288,7 @@ brackets: 2026-01-01: 97_100 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -297,7 +299,7 @@ brackets: 2026-01-01: 97_200 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -308,7 +310,7 @@ brackets: 2026-01-01: 97_300 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -319,7 +321,7 @@ brackets: 2026-01-01: 97_400 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -330,7 +332,7 @@ brackets: 2026-01-01: 97_500 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -341,7 +343,7 @@ brackets: 2026-01-01: 97_600 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml index 643edacf349..49201402b8e 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml @@ -5,6 +5,8 @@ metadata: rate_unit: /1 label: Arkansas high-income tax table reference: + - title: A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2 (annual CPI-U indexing capped at three percent) + href: https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=3 - title: A.C.A. § 26-51-201(a)(4)(B), as amended by Act 1 of 2026 (1st Extraordinary Session), § 1 href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=2 - title: A.C.A. § 26-51-201(d)(1), as amended by Act 532 of 2023, § 2 (bracket amounts indexed annually, rounded to the nearest $100) @@ -21,7 +23,7 @@ brackets: 2026-01-01: 4_700 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/threshold.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/threshold.yaml index 40ca4b1fd92..710c9d84261 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/threshold.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/threshold.yaml @@ -6,11 +6,13 @@ metadata: period: year label: Arkansas high-income tax table threshold uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 reference: + - title: A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2 (annual CPI-U indexing capped at three percent) + href: https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=3 - title: A.C.A. § 26-51-201(a)(4)(A)-(B), as amended by Act 1 of 2026 (1st Extraordinary Session), § 1 href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=1 - title: A.C.A. § 26-51-201(d)(1), as amended by Act 532 of 2023, § 2 (bracket amounts indexed annually, rounded to the nearest $100) diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml index 4e494618295..8c53dc2aad4 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml @@ -8,8 +8,8 @@ metadata: reference: - title: Tax Legislation for 2022 Ark. Code § 26-51-201 (a)(1)(A) # (d)(1) for uprating. href: https://law.justia.com/codes/arkansas/title-26/subtitle-5/chapter-51/subchapter-2/section-26-51-201/ - - title: Arkansas Act 1459 of 2013, § 1 (0.9% first bracket from tax year 2014) - href: https://www.arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013R%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=1 + - title: "Arkansas Act 1459 of 2013, §§ 1-2 (2014 first bracket and capped CPI-U indexing) #page 1,3" + href: https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=1 - title: Arkansas Act 22 of 2015, § 2, amending Ark. Code § 26-51-201(a) (tax year 2014-2018 tables and the bracket adjustment above $75,000) href: https://www.arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2015R%2FPublic%2F&file=22.pdf&ddBienniumSession=2015%2F2015R#page=1 - title: Arkansas Act 78 of 2017, § 2, amending Ark. Code § 26-51-201(a)(7)-(8) (tax year 2019) @@ -61,7 +61,7 @@ brackets: 2014-01-01: 0 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -83,7 +83,7 @@ brackets: 2026-01-01: 5_600 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -107,7 +107,7 @@ brackets: 2026-01-01: 11_200 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -131,7 +131,7 @@ brackets: 2026-01-01: 16_000 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 @@ -156,7 +156,7 @@ brackets: 2026-01-01: 26_400 metadata: uprating: - parameter: gov.irs.uprating + parameter: gov.states.ar.tax.income.uprating rounding: type: nearest interval: 100 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml new file mode 100644 index 00000000000..2b9be524de4 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml @@ -0,0 +1,13 @@ +description: Arkansas uses this index to adjust its income tax bracket boundaries for annual increases in the Consumer Price Index for All Urban Consumers, capped at three percent. +values: + # Neutral history supports zero and infinite bounds defined before 2026. + 2014-01-01: 1 + # The published 2026 statutory tables anchor subsequent projections. + 2026-01-01: 1 +metadata: + unit: /1 + label: Arkansas income tax uprating index + economy: false + reference: + - title: A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2 (CPI-U average for twelve months ending August 31, positive increase capped at three percent) + href: https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=3 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/uprating_cap.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/uprating_cap.yaml new file mode 100644 index 00000000000..c1d811a28c8 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ar/tax/income/uprating_cap.yaml @@ -0,0 +1,10 @@ +description: Arkansas limits the annual increase in its income tax bracket boundaries to this share. +values: + 2014-01-01: 0.03 +metadata: + unit: /1 + period: year + label: Arkansas income tax annual uprating cap + reference: + - title: A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2 + href: https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=3 diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index c1c598de334..2b1995b882a 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -82,6 +82,45 @@ def get_cpi_u_average_for_12_months_ending_august( return get_average_for_12_months_ending_august(parameters.gov.bls.cpi.cpi_u, year) +def get_ar_cpi(parameters: ParameterNode, calendar_year: int) -> float: + """Average unchained CPI-U for the twelve months ending August 31. + + A.C.A. 26-51-201(d)(2), Act 1459 of 2013, section 2, PDF page 3. + For calendar year T this includes September T-1 through August T. + """ + cpi = parameters.gov.bls.cpi.cpi_u + start = instant(f"{calendar_year - 1}-09-01") + return ( + sum(cpi(start.offset(month, MONTH)) for month in range(MONTHS_IN_YEAR)) + / MONTHS_IN_YEAR + ) + + +def extend_ar_income_tax_uprating(parameters: ParameterNode, end_year: int) -> None: + """Compound Arkansas's positive annual CPI-U increase, subject to its cap. + + The published 2026 tables anchor projections. For tax year Y, + 26-51-201(d)(2) compares September Y-2 through August Y-1 with + September Y-3 through August Y-2. The statute's increase-if-any + language prevents a decrease. Bracket rounding remains in the + individual parameters' metadata, and bracket-adjustment amounts + remain fixed. + """ + income = parameters.gov.states.ar.tax.income + index = income.uprating + for year in range(2027, end_year + 1): + current = get_ar_cpi(parameters, year - 1) + prior = get_ar_cpi(parameters, year - 2) + increase = min( + income.uprating_cap(f"{year}-01-01"), max(current / prior - 1, 0) + ) + index.update( + period=f"year:{year}-01-01:1", + value=index(f"{year - 1}-01-01") * (1 + increase), + ) + index.update(start=instant(f"{end_year}-01-01"), value=index(f"{end_year}-01-01")) + + def get_or_ctc_cola(parameters: ParameterNode, tax_year: int) -> float: """Calculate the Oregon Kids' Credit cost-of-living adjustment. @@ -1018,6 +1057,10 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: period_day=1, ) + # Arkansas uses capped positive CPI-U changes over September-August + # windows, independently of the shared IRS chained CPI index. + extend_ar_income_tax_uprating(parameters, end_year=END_YEAR) + # Chained CPI-U (February values, last projection year 2034) extend_parameter_values( parameters.gov.bls.cpi.c_cpi_u, diff --git a/policyengine_us/tests/core/test_state_uprating_rounding.py b/policyengine_us/tests/core/test_state_uprating_rounding.py index dd11c2cbfdf..187febc0627 100644 --- a/policyengine_us/tests/core/test_state_uprating_rounding.py +++ b/policyengine_us/tests/core/test_state_uprating_rounding.py @@ -5,6 +5,9 @@ import pytest from policyengine_us.system import system as SYSTEM +from policyengine_us.tests.fixtures.ar_indexing import ( + ar_cpi_four_percent_chained_one_percent, +) def _thresholds(scale, period, bracket_indexes=(1, 2, 3)): @@ -13,10 +16,11 @@ def _thresholds(scale, period, bracket_indexes=(1, 2, 3)): def test_ar_income_tax_bounds_round_to_nearest_100_after_last_published_year(): # A.C.A. 26-51-201(d)(1): bracket amounts are indexed "rounding to the - # nearest one hundred dollars ($100)". Expected values follow the loaded - # index, so a CPI refresh moves them without breaking the test. - main = SYSTEM.parameters.gov.states.ar.tax.income.rates.main - uprating = SYSTEM.parameters.gov.irs.uprating + # nearest one hundred dollars ($100)". Complete synthetic CPI windows + # avoid assuming a resolution for the missing October 2025 observation. + parameters = ar_cpi_four_percent_chained_one_percent(SYSTEM).parameters + main = parameters.gov.states.ar.tax.income.rates.main + uprating = parameters.gov.states.ar.tax.income.uprating high_income = main.high_income bounds_2026 = { "rate": (main.rate, (1, 2, 3, 4), (5_600, 11_200, 16_000, 26_400)), @@ -43,7 +47,7 @@ def test_ar_income_tax_bounds_round_to_nearest_100_after_last_published_year(): differs_from_rounding_down |= loaded != rounded_down assert isinf(main.rate.brackets[5].threshold(period)) assert isinf(main.rate.brackets[6].threshold(period)) - # With 2027's factor, 5,600 x 1.0298 = 5,766.9 rounds up to 5,800. + # The synthetic 3% increase takes $5,600 to $5,768, rounded to $5,800. assert differs_from_rounding_down diff --git a/policyengine_us/tests/test_ar_income_tax_indexing.py b/policyengine_us/tests/test_ar_income_tax_indexing.py new file mode 100644 index 00000000000..16300eb7428 --- /dev/null +++ b/policyengine_us/tests/test_ar_income_tax_indexing.py @@ -0,0 +1,122 @@ +"""Arkansas's statutory CPI-U window, cap, floor, and annual compounding.""" + +import pytest +from policyengine_core.parameters import ParameterNode +from policyengine_core.periods import instant + +from policyengine_us.parameters.uprating_extensions import ( + extend_ar_income_tax_uprating, + get_ar_cpi, +) +from policyengine_us.system import system +from policyengine_us.tests.fixtures.ar_indexing import ( + ar_cpi_four_percent_chained_one_percent, +) + + +def synthetic_parameters(annual_levels): + monthly = {} + for offset, level in enumerate(annual_levels): + start = instant(f"{2024 + offset}-09-01") + for month in range(12): + monthly[str(start.offset(month, "month"))] = level + return ParameterNode( + data={ + "gov": { + "bls": {"cpi": {"cpi_u": {"values": monthly}}}, + "states": { + "ar": { + "tax": { + "income": { + "uprating": {"values": {"2026-01-01": 1}}, + "uprating_cap": {"values": {"2014-01-01": 0.03}}, + } + } + } + }, + } + } + ) + + +@pytest.fixture(scope="module") +def capped_parameters(): + """Read-only reformed parameters shared by the Core integration checks.""" + return ar_cpi_four_percent_chained_one_percent(system).parameters + + +def test_ar_cpi_averages_september_through_august(): + parameters = synthetic_parameters((100, 100)) + cpi = parameters.gov.bls.cpi.cpi_u + # An IRS-style August-July window would include the first poison month + # and miss August 2026. The following September must also be excluded. + cpi.update(period="month:2024-08-01:1", value=200) + cpi.update(period="month:2026-08-01:1", value=124) + cpi.update(period="month:2026-09-01:1", value=500) + + assert get_ar_cpi(parameters, 2025) == pytest.approx(100) + assert get_ar_cpi(parameters, 2026) == pytest.approx(102) + extend_ar_income_tax_uprating(parameters, end_year=2027) + assert parameters.gov.states.ar.tax.income.uprating("2027-01-01") == pytest.approx( + 1.02 + ) + + +@pytest.mark.parametrize( + "current_level, expected_index", + ((99, 1), (100, 1), (101, 1.01), (103, 1.03), (104, 1.03)), +) +def test_ar_index_applies_the_increase_if_any_and_cap(current_level, expected_index): + parameters = synthetic_parameters((100, current_level)) + + extend_ar_income_tax_uprating(parameters, end_year=2027) + + assert parameters.gov.states.ar.tax.income.uprating("2027-01-01") == pytest.approx( + expected_index + ) + + +def test_ar_index_compounds_each_year_from_the_preceding_index(): + parameters = synthetic_parameters((100, 104, 106.08)) + + extend_ar_income_tax_uprating(parameters, end_year=2028) + + index = parameters.gov.states.ar.tax.income.uprating + assert index("2026-01-01") == 1 + assert index("2027-01-01") == pytest.approx(1.03) + # Four percent is capped in 2027, then two percent compounds in 2028. + assert index("2028-01-01") == pytest.approx(1.03 * 1.02) + assert index("2029-01-01") == pytest.approx(index("2028-01-01")) + + +def test_ar_index_and_irs_index_remain_independent(capped_parameters): + ar_index = capped_parameters.gov.states.ar.tax.income.uprating + irs_index = capped_parameters.gov.irs.uprating + + assert ar_index("2027-01-01") / ar_index("2026-01-01") == pytest.approx(1.03) + assert irs_index("2027-01-01") / irs_index("2026-01-01") == pytest.approx(1.01) + + +def test_ar_published_tables_and_fixed_bracket_adjustments_are_preserved( + capped_parameters, +): + original = system.parameters.gov.states.ar.tax.income.rates.main + reformed = capped_parameters.gov.states.ar.tax.income.rates.main + + for year in range(2014, 2027): + period = f"{year}-01-01" + assert [b.threshold(period) for b in reformed.rate.brackets] == [ + b.threshold(period) for b in original.rate.brackets + ] + assert [b.threshold("2026-01-01") for b in reformed.rate.brackets[1:5]] == [ + 5_600, + 11_200, + 16_000, + 26_400, + ] + assert reformed.high_income.threshold("2026-01-01") == 94_700 + assert reformed.high_income.threshold("2027-01-01") == 97_500 + assert reformed.high_income.rate.brackets[1].threshold("2027-01-01") == 4_800 + assert [ + b.amount("2027-01-01") for b in reformed.high_income.bracket_adjustment.brackets + ] == list(range(290, -1, -10)) diff --git a/policyengine_us/tests/test_ar_income_tax_schedule.py b/policyengine_us/tests/test_ar_income_tax_schedule.py index 3e3bd69e314..bb963e8816e 100644 --- a/policyengine_us/tests/test_ar_income_tax_schedule.py +++ b/policyengine_us/tests/test_ar_income_tax_schedule.py @@ -27,7 +27,7 @@ row's start is skipped. - 2026: Act 1 of 2026 (1st Extraordinary Session), A.C.A. 26-51-201(a)(4). - 2027-2035: Act 1 of 2026's tables with every bracket bound indexed from - 2026 by PolicyEngine's uprating index and rounded to the nearest $100. + 2026 by Arkansas's capped CPI-U index and rounded to the nearest $100. Invariants: - reduction.yaml rows carry strictly increasing finite thresholds, with every @@ -66,7 +66,7 @@ ) P = system.parameters.gov.states.ar.tax.income.rates.main -UPRATING = system.parameters.gov.irs.uprating +UPRATING = system.parameters.gov.states.ar.tax.income.uprating PUBLISHED_YEARS = range(2021, 2026) SOURCE_YEARS = range(2021, 2036) LONG_RUN_YEARS = [2050, 2075, 2100] From 09531917d5318c22c0446cae3fa53900e427bc17 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 18:41:16 -0400 Subject: [PATCH 05/13] Use complete synthetic CPI windows in Arkansas future-year tests --- ...x_before_non_refundable_credits_indiv.yaml | 40 ++++++++++++------- ...x_before_non_refundable_credits_joint.yaml | 7 +++- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml index dc467c8ae23..7a024de1465 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml @@ -541,23 +541,26 @@ # Midpoint 95,550.50 x 3.9% - 87.40 = 3,639.07 -> 3,639 ar_income_tax_before_non_refundable_credits_indiv: 3_639 -# 2027 cases use incomes whose tax survives a one-step change in the projected -# uprating factor. The (B)-(C) rows near the indexed threshold (about $97,500) -# sit within a few hundredths of a percent of a rounding flip, so -# test_ar_income_tax_schedule.py checks them against the loaded index instead. -- name: AR 2027 indexed Act 1 of 2026 tables - top of the 0% row +# These 2027 mechanism tests use complete synthetic CPI-U windows with 4% +# growth, capped at 3%, and independent chained growth of 1%. They do not +# assert an actual 2027 coefficient: October 2025 CPI was not published. +# The synthetic indexed boundaries are $5,800/$11,500/$16,500/$27,200, +# the high-income threshold is $97,500, and its 2% boundary is $4,800. +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - top of the 0% row period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_indiv: 5_799 state_code: AR output: - # the indexed 2% row starts at $5,800 (5,600 x 1.0298 = 5,767, nearest $100) + # The synthetic 2% row starts at $5,800 (5,600 x 1.03 = 5,768, nearest $100). ar_income_tax_before_non_refundable_credits_indiv: 0 -- name: AR 2027 indexed Act 1 of 2026 tables - 2% row +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - 2% row period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_indiv: 5_900 state_code: AR @@ -565,9 +568,10 @@ # Row $5,900-$5,999, midpoint 5,949.50: (5,949.50 - 5,800) x 2% = 2.99 -> 3 ar_income_tax_before_non_refundable_credits_indiv: 3 -- name: AR 2027 indexed Act 1 of 2026 tables - income the frozen 2026 reduction undertaxed by $108 +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - income the frozen 2026 reduction undertaxed by $108 period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_indiv: 11_300 state_code: AR @@ -577,9 +581,10 @@ # reduction schedule gave 2% x 11,300 - 224 = 2.00 ar_income_tax_before_non_refundable_credits_indiv: 111 -- name: AR 2027 indexed Act 1 of 2026 tables - start of the 3% row +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - start of the 3% row period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_indiv: 11_500 state_code: AR @@ -588,9 +593,10 @@ # (11,500 - 5,800) x 2% + (11,549.50 - 11,500) x 3% = 114 + 1.485 = 115.485 -> 115 ar_income_tax_before_non_refundable_credits_indiv: 115 -- name: AR 2027 indexed Act 1 of 2026 tables - start of the 3.4% row +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - start of the 3.4% row period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_indiv: 16_500 state_code: AR @@ -599,9 +605,10 @@ # 114 + (16,500 - 11,500) x 3% + (16,549.50 - 16,500) x 3.4% = 264 + 1.683 = 265.683 -> 266 ar_income_tax_before_non_refundable_credits_indiv: 266 -- name: AR 2027 indexed Act 1 of 2026 tables - income the frozen 2026 reduction undertaxed by $70.20 +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - income the frozen 2026 reduction undertaxed by $70.20 period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_indiv: 26_500 state_code: AR @@ -611,9 +618,10 @@ # the frozen 2026 reduction schedule gave 3.4% x 26,500 - 367.20 = 533.80 ar_income_tax_before_non_refundable_credits_indiv: 606 -- name: AR 2027 indexed Act 1 of 2026 tables - start of the 3.7% row +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - start of the 3.7% row period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_indiv: 27_200 state_code: AR @@ -622,21 +630,23 @@ # 264 + (27,200 - 16,500) x 3.4% + (27,249.50 - 27,200) x 3.7% = 627.80 + 1.8315 = 629.63 -> 630 ar_income_tax_before_non_refundable_credits_indiv: 630 -- name: AR 2027 indexed Act 1 of 2026 tables - top of table (A) at the indexed high-income threshold +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - top of table (A) at the indexed high-income threshold period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_indiv: 97_500 state_code: AR output: - # The threshold is 94,700 x 1.0298 = 97,522, nearest $100 = 97,500. Row + # The synthetic threshold is 94,700 x 1.03 = 97,541, nearest $100 = 97,500. Row # $97,401-$97,500, midpoint 97,450.50 <= 97,500, so table (A): # 627.80 + (97,450.50 - 27,200) x 3.7% = 627.80 + 2,599.27 = 3,227.07 -> 3,227 ar_income_tax_before_non_refundable_credits_indiv: 3_227 -- name: AR 2027 indexed Act 1 of 2026 tables - high income +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - high income period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_indiv: 150_000 state_code: AR diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml index 973c835dbc3..24546ff9518 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml @@ -208,15 +208,18 @@ # 2024 published last row $35,001-$35,100 -> $765 ar_income_tax_before_non_refundable_credits_joint: [765, 0, 0, 0] -- name: AR 2027 indexed Act 1 of 2026 tables - joint return above the low-income tables +- name: AR 2027 synthetic indexed Act 1 of 2026 tables - joint return above the low-income tables period: 2027 absolute_error_margin: 0.01 + reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_joint: 50_000 ar_low_income_tax_joint: 5_000 state_code: AR output: - # Indexed 2027 rows start at 5,800 / 11,500 / 16,500 / 27,200 (nearest $100). + # Complete synthetic CPI-U windows grow 4%, capped at 3%, while chained + # growth is 1%. These rows do not assert an actual 2027 CPI coefficient. + # Synthetic rows start at 5,800 / 11,500 / 16,500 / 27,200 (nearest $100). # Table row $50,000-$50,099, midpoint 50,049.50: # 5,700 x 2% + 5,000 x 3% + 10,700 x 3.4% + (50,049.50 - 27,200) x 3.7% # = 627.80 + 845.43 = 1,473.23 -> 1,473 From d7fd2b508f531a9e50eeb5131b75a8b536dec2d7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 19:23:33 -0400 Subject: [PATCH 06/13] Avoid redundant parameter copy in Arkansas test reforms --- policyengine_us/tests/fixtures/ar_indexing.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/policyengine_us/tests/fixtures/ar_indexing.py b/policyengine_us/tests/fixtures/ar_indexing.py index 87a7f6be8a1..49191846a24 100644 --- a/policyengine_us/tests/fixtures/ar_indexing.py +++ b/policyengine_us/tests/fixtures/ar_indexing.py @@ -40,10 +40,9 @@ def _synthetic_indexing(parameters, cpi_u_factor, chained_factor): def _indexing_reform(cpi_u_factor, chained_factor): class reform(Reform): def apply(self): - self.modify_parameters( - lambda parameters: _synthetic_indexing( - parameters, cpi_u_factor, chained_factor - ) + # Core clones parameters before apply, so mutate that owned tree. + self.parameters = _synthetic_indexing( + self.parameters, cpi_u_factor, chained_factor ) return reform From bfe165ff288994facb146b5af441640aac2ddd70 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 20:42:50 -0400 Subject: [PATCH 07/13] Fix Kentucky CDCC rate boundary test populations --- .../dependent_care_credit/ky_cdcc.yaml | 88 ++++++++++++++++++- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/credits/dependent_care_credit/ky_cdcc.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/credits/dependent_care_credit/ky_cdcc.yaml index 38b61cf979b..ada3807fa50 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/credits/dependent_care_credit/ky_cdcc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/credits/dependent_care_credit/ky_cdcc.yaml @@ -445,21 +445,101 @@ output: ky_cdcc_potential: 120 -- name: 2021 Kentucky applies each started two-thousand-dollar rate increment +- name: 2021 Kentucky rate boundary at AGI 15,000 is 35 percent period: 2021 absolute_error_margin: 0.01 input: state_code: KY filing_status: SINGLE - adjusted_gross_income: [15_000, 15_001, 17_000, 17_001, 43_000, 43_001] + adjusted_gross_income: 15_000 cdcc_filing_status_eligible: true capped_count_cdcc_eligible: 1 cdcc_qualifying_childcare_expenses: 3_000 min_head_spouse_earned: 50_000 cdcc_credit_limit: 5_000 output: - # 20% match x $3,000 x [35%,34%,34%,33%,21%,20%]. - ky_cdcc_potential: [210, 204, 204, 198, 126, 120] + # 20% Kentucky match x $3,000 expenses x 35% conformed rate. + ky_cdcc_potential: 210 + +- name: 2021 Kentucky rate boundary at AGI 15,001 is 34 percent + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 15_001 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 3_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 5_000 + output: + # 20% Kentucky match x $3,000 expenses x 34% conformed rate. + ky_cdcc_potential: 204 + +- name: 2021 Kentucky rate boundary at AGI 17,000 is 34 percent + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 17_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 3_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 5_000 + output: + # 20% Kentucky match x $3,000 expenses x 34% conformed rate. + ky_cdcc_potential: 204 + +- name: 2021 Kentucky rate boundary at AGI 17,001 is 33 percent + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 17_001 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 3_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 5_000 + output: + # 20% Kentucky match x $3,000 expenses x 33% conformed rate. + ky_cdcc_potential: 198 + +- name: 2021 Kentucky rate boundary at AGI 43,000 is 21 percent + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 43_000 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 3_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 5_000 + output: + # 20% Kentucky match x $3,000 expenses x 21% conformed rate. + ky_cdcc_potential: 126 + +- name: 2021 Kentucky rate boundary at AGI 43,001 is 20 percent + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: KY + filing_status: SINGLE + adjusted_gross_income: 43_001 + cdcc_filing_status_eligible: true + capped_count_cdcc_eligible: 1 + cdcc_qualifying_childcare_expenses: 3_000 + min_head_spouse_earned: 50_000 + cdcc_credit_limit: 5_000 + output: + # 20% Kentucky match x $3,000 expenses x 20% conformed rate. + ky_cdcc_potential: 120 - name: 2020 Kentucky preserves the federal credit computation period: 2020 From 811d94de43901d6cdad443cc5516bd69011abd4a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 21:56:06 -0400 Subject: [PATCH 08/13] Preserve Arkansas rounding convention in tax references --- policyengine_us/tests/test_ar_income_tax_schedule.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/test_ar_income_tax_schedule.py b/policyengine_us/tests/test_ar_income_tax_schedule.py index bb963e8816e..342b24029c2 100644 --- a/policyengine_us/tests/test_ar_income_tax_schedule.py +++ b/policyengine_us/tests/test_ar_income_tax_schedule.py @@ -188,7 +188,9 @@ def act_1_of_2026_tax(ti): def nearest_100(amount): # 26-51-201(d)(1): "rounding to the nearest one hundred dollars ($100)". - return np.floor(amount / 100 + 0.5) * 100 + # Preserve Core's existing ties-to-even convention for exact $50 ties; + # the statute does not specify a tie-breaking rule. + return round(amount / 100) * 100 def indexed_act_1_of_2026_tax(year, ti): From b58117629a979a9e938b1451f8c89975b41c23e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 07:38:16 +0000 Subject: [PATCH 09/13] Index Arkansas brackets on the statute's window, from 2026, ties down A.C.A. 26-51-201(d)(2), as amended by Act 1459 of 2013, compares the current calendar year's CPI with the preceding year's, so tax year Y uses September Y-1 through August Y. The index used the struck preceding-year window and ran one adjustment behind; DFA's 2018-2021 cards follow the current-year window, and a new back-test reproduces all 44 of their bounds from the repository's CPI-U. Index the Act 1 of 2026 tables in 2026 itself, as (a)(5) directs and as DFA indexed each new table in its first year (2022-2024 cards), and round exact $50 ties down, as DFA's 2022 and 2024 cards did. Bounds are written by the extension, since Core's nearest rounding sends ties to even. The 2026 tables now equal the synthetic 3% cases, so those cases move to the 2026 baseline and the three synthetic CPI reforms and their fixture go, along with the two YAML cases the tiny-tree tests already covered. Record the WV 529 coordination as a candidate that follows the statute, and correct the excluded-line-range note to lines 36 and 38. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/ar/tax/income/uprating.yaml | 17 +- .../parameters/uprating_extensions.py | 91 ++++- .../core/test_state_uprating_rounding.py | 35 +- policyengine_us/tests/fixtures/ar_indexing.py | 53 --- .../ar/tax/income/ar_gross_income_indiv.yaml | 8 +- ...x_before_non_refundable_credits_indiv.yaml | 367 ++++++++---------- ...x_before_non_refundable_credits_joint.yaml | 10 +- .../tests/test_ar_income_tax_indexing.py | 201 ++++++++-- .../tests/test_ar_income_tax_schedule.py | 39 +- source_conflicts/candidates.yaml | 37 +- 10 files changed, 495 insertions(+), 363 deletions(-) delete mode 100644 policyengine_us/tests/fixtures/ar_indexing.py diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml index 2b9be524de4..cd904c6155f 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml @@ -1,13 +1,22 @@ description: Arkansas uses this index to adjust its income tax bracket boundaries for annual increases in the Consumer Price Index for All Urban Consumers, capped at three percent. values: - # Neutral history supports zero and infinite bounds defined before 2026. + # 1 through 2025, so the tax year 2026 statutory tables are the base. + # extend_ar_income_tax_uprating compounds each year's adjustment from 2026: + # A.C.A. 26-51-201(a)(5) indexes the Act 1 of 2026 tables under (d), and + # DFA indexed each newly enacted table in its first year (2022-2024 cards). 2014-01-01: 1 - # The published 2026 statutory tables anchor subsequent projections. - 2026-01-01: 1 metadata: unit: /1 label: Arkansas income tax uprating index economy: false reference: - - title: A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2 (CPI-U average for twelve months ending August 31, positive increase capped at three percent) + - title: A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2 (CPI-U average for twelve months ending August 31 of the current calendar year, positive increase capped at three percent) href: https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=3 + - title: A.C.A. § 26-51-201(a)(5), added by Act 1 of 2026 (1st Extraordinary Session), § 1 (the (a)(1)-(4) tables are indexed under (d)) + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=3 + - title: 2022 Indexed Tax Brackets + href: https://www.dfa.arkansas.gov/wp-content/uploads/TaxBrackets_2022.pdf#page=1 + - title: 2023 Indexed Tax Brackets + href: https://www.dfa.arkansas.gov/wp-content/uploads/TaxBrackets_2023.pdf#page=1 + - title: 2024 Indexed Tax Brackets + href: https://www.dfa.arkansas.gov/wp-content/uploads/2024_TaxBrackets.pdf#page=1 diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 02a4d3f05b9..c654ea5c7dd 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -100,30 +100,93 @@ def get_ar_cpi(parameters: ParameterNode, calendar_year: int) -> float: ) +AR_INCOME_TAX_INDEX = "gov.states.ar.tax.income.uprating" +# Act 1 of 2026 (1st Extraordinary Session) enacts the tax year 2026 tables, +# and A.C.A. 26-51-201(a)(5) indexes them under (d). DFA indexed each newly +# enacted table in its first year (the 2022, 2023 and 2024 cards), so the +# 2026 tables are the base and 2026 is the first indexed year. +AR_FIRST_INDEXED_YEAR = 2026 + + +def get_ar_cost_of_living_adjustment(parameters: ParameterNode, year: int) -> float: + """Arkansas's cost-of-living adjustment for calendar (tax) year ``year``. + + A.C.A. 26-51-201(d)(2), as amended by Act 1459 of 2013, section 2: the + percentage, if any, by which the CPI for the current calendar year + exceeds the CPI for the preceding one, capped at the uprating cap. Each + calendar year's CPI is the average for the twelve months ending August + 31 of that year, so tax year Y compares September Y-1 through August Y + with September Y-2 through August Y-1. DFA's 2018-2021 cards follow this + window (see test_ar_income_tax_indexing.py). + """ + increase = get_ar_cpi(parameters, year) / get_ar_cpi(parameters, year - 1) - 1 + cap = parameters.gov.states.ar.tax.income.uprating_cap(f"{year}-01-01") + return min(cap, max(increase, 0)) + + +def round_ar_income_tax_bound(amount: float, interval: float) -> float: + """Round to the nearest ``interval``, with an exact half rounded down. + + 26-51-201(d)(1) rounds to the nearest $100 and names no rule for an + exact $50. DFA rounded both observed ties down: the 2022 card's $5,100 + ($5,000 x 1.03 = $5,150) and the 2024 card's $25,700 ($25,000 x 1.03 = + $25,750). Rounding to 1e-6 units first keeps floating-point error in the + product from breaking a tie. + """ + return math.ceil(round(amount / interval - 0.5, 6)) * interval + + def extend_ar_income_tax_uprating(parameters: ParameterNode, end_year: int) -> None: - """Compound Arkansas's positive annual CPI-U increase, subject to its cap. - - The published 2026 tables anchor projections. For tax year Y, - 26-51-201(d)(2) compares September Y-2 through August Y-1 with - September Y-3 through August Y-2. The statute's increase-if-any - language prevents a decrease. Bracket rounding remains in the - individual parameters' metadata, and bracket-adjustment amounts - remain fixed. + """Build Arkansas's income tax index and index its bracket bounds. + + The index is 1 before AR_FIRST_INDEXED_YEAR and compounds each year's + cost-of-living adjustment from then on. The statute's "if any" language + prevents a decrease. + + Each bound whose uprating metadata names this index is then set, for + every year from AR_FIRST_INDEXED_YEAR to ``end_year``, to its tax year + 2026 statutory value times the index, rounded by + round_ar_income_tax_bound to its metadata's interval. Core's own + rounding would send exact ties to even, so the bounds are written here, + and Core's uprating, which starts after a parameter's last value, then + adds nothing. Bracket-adjustment amounts are not bounds and stay fixed. + Call this once, on a freshly loaded tree: it reads each bound's 2026 + value as the statutory base. """ income = parameters.gov.states.ar.tax.income index = income.uprating - for year in range(2027, end_year + 1): - current = get_ar_cpi(parameters, year - 1) - prior = get_ar_cpi(parameters, year - 2) - increase = min( - income.uprating_cap(f"{year}-01-01"), max(current / prior - 1, 0) - ) + for year in range(AR_FIRST_INDEXED_YEAR, end_year + 1): + increase = get_ar_cost_of_living_adjustment(parameters, year) index.update( period=f"year:{year}-01-01:1", value=index(f"{year - 1}-01-01") * (1 + increase), ) index.update(start=instant(f"{end_year}-01-01"), value=index(f"{end_year}-01-01")) + base_instant = f"{AR_FIRST_INDEXED_YEAR}-01-01" + for parameter in income.rates.main.get_descendants(): + if not isinstance(parameter, Parameter): + continue + meta = parameter.metadata.get("uprating") + if not isinstance(meta, dict) or meta.get("parameter") != AR_INCOME_TAX_INDEX: + continue + interval = meta["rounding"]["interval"] + base = parameter(base_instant) + starts_at_base = parameter.values_list[-1].instant_str >= base_instant + for year in range(AR_FIRST_INDEXED_YEAR, end_year + 1): + parameter.update( + start=instant(f"{year}-01-01"), + value=round_ar_income_tax_bound( + base * index(f"{year}-01-01"), interval + ), + ) + if starts_at_base: + # Backdating copies a parameter's earliest value to earlier + # years; keep that the statutory value, not the indexed one. + parameter.update( + period=f"year:{AR_FIRST_INDEXED_YEAR - 1}-01-01:1", value=base + ) + def get_or_ctc_cola(parameters: ParameterNode, tax_year: int) -> float: """Calculate the Oregon Kids' Credit cost-of-living adjustment. diff --git a/policyengine_us/tests/core/test_state_uprating_rounding.py b/policyengine_us/tests/core/test_state_uprating_rounding.py index 187febc0627..ddc9d79d957 100644 --- a/policyengine_us/tests/core/test_state_uprating_rounding.py +++ b/policyengine_us/tests/core/test_state_uprating_rounding.py @@ -1,28 +1,33 @@ """Regression coverage for state parameter uprating and rounding.""" +from decimal import ROUND_HALF_DOWN, Decimal from math import isinf import pytest from policyengine_us.system import system as SYSTEM -from policyengine_us.tests.fixtures.ar_indexing import ( - ar_cpi_four_percent_chained_one_percent, -) def _thresholds(scale, period, bracket_indexes=(1, 2, 3)): return tuple(scale.brackets[index].threshold(period) for index in bracket_indexes) -def test_ar_income_tax_bounds_round_to_nearest_100_after_last_published_year(): +def _nearest_100_half_down(amount): + # Independent of round_ar_income_tax_bound: decimal half-down rounding + # after rounding the float product to the cent. + cents = Decimal(str(round(amount, 2))) + return int((cents / 100).quantize(Decimal(1), rounding=ROUND_HALF_DOWN) * 100) + + +def test_ar_income_tax_bounds_round_to_nearest_100_from_2026(): # A.C.A. 26-51-201(d)(1): bracket amounts are indexed "rounding to the - # nearest one hundred dollars ($100)". Complete synthetic CPI windows - # avoid assuming a resolution for the missing October 2025 observation. - parameters = ar_cpi_four_percent_chained_one_percent(SYSTEM).parameters - main = parameters.gov.states.ar.tax.income.rates.main - uprating = parameters.gov.states.ar.tax.income.uprating + # nearest one hundred dollars ($100)"; DFA rounded exact $50 ties down. + # From 2026 each bound is Act 1 of 2026's value times the loaded index, + # so a CPI refresh moves the expected values without breaking the test. + main = SYSTEM.parameters.gov.states.ar.tax.income.rates.main + uprating = SYSTEM.parameters.gov.states.ar.tax.income.uprating high_income = main.high_income - bounds_2026 = { + act_1_of_2026 = { "rate": (main.rate, (1, 2, 3, 4), (5_600, 11_200, 16_000, 26_400)), "high-income threshold": (None, None, (94_700,)), "(B) 2% row top": (high_income.rate, (1,), (4_700,)), @@ -33,21 +38,21 @@ def test_ar_income_tax_bounds_round_to_nearest_100_after_last_published_year(): ), } differs_from_rounding_down = False - for year in range(2027, 2036): + for year in range(2026, 2036): period = f"{year}-01-01" - factor = uprating(period) / uprating("2026-01-01") - for name, (scale, indexes, bases) in bounds_2026.items(): + factor = uprating(period) / uprating("2025-01-01") + for name, (scale, indexes, bases) in act_1_of_2026.items(): if scale is None: loaded = (high_income.threshold(period),) else: loaded = _thresholds(scale, period, indexes) - expected = tuple(round(base * factor / 100) * 100 for base in bases) + expected = tuple(_nearest_100_half_down(base * factor) for base in bases) assert loaded == expected, (year, name) rounded_down = tuple(base * factor // 100 * 100 for base in bases) differs_from_rounding_down |= loaded != rounded_down assert isinf(main.rate.brackets[5].threshold(period)) assert isinf(main.rate.brackets[6].threshold(period)) - # The synthetic 3% increase takes $5,600 to $5,768, rounded to $5,800. + # In 2026, 3% takes $5,600 to $5,768, rounded to $5,800. assert differs_from_rounding_down diff --git a/policyengine_us/tests/fixtures/ar_indexing.py b/policyengine_us/tests/fixtures/ar_indexing.py deleted file mode 100644 index 49191846a24..00000000000 --- a/policyengine_us/tests/fixtures/ar_indexing.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Complete synthetic CPI windows for Arkansas indexing mechanism tests.""" - -from policyengine_core.model_api import Reform -from policyengine_core.parameters import Parameter -from policyengine_core.parameters.operations.uprate_parameters import uprate_parameters -from policyengine_core.periods import instant - -from policyengine_us.parameters.uprating_extensions import set_all_uprating_parameters - - -def install_monthly_window(parameter, first_month, value): - """Replace all twelve observations, including any missing baseline month.""" - start = instant(first_month) - for month in range(12): - parameter.update(period=f"month:{start.offset(month, 'month')}:1", value=value) - - -def _synthetic_indexing(parameters, cpi_u_factor, chained_factor): - cpi = parameters.gov.bls.cpi - install_monthly_window(cpi.cpi_u, "2024-09-01", 100) - install_monthly_window(cpi.cpi_u, "2025-09-01", 100 * cpi_u_factor) - # Preserve the shared IRS index's existing August-July window. This - # makes its growth intentionally differ from Arkansas's CPI-U growth. - install_monthly_window(cpi.c_cpi_u, "2024-08-01", 100) - install_monthly_window(cpi.c_cpi_u, "2025-08-01", 100 * chained_factor) - - main = parameters.gov.states.ar.tax.income.rates.main - for parameter in main.get_descendants(): - if isinstance(parameter, Parameter) and parameter.metadata.get("uprating"): - parameter.update( - start=instant("2026-01-01"), - value=parameter("2026-01-01"), - remove_after=True, - ) - - set_all_uprating_parameters(parameters) - return uprate_parameters(parameters) - - -def _indexing_reform(cpi_u_factor, chained_factor): - class reform(Reform): - def apply(self): - # Core clones parameters before apply, so mutate that owned tree. - self.parameters = _synthetic_indexing( - self.parameters, cpi_u_factor, chained_factor - ) - - return reform - - -ar_cpi_four_percent_chained_one_percent = _indexing_reform(1.04, 1.01) -ar_cpi_one_percent_chained_four_percent = _indexing_reform(1.01, 1.04) -ar_cpi_minus_one_percent_chained_four_percent = _indexing_reform(0.99, 1.04) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_gross_income_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_gross_income_indiv.yaml index cc211e3e470..b1c1f1d1bb4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_gross_income_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_gross_income_indiv.yaml @@ -130,9 +130,11 @@ ar_gross_income_indiv: 60_000 ar_gross_income_joint: 60_000 # Net taxable income 60,000 - 2,520 = 57,480; table row $57,400-$57,499, - # midpoint 57,449.50: 609.60 + (57,449.50 - 26,400) x 3.7% = 1,758.43 -> 1,758; - # less the 29 personal credit = 1,729 - ar_income_tax: 1_729 + # midpoint 57,449.50. The 2026 tables are Act 1's indexed by 3%, so the + # 3.7% row starts at 27,200 and the tax there is 627.80: + # 627.80 + (57,449.50 - 27,200) x 3.7% = 1,747.03 -> 1,747; + # less the 29 personal credit = 1,718 + ar_income_tax: 1_718 - name: Arkansas unemployment compensation is exempt through 2017 (2016) period: 2016 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml index 7a024de1465..0891e707bd7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml @@ -160,33 +160,85 @@ # 3_809 + 0.039 * 50_000 = 5_759 ar_income_tax_before_non_refundable_credits_indiv: 5_759 -# 2026 tests follow A.C.A. § 26-51-201(a)(4) as amended by HB1001 (2026 1st Extraordinary Session). -# Bill schedules: (a)(4)(A) for net income <= $94,700, (a)(4)(B) for > $94,700, plus -# the (a)(4)(C) bracket adjustment for $94,701 - $97,600. +# 2026 tests follow A.C.A. § 26-51-201(a)(4) as amended by Act 1 of 2026 (HB1001, +# 1st Extraordinary Session), indexed for 2026 under (a)(5) and (d): DFA indexed +# each newly enacted table in its first year (2022-2024 cards). The 2026 +# adjustment is capped at 3% (the model's CPI-U gives 3.06%), and each bound is +# rounded to the nearest $100, an exact $50 down: +# - (A): 0% to $5,799, then 2% from $5,800 (5,600 x 1.03 = 5,768), 3% from +# $11,500 (11,536), 3.4% from $16,500 (16,480) and 3.7% from $27,200 +# (27,192). Tax at each start: 114 at 11,500; 264 at 16,500; 627.80 at 27,200. +# - Net income above $97,500 (94,700 x 1.03 = 97,541) uses (B): 2% on the first +# $4,800 (4,841), 3.7% above, less the (C) adjustment. +# - (C): Act 1's rows, each "From - 1" bound x 1.03. $290 above $97,500, $280 +# above $97,600, $270 above $97,700, $260 above $97,800 (95,000 x 1.03 = +# 97,850, a tie, down), $250 above $98,000 (97,953), then $10 less per $100 +# to $10 above $100,400 (97,500 x 1.03 = 100,425) and $0 above $100,500. # No 2026 Regular Income Tax Table is published yet, so below $100,001 the # model applies DFA's table method: these schedules at the midpoint of the # income's $100 table row, rounded to whole dollars. -- name: AR HB1001 2026 - 0% bracket, no tax +- name: AR 2026 indexed Act 1 tables - 0% row period: 2026 absolute_error_margin: 0.01 input: ar_taxable_income_indiv: 5_000 state_code: AR output: - # within $0 - $5,599 0% bracket + # Within $0 - $5,799 at 0%. ar_income_tax_before_non_refundable_credits_indiv: 0 -- name: AR HB1001 2026 - 2% bracket +- name: AR 2026 indexed Act 1 tables - top of the 0% row + period: 2026 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 5_799 + state_code: AR + output: + # Row $5,700-$5,799, midpoint 5,749.50 < 5,800. + ar_income_tax_before_non_refundable_credits_indiv: 0 + +- name: AR 2026 indexed Act 1 tables - 2% row + period: 2026 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 5_900 + state_code: AR + output: + # Row $5,900-$5,999, midpoint 5,949.50: (5,949.50 - 5,800) x 2% = 2.99 -> 3 + ar_income_tax_before_non_refundable_credits_indiv: 3 + +- name: AR 2026 indexed Act 1 tables - inside the 2% row period: 2026 absolute_error_margin: 0.01 input: ar_taxable_income_indiv: 10_000 state_code: AR output: - # Row $10,000-$10,099, midpoint 10,049.50: (10,049.50 - 5,600) x 2% = 88.99 -> 89 - ar_income_tax_before_non_refundable_credits_indiv: 89 + # Row $10,000-$10,099, midpoint 10,049.50: (10,049.50 - 5,800) x 2% = 84.99 -> 85 + ar_income_tax_before_non_refundable_credits_indiv: 85 -- name: AR HB1001 2026 - 3% bracket +- name: AR 2026 indexed Act 1 tables - top of the 2% row + period: 2026 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 11_300 + state_code: AR + output: + # Row $11,300-$11,399, midpoint 11,349.50: (11,349.50 - 5,800) x 2% = 110.99 -> 111 + ar_income_tax_before_non_refundable_credits_indiv: 111 + +- name: AR 2026 indexed Act 1 tables - start of the 3% row + period: 2026 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 11_500 + state_code: AR + output: + # Row $11,500-$11,599, midpoint 11,549.50: + # 114 + (11,549.50 - 11,500) x 3% = 114 + 1.485 = 115.485 -> 115 + ar_income_tax_before_non_refundable_credits_indiv: 115 + +- name: AR 2026 indexed Act 1 tables - inside the 3% row period: 2026 absolute_error_margin: 0.01 input: @@ -194,10 +246,21 @@ state_code: AR output: # Row $15,000-$15,099, midpoint 15,049.50: - # 112 + (15,049.50 - 11,200) x 3% = 112 + 115.485 = 227.485 -> 227 - ar_income_tax_before_non_refundable_credits_indiv: 227 + # 114 + (15,049.50 - 11,500) x 3% = 114 + 106.485 = 220.485 -> 220 + ar_income_tax_before_non_refundable_credits_indiv: 220 -- name: AR HB1001 2026 - 3.4% bracket +- name: AR 2026 indexed Act 1 tables - start of the 3.4% row + period: 2026 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 16_500 + state_code: AR + output: + # Row $16,500-$16,599, midpoint 16,549.50: + # 264 + (16,549.50 - 16,500) x 3.4% = 264 + 1.683 = 265.683 -> 266 + ar_income_tax_before_non_refundable_credits_indiv: 266 + +- name: AR 2026 indexed Act 1 tables - inside the 3.4% row period: 2026 absolute_error_margin: 0.01 input: @@ -205,10 +268,32 @@ state_code: AR output: # Row $20,000-$20,099, midpoint 20,049.50: - # 112 + 144 + (20,049.50 - 16,000) x 3.4% = 256 + 137.683 = 393.683 -> 394 - ar_income_tax_before_non_refundable_credits_indiv: 394 + # 264 + (20,049.50 - 16,500) x 3.4% = 264 + 120.683 = 384.683 -> 385 + ar_income_tax_before_non_refundable_credits_indiv: 385 -- name: AR HB1001 2026 - top low-income bracket (3.7%) +- name: AR 2026 indexed Act 1 tables - income between Act 1's and the indexed 3.7% start + period: 2026 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 26_500 + state_code: AR + output: + # Row $26,500-$26,599, midpoint 26,549.50: + # 264 + (26,549.50 - 16,500) x 3.4% = 264 + 341.683 = 605.683 -> 606 + ar_income_tax_before_non_refundable_credits_indiv: 606 + +- name: AR 2026 indexed Act 1 tables - start of the 3.7% row + period: 2026 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 27_200 + state_code: AR + output: + # Row $27,200-$27,299, midpoint 27,249.50: + # 627.80 + (27,249.50 - 27,200) x 3.7% = 627.80 + 1.8315 = 629.63 -> 630 + ar_income_tax_before_non_refundable_credits_indiv: 630 + +- name: AR 2026 indexed Act 1 tables - inside the 3.7% row period: 2026 absolute_error_margin: 0.01 input: @@ -216,96 +301,116 @@ state_code: AR output: # Row $50,000-$50,099, midpoint 50,049.50: - # 609.60 + (50,049.50 - 26,400) x 3.7% = 609.60 + 875.03 = 1,484.63 -> 1,485 - ar_income_tax_before_non_refundable_credits_indiv: 1_485 + # 627.80 + (50,049.50 - 27,200) x 3.7% = 627.80 + 845.43 = 1,473.23 -> 1,473 + ar_income_tax_before_non_refundable_credits_indiv: 1_473 -- name: AR HB1001 2026 - boundary at $94,700 under (a)(4)(A) +- name: AR 2026 indexed Act 1 tables - Act 1's unindexed threshold stays under (A) period: 2026 absolute_error_margin: 0.01 input: - ar_taxable_income_indiv: 94_700 + ar_taxable_income_indiv: 94_701 state_code: AR output: - # Row $94,601-$94,700, midpoint 94,650.50 <= 94,700, so (a)(4)(A): - # 609.60 + (94,650.50 - 26,400) x 3.7% = 609.60 + 2,525.27 = 3,134.87 -> 3,135 - ar_income_tax_before_non_refundable_credits_indiv: 3_135 + # Row $94,701-$94,800, midpoint 94,750.50 <= 97,500, so (A): + # 627.80 + (94,750.50 - 27,200) x 3.7% = 627.80 + 2,499.37 = 3,127.17 -> 3,127 + ar_income_tax_before_non_refundable_credits_indiv: 3_127 -- name: AR HB1001 2026 - bracket adjustment top ($94,701, $290 adj) +- name: AR 2026 indexed Act 1 tables - top of (A) at the indexed threshold period: 2026 absolute_error_margin: 0.01 input: - ar_taxable_income_indiv: 94_701 + ar_taxable_income_indiv: 97_500 state_code: AR output: - # Row $94,701-$94,800, midpoint 94,750.50 > 94,700, so (a)(4)(B) less (C): - # 94 + (94,750.50 - 4,700) x 3.7% = 3,425.87; - 290 = 3,135.87 -> 3,136 - # (the table removes the statute's $2.66 drop from $94,700 to $94,701) - ar_income_tax_before_non_refundable_credits_indiv: 3_136 + # Row $97,401-$97,500, midpoint 97,450.50 <= 97,500, so (A): + # 627.80 + (97,450.50 - 27,200) x 3.7% = 627.80 + 2,599.27 = 3,227.07 -> 3,227 + ar_income_tax_before_non_refundable_credits_indiv: 3_227 -- name: AR HB1001 2026 - bracket adjustment mid ($95,500, $220 adj) +- name: AR 2026 indexed Act 1 tables - first (C) row ($290) period: 2026 absolute_error_margin: 0.01 input: - ar_taxable_income_indiv: 95_500 + ar_taxable_income_indiv: 97_600 state_code: AR output: - # Row $95,401-$95,500, midpoint 95,450.50: - # 94 + (95,450.50 - 4,700) x 3.7% = 3,451.77; (C) row "$95,401-$95,500": - 220 = 3,231.77 -> 3,232 - ar_income_tax_before_non_refundable_credits_indiv: 3_232 + # Row $97,501-$97,600, midpoint 97,550.50 > 97,500, so (B) less (C): + # 96 + (97,550.50 - 4,800) x 3.7% = 3,527.77; - 290 = 3,237.77 -> 3,238 + ar_income_tax_before_non_refundable_credits_indiv: 3_238 -- name: AR HB1001 2026 - bracket adjustment mid ($96,500, $120 adj) +- name: AR 2026 indexed Act 1 tables - second (C) row ($280) period: 2026 absolute_error_margin: 0.01 input: - ar_taxable_income_indiv: 96_500 + ar_taxable_income_indiv: 97_601 state_code: AR output: - # Row $96,401-$96,500, midpoint 96,450.50: - # 94 + (96,450.50 - 4,700) x 3.7% = 3,488.77; (C) row "$96,401-$96,500": - 120 = 3,368.77 -> 3,369 - ar_income_tax_before_non_refundable_credits_indiv: 3_369 + # Row $97,601-$97,700, midpoint 97,650.50: + # 96 + (97,650.50 - 4,800) x 3.7% = 3,531.47; - 280 = 3,251.47 -> 3,251 + ar_income_tax_before_non_refundable_credits_indiv: 3_251 -- name: AR HB1001 2026 - bracket adjustment mid ($97,000, $70 adj) +- name: AR 2026 indexed Act 1 tables - $200-wide (C) row from the rounded-down tie ($260) period: 2026 absolute_error_margin: 0.01 input: - ar_taxable_income_indiv: 97_000 + ar_taxable_income_indiv: 97_950 state_code: AR output: - # Row $96,901-$97,000, midpoint 96,950.50: - # 94 + (96,950.50 - 4,700) x 3.7% = 3,507.27; (C) row "$96,901-$97,000": - 70 = 3,437.27 -> 3,437 - ar_income_tax_before_non_refundable_credits_indiv: 3_437 + # Row $97,901-$98,000, midpoint 97,950.50, is in the $97,801-$98,000 (C) row: + # 96 + (97,950.50 - 4,800) x 3.7% = 3,542.57; - 260 = 3,282.57 -> 3,283 + # (rounding the 97,850 tie up would make this the $250 row: 3,293) + ar_income_tax_before_non_refundable_credits_indiv: 3_283 -- name: AR HB1001 2026 - bracket adjustment bottom ($97,600, $10 adj) +- name: AR 2026 indexed Act 1 tables - (C) row after the wide row ($250) period: 2026 absolute_error_margin: 0.01 input: - ar_taxable_income_indiv: 97_600 + ar_taxable_income_indiv: 98_001 state_code: AR output: - # Row $97,501-$97,600, midpoint 97,550.50: - # 94 + (97,550.50 - 4,700) x 3.7% = 3,529.47; (C) adjustment - 10 = 3,519.47 -> 3,519 - ar_income_tax_before_non_refundable_credits_indiv: 3_519 + # Row $98,001-$98,100, midpoint 98,050.50: + # 96 + (98,050.50 - 4,800) x 3.7% = 3,546.27; - 250 = 3,296.27 -> 3,296 + ar_income_tax_before_non_refundable_credits_indiv: 3_296 -- name: AR HB1001 2026 - just past adjustment ($97,601, $0 adj) +- name: AR 2026 indexed Act 1 tables - last (C) row ($10) period: 2026 absolute_error_margin: 0.01 input: - ar_taxable_income_indiv: 97_601 + ar_taxable_income_indiv: 100_500 state_code: AR output: - # Row $97,601-$97,700, midpoint 97,650.50: - # 94 + (97,650.50 - 4,700) x 3.7% = 3,533.17 -> 3,533 - ar_income_tax_before_non_refundable_credits_indiv: 3_533 + # At or above $100,001 the formula applies to the exact income: + # 96 + (100,500 - 4,800) x 3.7% = 3,636.90; - 10 = 3,626.90 + ar_income_tax_before_non_refundable_credits_indiv: 3_626.90 + +- name: AR 2026 indexed Act 1 tables - past the (C) ladder + period: 2026 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 100_501 + state_code: AR + output: + # 96 + (100,501 - 4,800) x 3.7% = 96 + 3,540.937 = 3,636.94 + ar_income_tax_before_non_refundable_credits_indiv: 3_636.94 -- name: AR HB1001 2026 - high income (a)(4)(B) +- name: AR 2026 indexed Act 1 tables - high income + period: 2026 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 150_000 + state_code: AR + output: + # 2% x 4,800 + 3.7% x 145,200 = 96 + 5,372.40 = 5,468.40 + ar_income_tax_before_non_refundable_credits_indiv: 5_468.40 + +- name: AR 2026 indexed Act 1 tables - high income (B) period: 2026 absolute_error_margin: 0.01 input: ar_taxable_income_indiv: 200_000 state_code: AR output: - # $94 + ($200,000 - $4,700) * 0.037 = $7,320.10 - ar_income_tax_before_non_refundable_credits_indiv: 7_320.10 + # 96 + (200,000 - 4,800) x 3.7% = 96 + 7,222.40 = 7,318.40 + ar_income_tax_before_non_refundable_credits_indiv: 7_318.40 - name: AR 2023 bracket adjustment - row 89,901-90,000 that PE previously omitted (pe-us 9026) period: 2023 @@ -541,119 +646,6 @@ # Midpoint 95,550.50 x 3.9% - 87.40 = 3,639.07 -> 3,639 ar_income_tax_before_non_refundable_credits_indiv: 3_639 -# These 2027 mechanism tests use complete synthetic CPI-U windows with 4% -# growth, capped at 3%, and independent chained growth of 1%. They do not -# assert an actual 2027 coefficient: October 2025 CPI was not published. -# The synthetic indexed boundaries are $5,800/$11,500/$16,500/$27,200, -# the high-income threshold is $97,500, and its 2% boundary is $4,800. -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - top of the 0% row - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - ar_taxable_income_indiv: 5_799 - state_code: AR - output: - # The synthetic 2% row starts at $5,800 (5,600 x 1.03 = 5,768, nearest $100). - ar_income_tax_before_non_refundable_credits_indiv: 0 - -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - 2% row - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - ar_taxable_income_indiv: 5_900 - state_code: AR - output: - # Row $5,900-$5,999, midpoint 5,949.50: (5,949.50 - 5,800) x 2% = 2.99 -> 3 - ar_income_tax_before_non_refundable_credits_indiv: 3 - -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - income the frozen 2026 reduction undertaxed by $108 - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - ar_taxable_income_indiv: 11_300 - state_code: AR - output: - # Row $11,300-$11,399, midpoint 11,349.50: (11,349.50 - 5,800) x 2% = 110.99 -> 111; - # at the exact income, (11,300 - 5,800) x 2% = 110.00, and the frozen 2026 - # reduction schedule gave 2% x 11,300 - 224 = 2.00 - ar_income_tax_before_non_refundable_credits_indiv: 111 - -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - start of the 3% row - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - ar_taxable_income_indiv: 11_500 - state_code: AR - output: - # Row $11,500-$11,599, midpoint 11,549.50: - # (11,500 - 5,800) x 2% + (11,549.50 - 11,500) x 3% = 114 + 1.485 = 115.485 -> 115 - ar_income_tax_before_non_refundable_credits_indiv: 115 - -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - start of the 3.4% row - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - ar_taxable_income_indiv: 16_500 - state_code: AR - output: - # Row $16,500-$16,599, midpoint 16,549.50: - # 114 + (16,500 - 11,500) x 3% + (16,549.50 - 16,500) x 3.4% = 264 + 1.683 = 265.683 -> 266 - ar_income_tax_before_non_refundable_credits_indiv: 266 - -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - income the frozen 2026 reduction undertaxed by $70.20 - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - ar_taxable_income_indiv: 26_500 - state_code: AR - output: - # Row $26,500-$26,599, midpoint 26,549.50: - # 264 + (26,549.50 - 16,500) x 3.4% = 264 + 341.683 = 605.683 -> 606; - # the frozen 2026 reduction schedule gave 3.4% x 26,500 - 367.20 = 533.80 - ar_income_tax_before_non_refundable_credits_indiv: 606 - -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - start of the 3.7% row - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - ar_taxable_income_indiv: 27_200 - state_code: AR - output: - # Row $27,200-$27,299, midpoint 27,249.50: - # 264 + (27,200 - 16,500) x 3.4% + (27,249.50 - 27,200) x 3.7% = 627.80 + 1.8315 = 629.63 -> 630 - ar_income_tax_before_non_refundable_credits_indiv: 630 - -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - top of table (A) at the indexed high-income threshold - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - ar_taxable_income_indiv: 97_500 - state_code: AR - output: - # The synthetic threshold is 94,700 x 1.03 = 97,541, nearest $100 = 97,500. Row - # $97,401-$97,500, midpoint 97,450.50 <= 97,500, so table (A): - # 627.80 + (97,450.50 - 27,200) x 3.7% = 627.80 + 2,599.27 = 3,227.07 -> 3,227 - ar_income_tax_before_non_refundable_credits_indiv: 3_227 - -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - high income - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - ar_taxable_income_indiv: 150_000 - state_code: AR - output: - # 96 + (150,000 - 4,800) x 3.7% = 5,468.40 - ar_income_tax_before_non_refundable_credits_indiv: 5_468.40 - # Regular Income Tax Table (issue 9681). Below the table threshold ($100,001 # from 2023), filers look up net taxable income in $100 rows. DFA computes each # row at its midpoint and rounds to whole dollars: "If you use a formula to @@ -807,40 +799,3 @@ output: # "For $50,000 and over" (p.6): 50,000 x 7% - 949.24 = 2,550.76 ar_income_tax_before_non_refundable_credits_indiv: 2_550.76 - -# A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2, PDF page 3: -# https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=3 -# These complete synthetic windows test the indexing rule, not an actual -# 2027 coefficient: the October 2025 CPI observation was not published. -- name: Arkansas caps synthetic four percent CPI-U growth at three percent - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent - input: - state_code: AR - ar_taxable_income_indiv: 150_000 - output: - # $4,700 * 1.03 rounds to $4,800; $96 + $145,200 * 3.7%. - ar_income_tax_before_non_refundable_credits_indiv: 5_468.40 - -- name: Arkansas uses synthetic CPI-U growth rather than chained growth - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_one_percent_chained_four_percent - input: - state_code: AR - ar_taxable_income_indiv: 150_000 - output: - # $4,700 * 1.01 rounds to $4,700; $94 + $145,300 * 3.7%. - ar_income_tax_before_non_refundable_credits_indiv: 5_470.10 - -- name: Arkansas does not lower boundaries after a synthetic CPI-U decrease - period: 2027 - absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_minus_one_percent_chained_four_percent - input: - state_code: AR - ar_taxable_income_indiv: 150_000 - output: - # Negative CPI-U growth leaves the $4,700 boundary unchanged. - ar_income_tax_before_non_refundable_credits_indiv: 5_470.10 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml index 24546ff9518..992aecc4e1b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml @@ -208,18 +208,16 @@ # 2024 published last row $35,001-$35,100 -> $765 ar_income_tax_before_non_refundable_credits_joint: [765, 0, 0, 0] -- name: AR 2027 synthetic indexed Act 1 of 2026 tables - joint return above the low-income tables - period: 2027 +- name: AR 2026 indexed Act 1 of 2026 tables - joint return above the low-income tables + period: 2026 absolute_error_margin: 0.01 - reforms: policyengine_us.tests.fixtures.ar_indexing.ar_cpi_four_percent_chained_one_percent input: ar_taxable_income_joint: 50_000 ar_low_income_tax_joint: 5_000 state_code: AR output: - # Complete synthetic CPI-U windows grow 4%, capped at 3%, while chained - # growth is 1%. These rows do not assert an actual 2027 CPI coefficient. - # Synthetic rows start at 5,800 / 11,500 / 16,500 / 27,200 (nearest $100). + # 2026 rows are Act 1's x 1.03 (capped), to the nearest $100: + # 5,800 / 11,500 / 16,500 / 27,200. # Table row $50,000-$50,099, midpoint 50,049.50: # 5,700 x 2% + 5,000 x 3% + 10,700 x 3.4% + (50,049.50 - 27,200) x 3.7% # = 627.80 + 845.43 = 1,473.23 -> 1,473 diff --git a/policyengine_us/tests/test_ar_income_tax_indexing.py b/policyengine_us/tests/test_ar_income_tax_indexing.py index 16300eb7428..55f4e290bb0 100644 --- a/policyengine_us/tests/test_ar_income_tax_indexing.py +++ b/policyengine_us/tests/test_ar_income_tax_indexing.py @@ -1,25 +1,52 @@ -"""Arkansas's statutory CPI-U window, cap, floor, and annual compounding.""" +"""Arkansas's statutory CPI-U window, cap, floor, rounding and compounding. + +A.C.A. 26-51-201(d)(2), as amended by Act 1459 of 2013, section 2, compares +the CPI for the current calendar year (the twelve months ending August 31) +with the preceding calendar year's. So tax year Y uses September Y-1 through +August Y over September Y-2 through August Y-1. The tiny trees below hold +only CPI-U and the Arkansas parameters, so the index cannot read chained +CPI-U or the IRS index. The back-test reads the full system's parameters +only; no model or simulation is built. +""" import pytest from policyengine_core.parameters import ParameterNode from policyengine_core.periods import instant from policyengine_us.parameters.uprating_extensions import ( + AR_INCOME_TAX_INDEX, extend_ar_income_tax_uprating, + get_ar_cost_of_living_adjustment, get_ar_cpi, + round_ar_income_tax_bound, ) from policyengine_us.system import system -from policyengine_us.tests.fixtures.ar_indexing import ( - ar_cpi_four_percent_chained_one_percent, -) +BOUND_METADATA = { + "uprating": { + "parameter": AR_INCOME_TAX_INDEX, + "rounding": {"type": "nearest", "interval": 100}, + } +} -def synthetic_parameters(annual_levels): + +def synthetic_parameters(annual_levels, bounds=None): + """CPI-U windows from September 2024, one level per twelve months.""" monthly = {} for offset, level in enumerate(annual_levels): start = instant(f"{2024 + offset}-09-01") for month in range(12): monthly[str(start.offset(month, "month"))] = level + main = { + name: {"values": {"2026-01-01": base}, "metadata": BOUND_METADATA} + for name, base in (bounds or {}).items() + } + # A parameter with an earlier history and a non-Arkansas index. + main["unindexed"] = {"values": {"2026-01-01": 25_000}} + main["history"] = { + "values": {"2025-01-01": 5_600, "2026-01-01": 5_600}, + "metadata": BOUND_METADATA, + } return ParameterNode( data={ "gov": { @@ -28,8 +55,9 @@ def synthetic_parameters(annual_levels): "ar": { "tax": { "income": { - "uprating": {"values": {"2026-01-01": 1}}, + "uprating": {"values": {"2014-01-01": 1}}, "uprating_cap": {"values": {"2014-01-01": 0.03}}, + "rates": {"main": main}, } } } @@ -39,12 +67,6 @@ def synthetic_parameters(annual_levels): ) -@pytest.fixture(scope="module") -def capped_parameters(): - """Read-only reformed parameters shared by the Core integration checks.""" - return ar_cpi_four_percent_chained_one_percent(system).parameters - - def test_ar_cpi_averages_september_through_august(): parameters = synthetic_parameters((100, 100)) cpi = parameters.gov.bls.cpi.cpi_u @@ -55,9 +77,11 @@ def test_ar_cpi_averages_september_through_august(): cpi.update(period="month:2026-09-01:1", value=500) assert get_ar_cpi(parameters, 2025) == pytest.approx(100) + # 11 months at 100 and August 2026 at 124: 1,224 / 12 = 102. assert get_ar_cpi(parameters, 2026) == pytest.approx(102) - extend_ar_income_tax_uprating(parameters, end_year=2027) - assert parameters.gov.states.ar.tax.income.uprating("2027-01-01") == pytest.approx( + extend_ar_income_tax_uprating(parameters, end_year=2026) + # Tax year 2026 takes calendar 2026's adjustment: 102 / 100 - 1 = 2%. + assert parameters.gov.states.ar.tax.income.uprating("2026-01-01") == pytest.approx( 1.02 ) @@ -69,11 +93,11 @@ def test_ar_cpi_averages_september_through_august(): def test_ar_index_applies_the_increase_if_any_and_cap(current_level, expected_index): parameters = synthetic_parameters((100, current_level)) - extend_ar_income_tax_uprating(parameters, end_year=2027) + extend_ar_income_tax_uprating(parameters, end_year=2026) - assert parameters.gov.states.ar.tax.income.uprating("2027-01-01") == pytest.approx( - expected_index - ) + index = parameters.gov.states.ar.tax.income.uprating + assert index("2025-01-01") == 1 + assert index("2026-01-01") == pytest.approx(expected_index) def test_ar_index_compounds_each_year_from_the_preceding_index(): @@ -82,41 +106,132 @@ def test_ar_index_compounds_each_year_from_the_preceding_index(): extend_ar_income_tax_uprating(parameters, end_year=2028) index = parameters.gov.states.ar.tax.income.uprating - assert index("2026-01-01") == 1 - assert index("2027-01-01") == pytest.approx(1.03) - # Four percent is capped in 2027, then two percent compounds in 2028. + # Four percent is capped in 2026, then 106.08 / 104 = 2% in 2027. + assert index("2026-01-01") == pytest.approx(1.03) + assert index("2027-01-01") == pytest.approx(1.03 * 1.02) + # The window after August 2027 repeats 106.08, so 2028 adds nothing. assert index("2028-01-01") == pytest.approx(1.03 * 1.02) assert index("2029-01-01") == pytest.approx(index("2028-01-01")) -def test_ar_index_and_irs_index_remain_independent(capped_parameters): - ar_index = capped_parameters.gov.states.ar.tax.income.uprating - irs_index = capped_parameters.gov.irs.uprating +@pytest.mark.parametrize( + "amount, expected", + ( + # DFA's ties: 2022 card ($5,000 x 1.03) and 2024 card ($25,000 x 1.03). + (5_000 * 1.03, 5_100), + (25_000 * 1.03, 25_700), + (95_000 * 1.03, 97_800), + (5_749.99, 5_700), + (5_750.01, 5_800), + (5_768, 5_800), + (0, 0), + ), +) +def test_ar_bounds_round_to_nearest_100_with_ties_down(amount, expected): + assert round_ar_income_tax_bound(amount, 100) == expected - assert ar_index("2027-01-01") / ar_index("2026-01-01") == pytest.approx(1.03) - assert irs_index("2027-01-01") / irs_index("2026-01-01") == pytest.approx(1.01) +def test_ar_index_rounds_each_bound_from_its_2026_value(): + parameters = synthetic_parameters( + (100, 104, 106.08), + bounds={"tie": 25_000, "row": 5_600, "threshold": 94_700, "top": 4_700}, + ) -def test_ar_published_tables_and_fixed_bracket_adjustments_are_preserved( - capped_parameters, -): - original = system.parameters.gov.states.ar.tax.income.rates.main - reformed = capped_parameters.gov.states.ar.tax.income.rates.main + extend_ar_income_tax_uprating(parameters, end_year=2028) - for year in range(2014, 2027): - period = f"{year}-01-01" - assert [b.threshold(period) for b in reformed.rate.brackets] == [ - b.threshold(period) for b in original.rate.brackets - ] - assert [b.threshold("2026-01-01") for b in reformed.rate.brackets[1:5]] == [ + main = parameters.gov.states.ar.tax.income.rates.main + # 2026, index 1.03: 25,750 (tie, down), 5,768, 97,541 and 4,841. + assert main.tie("2026-01-01") == 25_700 + assert main.row("2026-01-01") == 5_800 + assert main.threshold("2026-01-01") == 97_500 + assert main.top("2026-01-01") == 4_800 + # 2027, index 1.0506: 26,265, 5,883.36, 99,491.82 and 4,937.82. + assert main.tie("2027-01-01") == 26_300 + assert main.row("2027-01-01") == 5_900 + assert main.threshold("2027-01-01") == 99_500 + assert main.top("2027-01-01") == 4_900 + # Earlier years keep the statutory value, whether or not the parameter + # has a history before 2026. + assert main.tie("2025-01-01") == 25_000 + assert main.history("2025-01-01") == 5_600 + assert main.history("2026-01-01") == 5_800 + # A parameter not indexed by the Arkansas index is untouched. + assert main.unindexed("2027-01-01") == 25_000 + + +def test_ar_2026_tables_index_act_1_and_keep_bracket_adjustments_fixed(): + income = system.parameters.gov.states.ar.tax.income + main = income.rates.main + # The model's CPI-U gives 3.06% for 2026, capped at 3%. + assert income.uprating("2026-01-01") == pytest.approx(1.03) + # Act 1 of 2026's $5,600, $11,200, $16,000 and $26,400 row starts, + # $94,700 threshold and $4,700 (B) bound, each x 1.03 to the nearest $100. + assert [b.threshold("2026-01-01") for b in main.rate.brackets[1:5]] == [ + 5_800, + 11_500, + 16_500, + 27_200, + ] + assert main.high_income.threshold("2026-01-01") == 97_500 + assert main.high_income.rate.brackets[1].threshold("2026-01-01") == 4_800 + # 2025 is DFA's 2025 card, unchanged. + assert [b.threshold("2025-01-01") for b in main.rate.brackets[1:5]] == [ 5_600, 11_200, 16_000, 26_400, ] - assert reformed.high_income.threshold("2026-01-01") == 94_700 - assert reformed.high_income.threshold("2027-01-01") == 97_500 - assert reformed.high_income.rate.brackets[1].threshold("2027-01-01") == 4_800 - assert [ - b.amount("2027-01-01") for b in reformed.high_income.bracket_adjustment.brackets - ] == list(range(290, -1, -10)) + for year in (2026, 2027, 2035): + assert [ + b.amount(f"{year}-01-01") + for b in main.high_income.bracket_adjustment.brackets + ] == list(range(290, -1, -10)) + + +def card_bounds(year): + """A DFA card's bracket bounds, as recorded in reduction.yaml. + + Row starts are recorded as the bound (for example 4,400) and rows of the + table above $75,000 as the bound plus 1 (for example 77,401). + """ + reduction = system.parameters.gov.states.ar.tax.income.rates.main.reduction + thresholds = [b.threshold(f"{year}-01-01") for b in reduction.brackets] + finite = [t for t in thresholds if 0 < t < float("inf")] + return [t - 1 if t % 100 == 1 else t for t in finite] + + +@pytest.mark.parametrize("year", (2018, 2019, 2020, 2021)) +def test_ar_window_reproduces_dfa_cards(year): + """Each card's bounds index to the next card's, all 11 of them. + + The repository's CPI-U is the not seasonally adjusted series before + 2024, so its windows for 2018-2021 are the statute's: 2.4193%, 1.9000%, + 1.4583% and 2.9977%. The window for tax year Y-1 (the rule Act 1459 + struck) gives 1.9874%, 2.4193%, 1.9000% and 1.4583%, and misses most of + these bounds. The 2025 card is not tested here: from 2024 the series is + seasonally adjusted, and across that splice the 2025 window computes to + 2.7265%, not the not seasonally adjusted 2.6531%. + """ + adjustment = get_ar_cost_of_living_adjustment(system.parameters, year) + previous = card_bounds(year - 1) + published = card_bounds(year) + assert len(previous) == len(published) == 11 + predicted = [ + round_ar_income_tax_bound(bound * (1 + adjustment), 100) for bound in previous + ] + assert predicted == published + + +@pytest.mark.parametrize("year", (2023, 2024)) +def test_ar_capped_years_reproduce_dfa_cards(year): + """2023 and 2024 are capped at 3% under either window; 2024 has a tie. + + The row starts and the high-income threshold (the first row of the + table above it, less 1) of the previous card, x 1.03, give the card's. + $25,000 x 1.03 = $25,750 is published as $25,700. + """ + assert get_ar_cost_of_living_adjustment(system.parameters, year) == 0.03 + previous = card_bounds(year - 1)[:5] + published = card_bounds(year)[:5] + predicted = [round_ar_income_tax_bound(bound * 1.03, 100) for bound in previous] + assert predicted == published diff --git a/policyengine_us/tests/test_ar_income_tax_schedule.py b/policyengine_us/tests/test_ar_income_tax_schedule.py index 342b24029c2..8229c96f8a6 100644 --- a/policyengine_us/tests/test_ar_income_tax_schedule.py +++ b/policyengine_us/tests/test_ar_income_tax_schedule.py @@ -25,19 +25,21 @@ Rows from the top of the flat rate bracket upward form a $100 ladder whose minus adjustment falls $10 per row. Asterisked rows span $200, so the next row's start is skipped. -- 2026: Act 1 of 2026 (1st Extraordinary Session), A.C.A. 26-51-201(a)(4). -- 2027-2035: Act 1 of 2026's tables with every bracket bound indexed from - 2026 by Arkansas's capped CPI-U index and rounded to the nearest $100. +- 2026-2035: Act 1 of 2026's (1st Extraordinary Session) A.C.A. + 26-51-201(a)(4) tables with every bracket bound indexed by Arkansas's + capped CPI-U index from 2026, the tables' first year (as DFA indexed each + new table in its first year), and rounded to the nearest $100 with exact + $50 ties down (as DFA's 2022 and 2024 cards did). Invariants: - reduction.yaml rows carry strictly increasing finite thresholds, with every .inf row after them (published years). - At every integer taxable income, tax is non-negative and never falls as income rises (2021-2040, 2050, 2075, 2100), except where the statute's own - (A) and (B)-(C) tables meet. In 2026 that seam drops: (a)(4)(A) taxes - $94,700 at $3,136.70, while (a)(4)(B) less the $290 (a)(4)(C) adjustment - taxes $94,701 at $3,134.04. Once indexing raises the (A) bounds, the seam - rises instead. + (A) and (B)-(C) tables meet. In Act 1's unindexed tables that seam drops: + (a)(4)(A) taxes $94,700 at $3,136.70, while (a)(4)(B) less the $290 + (a)(4)(C) adjustment taxes $94,701 at $3,134.04. Indexing moves the seam + to the indexed threshold, the only place tax may fall. - The (C) ladder starts at the high-income threshold, its bounds strictly increase, and its amounts stay Act 1's $290-$10 steps; the (B) top rate equals the (A) top rate (2026-2040, 2050, 2075, 2100). @@ -188,14 +190,19 @@ def act_1_of_2026_tax(ti): def nearest_100(amount): # 26-51-201(d)(1): "rounding to the nearest one hundred dollars ($100)". - # Preserve Core's existing ties-to-even convention for exact $50 ties; - # the statute does not specify a tie-breaking rule. - return round(amount / 100) * 100 + # The statute names no tie rule; DFA rounded both observed exact $50 + # ties down (2022: $5,150 to $5,100; 2024: $25,750 to $25,700). + hundreds = round(amount / 100, 6) + return (np.floor(hundreds) + (hundreds - np.floor(hundreds) > 0.5)) * 100 def indexed_act_1_of_2026_tax(year, ti): """Act 1 of 2026's tables, every bracket bound indexed from 2026.""" - factor = UPRATING(f"{year}-01-01") / UPRATING("2026-01-01") + return act_1_of_2026_tax_at(UPRATING(f"{year}-01-01") / UPRATING("2025-01-01"), ti) + + +def act_1_of_2026_tax_at(factor, ti): + """Act 1 of 2026's tables with every bracket bound times ``factor``.""" starts = [nearest_100(start * factor) for start, _ in ACT_1_TABLE_A] ends = starts[1:] + [np.inf] rates = [rate for _, rate in ACT_1_TABLE_A] @@ -223,8 +230,6 @@ def model_tax(year, ti, parameters=P): def source_tax(year, ti): if year in PUBLISHED_YEARS: return card_tax(year, ti) - if year == 2026: - return act_1_of_2026_tax(ti) return indexed_act_1_of_2026_tax(year, ti) @@ -238,10 +243,8 @@ def test_ar_schedule_matches_source_at_every_integer_income(year): ) -def test_ar_indexed_reference_is_act_1_of_2026_in_2026(): - diff = indexed_act_1_of_2026_tax(2026, TAXABLE_INCOME) - act_1_of_2026_tax( - TAXABLE_INCOME - ) +def test_ar_indexed_reference_with_no_increase_is_act_1_of_2026(): + diff = act_1_of_2026_tax_at(1, TAXABLE_INCOME) - act_1_of_2026_tax(TAXABLE_INCOME) assert np.abs(diff).max() < 1e-6 @@ -267,7 +270,7 @@ def test_ar_schedule_before_modeled_years_still_computes(year): # $100,000 at $3,810.70, and its "$100,001 and over" rule taxes $100,001 at # $3,809.04. (The card's table is computed at row midpoints, where the # 99,901-100,001 row is $3,809; the model uses exact incomes.) -STATUTORY_DROPS = {2025: {100_000: -1.66}, 2026: {94_700: -2.66}} +STATUTORY_DROPS = {2025: {100_000: -1.66}} def invariant_grid(year): diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 1cea80fc061..3de62a49646 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -9773,6 +9773,40 @@ candidates: note: 'PE-US keeps WV out of requires_income_below_limit, so income equal to the SSI rate qualifies. This assumes the group is the 42 CFR 435.210 group of section 1902(a)(10)(A)(ii)(I); West Virginia''s state plan page for the group was not checked. The archived 2013 manual pairs the same two wordings (§ 16.7.H and § 10.23.C).' +- id: us-wv:senior-modification-529-contribution + program: income-tax + years: + from: 2014 + type: tax + question: Does a 529 or prepaid tuition contribution deducted under W. Va. Code 11-21-12a reduce the income base of + the senior or disability modification under 11-21-12(c)(9)? + sources: + - kind: statute + citation: W. Va. Code 11-21-12a + url: https://code.wvlegislature.gov/11-21-12a/ + - kind: statute + citation: W. Va. Code 11-21-12(c)(9) + url: https://code.wvlegislature.gov/11-21-12/ + - kind: form + citation: 2025 Schedule M, line 39 and line 47 box (c), PDF p. 6 + url: https://tax.wv.gov/Documents/PIT/2025/it140.PersonalIncomeTaxFormsAndInstructions.2025.pdf#page=6 + readings: + statute: Section 12a's deduction is in addition to the subsection (c) modifications, and (c)(9)(ii) limits the + senior modification only by the modifications under (c)(1), (2) and (5)-(8), so a contribution does not reduce it. + other: Schedule M box (c) counts income not included in lines 35 to 46, and line 39 is the 529 contribution, so the + form reads as excluding it. + pe_us: + follows: statute + code: + - policyengine_us/variables/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.py + - policyengine_us/parameters/gov/states/wv/tax/income/subtractions/senior_citizen_disability_deduction/excluded_income_sources.yaml + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10324 + state: ruled + note: 'PolicyEngine follows the statute for line 39: a contribution is a payment, not income in federal AGI, so the + 12a deduction and the senior modification cannot cover the same dollars. Split from the registered + us-wv:senior-disability-modification-income-base, whose reading still lists lines 35 to 46 as a whole; that + register entry needs the same change in rulespec-us.' - id: us-wv:senior-modification-excluded-line-range program: income-tax years: @@ -9799,7 +9833,8 @@ candidates: - https://github.com/PolicyEngine/policyengine-us/pull/10082 state: unreviewed note: 'PE-US follows the form and worked example: box (c) excludes the person''s own amounts on 2025 Schedule - M lines 35-46, and the list holds lines 36, 38 and 39 (wv_senior_citizen_disability_deduction_person.py, excluded_income_sources.yaml). + M lines 35-46, and from 2014 the list holds lines 36 and 38; the line 39 529 contribution is not excluded (see + us-wv:senior-modification-529-contribution) (wv_senior_citizen_disability_deduction_person.py, excluded_income_sources.yaml). Verify the stale range and resulting amount difference.' - id: us-wv:zero-exemption-return-500-allowance program: income-tax From 28167c7b1dfc1b5d0a653591b1b3fb2b3324adcb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 08:26:31 +0000 Subject: [PATCH 10/13] Pin a WV senior modification that the 529 change moves at the cap Age 66, AGI 9,000 and a 2,000 contribution give 8,000; excluding the contribution from the base gave 7,000. Co-Authored-By: Claude Opus 5.5 --- ...v_senior_citizen_disability_deduction_person.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.yaml index 4a5445d7194..e535592a719 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wv/tax/income/subtractions/senior_citizen_disability/wv_senior_citizen_disability_deduction_person.yaml @@ -336,3 +336,15 @@ wv_subtractions: 8_000 wv_agi: 38_000 wv_taxable_income: 34_000 + +- name: A 529 contribution no longer lowers a senior modification at the cap. + period: 2025 + input: + age: 66 + adjusted_gross_income_person: 9_000 + investment_in_529_plan_indv: 2_000 + state_code: WV + output: + # min(9,000, 8,000) = 8,000. Excluding the contribution from the base, as + # before, gave min(9,000 - 2,000, 8,000) = 7,000. + wv_senior_citizen_disability_deduction_person: 8_000 From 74f16b9eaa1d87d4cd1cecf9bcfd458ee4d3d018 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 5 Nov 2026 10:57:26 -0500 Subject: [PATCH 11/13] Round Arkansas income tax bounds after each annual adjustment Add the five 2025 bounds from BLS observations and DFA brackets. Record the first-year and tie readings, and retain the WV register split for the shared-register update. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/ar/tax/income/uprating.yaml | 14 +-- .../parameters/uprating_extensions.py | 32 ++++--- .../core/test_state_uprating_rounding.py | 16 +++- .../tests/test_ar_income_tax_indexing.py | 94 ++++++++++++++++--- .../tests/test_ar_income_tax_schedule.py | 59 ++++++++---- source_conflicts/candidates.yaml | 84 ++++++++++++++++- 6 files changed, 241 insertions(+), 58 deletions(-) diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml index cd904c6155f..b6e089de761 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml @@ -1,9 +1,11 @@ -description: Arkansas uses this index to adjust its income tax bracket boundaries for annual increases in the Consumer Price Index for All Urban Consumers, capped at three percent. +description: Arkansas tracks cumulative unrounded CPI-U adjustments in this index, with each annual increase capped at three percent. Bound metadata identifies the indexed parameters; the extension adjusts and rounds each preceding bound annually. values: - # 1 through 2025, so the tax year 2026 statutory tables are the base. - # extend_ar_income_tax_uprating compounds each year's adjustment from 2026: - # A.C.A. 26-51-201(a)(5) indexes the Act 1 of 2026 tables under (d), and - # DFA indexed each newly enacted table in its first year (2022-2024 cards). + # PolicyEngine selects 2026 as the first adjustment year under candidate + # us-ar:first-indexed-year-after-act-1-2026; (a)(5) and (d) name no first year. + # DFA indexed each new table in its 2022-2024 cards, but lists unindexed + # bounds in its January 2026 withholding formula, before August CPI. + # We follow the cards' timing pending DFA's 2026 card. The index compounds + # unrounded adjustments; each bound separately rounds every annual step. 2014-01-01: 1 metadata: unit: /1 @@ -12,7 +14,7 @@ metadata: reference: - title: A.C.A. § 26-51-201(d)(2), Act 1459 of 2013, § 2 (CPI-U average for twelve months ending August 31 of the current calendar year, positive increase capped at three percent) href: https://arkleg.state.ar.us/Acts/FTPDocument?path=%2FACTS%2F2013%2FPublic%2F&file=1459.pdf&ddBienniumSession=2013%2F2013R#page=3 - - title: A.C.A. § 26-51-201(a)(5), added by Act 1 of 2026 (1st Extraordinary Session), § 1 (the (a)(1)-(4) tables are indexed under (d)) + - title: A.C.A. § 26-51-201(a)(5), as amended by Act 1 of 2026 (1st Extraordinary Session), § 1 (the (a)(1)-(4) tables are indexed under (d)) href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=3 - title: 2022 Indexed Tax Brackets href: https://www.dfa.arkansas.gov/wp-content/uploads/TaxBrackets_2022.pdf#page=1 diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 899335fbcc5..537212f3f24 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -101,10 +101,12 @@ def get_ar_cpi(parameters: ParameterNode, calendar_year: int) -> float: AR_INCOME_TAX_INDEX = "gov.states.ar.tax.income.uprating" -# Act 1 of 2026 (1st Extraordinary Session) enacts the tax year 2026 tables, -# and A.C.A. 26-51-201(a)(5) indexes them under (d). DFA indexed each newly -# enacted table in its first year (the 2022, 2023 and 2024 cards), so the -# 2026 tables are the base and 2026 is the first indexed year. +# PolicyEngine selects 2026 as the first adjustment year under +# us-ar:first-indexed-year-after-act-1-2026. Neither (a)(5) nor (d) names it. +# DFA indexed newly enacted tables in its 2022-2024 cards, but lists unindexed +# bounds in its withholding formula effective January 2026, before the +# adjustment's August 2026 CPI window ends. We follow the cards' timing +# pending DFA's 2026 card. AR_FIRST_INDEXED_YEAR = 2026 @@ -139,14 +141,15 @@ def round_ar_income_tax_bound(amount: float, interval: float) -> float: def extend_ar_income_tax_uprating(parameters: ParameterNode, end_year: int) -> None: """Build Arkansas's income tax index and index its bracket bounds. - The index is 1 before AR_FIRST_INDEXED_YEAR and compounds each year's - cost-of-living adjustment from then on. The statute's "if any" language - prevents a decrease. + The diagnostic index is 1 before AR_FIRST_INDEXED_YEAR and compounds + each year's unrounded cost-of-living adjustment from then on. Bound + metadata identifies the parameters this extension adjusts; the bounds + do not multiply their statutory bases by that cumulative index. Each bound whose uprating metadata names this index is then set, for - every year from AR_FIRST_INDEXED_YEAR to ``end_year``, to its tax year - 2026 statutory value times the index, rounded by - round_ar_income_tax_bound to its metadata's interval. Core's own + every year from AR_FIRST_INDEXED_YEAR to ``end_year``, by adjusting the + preceding rounded bound and rounding again to its metadata's interval. + The first adjustment starts from the 2026 statutory value. Core's own rounding would send exact ties to even, so the bounds are written here, and Core's uprating, which starts after a parameter's last value, then adds nothing. Bracket-adjustment amounts are not bounds and stay fixed. @@ -173,12 +176,15 @@ def extend_ar_income_tax_uprating(parameters: ParameterNode, end_year: int) -> N interval = meta["rounding"]["interval"] base = parameter(base_instant) starts_at_base = parameter.values_list[-1].instant_str >= base_instant + bound = base for year in range(AR_FIRST_INDEXED_YEAR, end_year + 1): + bound = round_ar_income_tax_bound( + bound * (1 + get_ar_cost_of_living_adjustment(parameters, year)), + interval, + ) parameter.update( start=instant(f"{year}-01-01"), - value=round_ar_income_tax_bound( - base * index(f"{year}-01-01"), interval - ), + value=bound, ) if starts_at_base: # Backdating copies a parameter's earliest value to earlier diff --git a/policyengine_us/tests/core/test_state_uprating_rounding.py b/policyengine_us/tests/core/test_state_uprating_rounding.py index ddc9d79d957..5a72bd0f4b3 100644 --- a/policyengine_us/tests/core/test_state_uprating_rounding.py +++ b/policyengine_us/tests/core/test_state_uprating_rounding.py @@ -22,8 +22,8 @@ def _nearest_100_half_down(amount): def test_ar_income_tax_bounds_round_to_nearest_100_from_2026(): # A.C.A. 26-51-201(d)(1): bracket amounts are indexed "rounding to the # nearest one hundred dollars ($100)"; DFA rounded exact $50 ties down. - # From 2026 each bound is Act 1 of 2026's value times the loaded index, - # so a CPI refresh moves the expected values without breaking the test. + # PolicyEngine selects 2026 for the first adjustment. Each annual step + # adjusts the preceding rounded bound, starting from Act 1's tables. main = SYSTEM.parameters.gov.states.ar.tax.income.rates.main uprating = SYSTEM.parameters.gov.states.ar.tax.income.uprating high_income = main.high_income @@ -37,19 +37,25 @@ def test_ar_income_tax_bounds_round_to_nearest_100_from_2026(): tuple(range(94_700, 97_601, 100)), ), } + previous = {name: bases for name, (_, _, bases) in act_1_of_2026.items()} differs_from_rounding_down = False for year in range(2026, 2036): period = f"{year}-01-01" - factor = uprating(period) / uprating("2025-01-01") + factor = uprating(period) / uprating(f"{year - 1}-01-01") for name, (scale, indexes, bases) in act_1_of_2026.items(): if scale is None: loaded = (high_income.threshold(period),) else: loaded = _thresholds(scale, period, indexes) - expected = tuple(_nearest_100_half_down(base * factor) for base in bases) + expected = tuple( + _nearest_100_half_down(bound * factor) for bound in previous[name] + ) assert loaded == expected, (year, name) - rounded_down = tuple(base * factor // 100 * 100 for base in bases) + rounded_down = tuple( + bound * factor // 100 * 100 for bound in previous[name] + ) differs_from_rounding_down |= loaded != rounded_down + previous[name] = expected assert isinf(main.rate.brackets[5].threshold(period)) assert isinf(main.rate.brackets[6].threshold(period)) # In 2026, 3% takes $5,600 to $5,768, rounded to $5,800. diff --git a/policyengine_us/tests/test_ar_income_tax_indexing.py b/policyengine_us/tests/test_ar_income_tax_indexing.py index 55f4e290bb0..862081acb54 100644 --- a/policyengine_us/tests/test_ar_income_tax_indexing.py +++ b/policyengine_us/tests/test_ar_income_tax_indexing.py @@ -67,6 +67,53 @@ def synthetic_parameters(annual_levels, bounds=None): ) +@pytest.fixture +def ar_2025_nsa_parameters(): + """Keep the 2025 back-test's two NSA windows in a separate tiny tree.""" + # BLS series CUUR0000SA0, September 2023 through August 2025: + # https://api.bls.gov/publicAPI/v2/timeseries/data/CUUR0000SA0?startyear=2023&endyear=2025 + monthly = { + "2023-09-01": 307.789, + "2023-10-01": 307.671, + "2023-11-01": 307.051, + "2023-12-01": 306.746, + "2024-01-01": 308.417, + "2024-02-01": 310.326, + "2024-03-01": 312.332, + "2024-04-01": 313.548, + "2024-05-01": 314.069, + "2024-06-01": 314.175, + "2024-07-01": 314.540, + "2024-08-01": 314.796, + "2024-09-01": 315.301, + "2024-10-01": 315.664, + "2024-11-01": 315.493, + "2024-12-01": 315.605, + "2025-01-01": 317.671, + "2025-02-01": 319.082, + "2025-03-01": 319.799, + "2025-04-01": 320.795, + "2025-05-01": 321.465, + "2025-06-01": 322.561, + "2025-07-01": 323.048, + "2025-08-01": 323.976, + } + return ParameterNode( + data={ + "gov": { + "bls": {"cpi": {"cpi_u": {"values": monthly}}}, + "states": { + "ar": { + "tax": { + "income": {"uprating_cap": {"values": {"2014-01-01": 0.03}}} + } + } + }, + } + } + ) + + def test_ar_cpi_averages_september_through_august(): parameters = synthetic_parameters((100, 100)) cpi = parameters.gov.bls.cpi.cpi_u @@ -131,7 +178,7 @@ def test_ar_bounds_round_to_nearest_100_with_ties_down(amount, expected): assert round_ar_income_tax_bound(amount, 100) == expected -def test_ar_index_rounds_each_bound_from_its_2026_value(): +def test_ar_index_rounds_each_bound_from_the_preceding_rounded_bound(): parameters = synthetic_parameters( (100, 104, 106.08), bounds={"tie": 25_000, "row": 5_600, "threshold": 94_700, "top": 4_700}, @@ -140,16 +187,23 @@ def test_ar_index_rounds_each_bound_from_its_2026_value(): extend_ar_income_tax_uprating(parameters, end_year=2028) main = parameters.gov.states.ar.tax.income.rates.main - # 2026, index 1.03: 25,750 (tie, down), 5,768, 97,541 and 4,841. + # 2026: 25,000 x 1.03 = 25,750 -> 25,700 (tie down); + # 5,600 x 1.03 = 5,768 -> 5,800; 94,700 x 1.03 = 97,541 -> 97,500; + # 4,700 x 1.03 = 4,841 -> 4,800. assert main.tie("2026-01-01") == 25_700 assert main.row("2026-01-01") == 5_800 assert main.threshold("2026-01-01") == 97_500 assert main.top("2026-01-01") == 4_800 - # 2027, index 1.0506: 26,265, 5,883.36, 99,491.82 and 4,937.82. - assert main.tie("2027-01-01") == 26_300 + # 2027: 25,700 x 1.02 = 26,214 -> 26,200; + # 5,800 x 1.02 = 5,916 -> 5,900; 97,500 x 1.02 = 99,450 -> 99,400 + # (tie down); 4,800 x 1.02 = 4,896 -> 4,900. + assert main.tie("2027-01-01") == 26_200 assert main.row("2027-01-01") == 5_900 - assert main.threshold("2027-01-01") == 99_500 + assert main.threshold("2027-01-01") == 99_400 assert main.top("2027-01-01") == 4_900 + for name in ("tie", "row", "threshold", "top"): + bound = main.children[name] + assert bound("2028-01-01") == bound("2027-01-01") # Earlier years keep the statutory value, whether or not the parameter # has a history before 2026. assert main.tie("2025-01-01") == 25_000 @@ -164,6 +218,8 @@ def test_ar_2026_tables_index_act_1_and_keep_bracket_adjustments_fixed(): main = income.rates.main # The model's CPI-U gives 3.06% for 2026, capped at 3%. assert income.uprating("2026-01-01") == pytest.approx(1.03) + # PolicyEngine selects 2026 for the first adjustment, following the + # recorded reading and DFA's 2022-2024 cards. # Act 1 of 2026's $5,600, $11,200, $16,000 and $26,400 row starts, # $94,700 threshold and $4,700 (B) bound, each x 1.03 to the nearest $100. assert [b.threshold("2026-01-01") for b in main.rate.brackets[1:5]] == [ @@ -200,22 +256,34 @@ def card_bounds(year): return [t - 1 if t % 100 == 1 else t for t in finite] -@pytest.mark.parametrize("year", (2018, 2019, 2020, 2021)) -def test_ar_window_reproduces_dfa_cards(year): - """Each card's bounds index to the next card's, all 11 of them. +@pytest.mark.parametrize("year", (2018, 2019, 2020, 2021, 2025)) +def test_ar_window_reproduces_dfa_cards(year, ar_2025_nsa_parameters): + """Index all 49 distinguishing bounds to their next published card. The repository's CPI-U is the not seasonally adjusted series before 2024, so its windows for 2018-2021 are the statute's: 2.4193%, 1.9000%, 1.4583% and 2.9977%. The window for tax year Y-1 (the rule Act 1459 struck) gives 1.9874%, 2.4193%, 1.9000% and 1.4583%, and misses most of - these bounds. The 2025 card is not tested here: from 2024 the series is - seasonally adjusted, and across that splice the 2025 window computes to - 2.7265%, not the not seasonally adjusted 2.6531%. + these bounds. The 2025 case uses the tiny NSA tree because the shared + CPI-U series uses seasonally adjusted observations from 2024. """ - adjustment = get_ar_cost_of_living_adjustment(system.parameters, year) + parameters = ar_2025_nsa_parameters if year == 2025 else system.parameters + adjustment = get_ar_cost_of_living_adjustment(parameters, year) + count = 5 if year == 2025 else 11 previous = card_bounds(year - 1) published = card_bounds(year) - assert len(previous) == len(published) == 11 + if year == 2025: + previous = previous[:5] + published = published[:5] + assert len(previous) == len(published) == count + if year == 2025: + # 3,731.460 / 12 = 310.955; 3,830.460 / 12 = 319.205. + # 319.205 / 310.955 = 1.026531170105, below the 1.03 cap. + assert get_ar_cpi(parameters, 2024) == pytest.approx(310.955) + assert get_ar_cpi(parameters, 2025) == pytest.approx(319.205) + # DFA's 2025 Indexed Tax Brackets card: + # https://www.dfa.arkansas.gov/wp-content/uploads/2025_TaxBrackets.pdf + assert published == [5_600, 11_200, 16_000, 26_400, 94_700] predicted = [ round_ar_income_tax_bound(bound * (1 + adjustment), 100) for bound in previous ] diff --git a/policyengine_us/tests/test_ar_income_tax_schedule.py b/policyengine_us/tests/test_ar_income_tax_schedule.py index 8229c96f8a6..b198769863a 100644 --- a/policyengine_us/tests/test_ar_income_tax_schedule.py +++ b/policyengine_us/tests/test_ar_income_tax_schedule.py @@ -11,11 +11,11 @@ threshold) silently shifts every later bracket. - Later years apply the statutory tables of A.C.A. 26-51-201(a)(4) (Act 1 of 2026): (A) marginal rates up to the high-income threshold, and above it the - (B) marginal rates less the (C) bracket adjustment. 26-51-201(a)(5) and - (d)(1) index every bracket bound each year, "rounding to the nearest one - hundred dollars ($100)"; the (C) dollar amounts are not bracket bounds and - stay fixed. The reduction is derived from the same indexed tables, so it - moves with the thresholds (a stored reduction would not). + (B) marginal rates less the (C) bracket adjustment. Under + 26-51-201(a)(5) and (d)(1), the secretary adjusts every bracket bound each + year and rounds it to the nearest $100. The (C) dollar amounts stay fixed. + The reduction is derived from the same indexed tables, so it moves with + the thresholds (a stored reduction would not). Differential tests compare every integer taxable income against an independent transcription of each year's source: @@ -26,10 +26,11 @@ minus adjustment falls $10 per row. Asterisked rows span $200, so the next row's start is skipped. - 2026-2035: Act 1 of 2026's (1st Extraordinary Session) A.C.A. - 26-51-201(a)(4) tables with every bracket bound indexed by Arkansas's - capped CPI-U index from 2026, the tables' first year (as DFA indexed each - new table in its first year), and rounded to the nearest $100 with exact - $50 ties down (as DFA's 2022 and 2024 cards did). + 26-51-201(a)(4) tables with each preceding rounded bound adjusted annually + by Arkansas's capped CPI-U increase. PolicyEngine selects 2026 as the + first adjustment year, following its recorded reading and DFA's + 2022-2024 cards. DFA's 2026 withholding formula retains unindexed bounds. + DFA rounded exact $50 ties down in its 2022 and 2024 cards. Invariants: - reduction.yaml rows carry strictly increasing finite thresholds, with every @@ -68,7 +69,8 @@ ) P = system.parameters.gov.states.ar.tax.income.rates.main -UPRATING = system.parameters.gov.states.ar.tax.income.uprating +CPI_U = system.parameters.gov.bls.cpi.cpi_u +UPRATING_CAP = system.parameters.gov.states.ar.tax.income.uprating_cap PUBLISHED_YEARS = range(2021, 2026) SOURCE_YEARS = range(2021, 2036) LONG_RUN_YEARS = [2050, 2075, 2100] @@ -197,29 +199,52 @@ def nearest_100(amount): def indexed_act_1_of_2026_tax(year, ti): - """Act 1 of 2026's tables, every bracket bound indexed from 2026.""" - return act_1_of_2026_tax_at(UPRATING(f"{year}-01-01") / UPRATING("2025-01-01"), ti) + """Adjust preceding rounded bounds annually, first in 2026.""" + + def calendar_cpi(calendar_year): + months = [(calendar_year - 1, month) for month in range(9, 13)] + [ + (calendar_year, month) for month in range(1, 9) + ] + return np.mean([CPI_U(f"{year}-{month:02d}-01") for year, month in months]) + + annual_factors = [ + 1 + + min( + UPRATING_CAP(f"{adjustment_year}-01-01"), + max( + calendar_cpi(adjustment_year) / calendar_cpi(adjustment_year - 1) - 1, 0 + ), + ) + for adjustment_year in range(2026, year + 1) + ] + return act_1_of_2026_tax_at(annual_factors, ti) def act_1_of_2026_tax_at(factor, ti): - """Act 1 of 2026's tables with every bracket bound times ``factor``.""" - starts = [nearest_100(start * factor) for start, _ in ACT_1_TABLE_A] + """Apply one factor or a sequence of annual factors to each bound.""" + + def adjusted_bound(bound): + for annual_factor in np.atleast_1d(factor): + bound = nearest_100(bound * annual_factor) + return bound + + starts = [adjusted_bound(start) for start, _ in ACT_1_TABLE_A] ends = starts[1:] + [np.inf] rates = [rate for _, rate in ACT_1_TABLE_A] table_a = sum( rate * np.clip(ti - start, 0, end - start) for start, end, rate in zip(starts, ends, rates) ) - top_2_percent = nearest_100(ACT_1_TABLE_B_2_PERCENT_TOP * factor) + top_2_percent = adjusted_bound(ACT_1_TABLE_B_2_PERCENT_TOP) table_b = 0.02 * np.minimum(ti, top_2_percent) + 0.037 * np.maximum( ti - top_2_percent, 0 ) - bounds = np.array([nearest_100(bound * factor) for bound, _ in ACT_1_LADDER]) + bounds = np.array([adjusted_bound(bound) for bound, _ in ACT_1_LADDER]) amounts = np.array([amount for _, amount in ACT_1_LADDER]) # Row k covers income above bounds[k] up to and including bounds[k + 1]. row = np.searchsorted(bounds, ti, side="left") - 1 adjustment = np.where(row >= 0, amounts[np.maximum(row, 0)], 0) - threshold = nearest_100(ACT_1_THRESHOLD * factor) + threshold = adjusted_bound(ACT_1_THRESHOLD) return np.where(ti <= threshold, table_a, table_b - adjustment) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 52534d10798..e6a32c07665 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -451,6 +451,37 @@ candidates: note: 'PE-US follows the administrative code: Alabama''s age limit is 19 from 2014 with no student condition, and the file''s comment records the S25 conflict (age_limit.yaml). Verify overlapping effective dates and any conflict before the cited 2023 code, since the model applies 19 from 2014.' +- id: us-ar:bound-rounding-ties-down + program: income-tax + years: + from: 2026 + type: tax + question: Does the secretary round an exact $50 income tax bracket tie down or up to the nearest $100? + sources: + - kind: statute + citation: A.C.A. 26-51-201(d)(1), as amended by Act 532 of 2023, section 2 + url: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2023R%2FPublic%2FACT532.pdf#page=4 + - kind: agency-practice + citation: Arkansas DFA 2022 Indexed Tax Brackets + url: https://www.dfa.arkansas.gov/wp-content/uploads/TaxBrackets_2022.pdf#page=1 + - kind: agency-practice + citation: Arkansas DFA 2024 Indexed Tax Brackets + url: https://www.dfa.arkansas.gov/wp-content/uploads/2024_TaxBrackets.pdf#page=1 + readings: + statute: The General Assembly requires the secretary to round each bracket bound to the nearest $100 but + prescribes no direction for an exact $50 tie. + other: DFA rounds exact ties down, publishing $5,100 after $5,000 times 1.03 gives $5,150 and $25,700 after + $25,000 times 1.03 gives $25,750. + example: Rounding $25,750 down gives $25,700; rounding the tie up or to even gives $25,800. + pe_us: + follows: gap-fill + code: + - policyengine_us/parameters/uprating_extensions.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10324 + state: ruled + note: PolicyEngine follows DFA's tie-down practice where the General Assembly leaves the rounding direction + unspecified. The hub will add the ruling and this PR's implementation to the shared register after landing. - id: us-ar:casualty-loss-aggregate-agi-floor program: income-tax years: @@ -579,6 +610,49 @@ candidates: state: ruled note: 'The October 2026 ruling leaves small repayments unmodelled; verify whether the fiscal notes establish a post-2013 remedy. Code not yet on main: ar_claim_of_right_deduction.py.' +- id: us-ar:first-indexed-year-after-act-1-2026 + program: income-tax + years: + from: 2026 + type: tax + question: Does the secretary first index Act 1 of 2026's individual income-tax tables in 2026 or 2027? + sources: + - kind: statute + citation: A.C.A. 26-51-201(a)(5), as amended by Act 1 of 2026, section 1 + url: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=3 + - kind: statute + citation: A.C.A. 26-51-201(d)(1), as amended by Act 532 of 2023, section 2 + url: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2023R%2FPublic%2FACT532.pdf#page=4 + - kind: agency-practice + citation: Arkansas DFA 2022 Indexed Tax Brackets + url: https://www.dfa.arkansas.gov/wp-content/uploads/TaxBrackets_2022.pdf#page=1 + - kind: agency-practice + citation: Arkansas DFA 2023 Indexed Tax Brackets + url: https://www.dfa.arkansas.gov/wp-content/uploads/TaxBrackets_2023.pdf#page=1 + - kind: agency-practice + citation: Arkansas DFA 2024 Indexed Tax Brackets + url: https://www.dfa.arkansas.gov/wp-content/uploads/2024_TaxBrackets.pdf#page=1 + - kind: guidance + citation: Arkansas DFA Withholding Tax Formula, effective January 1, 2026 + url: https://www.dfa.arkansas.gov/wp-content/uploads/Withholding-Tax-Formula.pdf#page=1 + readings: + statute: The secretary first adjusts the newly enacted tables in 2026 under (a)(5) and (d)(1), following + DFA's practice of indexing newly enacted tables in 2022, 2023 and 2024. + other: The secretary first adjusts the tables in 2027, retaining the unindexed $5,600 and $94,700 bounds + that DFA uses in its withholding formula effective January 1, 2026. + pe_us: + follows: gap-fill + code: + - policyengine_us/parameters/uprating_extensions.py + - policyengine_us/parameters/gov/states/ar/tax/income/uprating.yaml + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10324 + state: ruled + note: PolicyEngine selects 2026 as the first indexed year under the hub's reading of (a)(5) and (d)(1). + The General Assembly requires annual adjustment but names no first year. DFA indexed newly enacted tables + in 2022 through 2024. DFA's withholding formula effective January 1, 2026 uses unindexed bounds, but the + secretary cannot compute the 2026 adjustment until CPI through August 2026 becomes available. PolicyEngine + follows the cards until DFA publishes a 2026 indexed card. The hub will register this ruling after landing. - id: us-ar:gambling-loss-deduction-before-2015 program: income-tax years: @@ -10663,10 +10737,12 @@ candidates: prs: - https://github.com/PolicyEngine/policyengine-us/pull/10324 state: ruled - note: 'PolicyEngine follows the statute for line 39: a contribution is a payment, not income in federal AGI, so the - 12a deduction and the senior modification cannot cover the same dollars. Split from the registered - us-wv:senior-disability-modification-income-base, whose reading still lists lines 35 to 46 as a whole; that - register entry needs the same change in rulespec-us.' + note: 'PolicyEngine follows the statute for line 39. PolicyEngine keeps the section 12a contribution deduction + separate from exclusions of income receipts before applying the senior cap. The registered + us-wv:senior-disability-modification-income-base reading still excludes all Schedule M lines 35 to 46, + including line 39. This PR excludes income receipts on lines 36 and 38 and retains the separate contribution deduction. + The hub will update the shared register after landing. This PR keeps the existing marker, vendored register + and pin.' - id: us-wv:senior-modification-excluded-line-range program: income-tax years: From 5cc1dfb8b907d3b80466c658cb62023f5a88126f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 5 Nov 2026 11:27:23 -0500 Subject: [PATCH 12/13] Clarify the unindexed Arkansas table boundary Co-Authored-By: Claude Opus 5.5 --- .../gov/states/ar/tax/income/ar_income_tax_helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_helpers.py b/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_helpers.py index d028bdaf3fb..36ce0433114 100644 --- a/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_helpers.py +++ b/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_helpers.py @@ -19,8 +19,9 @@ def ar_main_income_tax(taxable_income, p): - (C) less the fixed bracket adjustment for the row the income falls in. Taxing at marginal rates keeps tax continuous at every (A) and (B) row bound wherever indexing moves them; a stored reduction would not move with them. - As in the statute, tax still steps where (A) meets (B)-(C) (down $2.66 at - 2026's $94,700) and rises by the $10 adjustment step at each (C) row bound. + Before indexing, filers pay $2.66 less when income crosses the statutory + $94,700 boundary between (A) and (B)-(C). They pay the $10 adjustment step + at each (C) row bound. """ if p.use_published_reduction: rate = p.rate.calc(taxable_income) From 812a055b9a6099b76fa3577ec29bbf37470df6c6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 5 Nov 2026 16:38:13 -0500 Subject: [PATCH 13/13] Correct Arkansas's Act 1 amendment reference Co-Authored-By: Claude Opus 5.5 --- .../parameters/gov/states/ar/tax/income/rates/main/rate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml index 8c53dc2aad4..aa522e3a6a5 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml @@ -52,7 +52,7 @@ metadata: href: https://arkleg.state.ar.us/Bills/Detail?id=hb1001&ddBienniumSession=2025%2F2026S1 - title: A.C.A. § 26-51-201(d)(1), as amended by Act 532 of 2023, § 2 (bracket amounts indexed annually, rounded to the nearest $100) href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2023R%2FPublic%2FACT532.pdf#page=4 - - title: A.C.A. § 26-51-201(a)(5), added by Act 1 of 2026 (1st Extraordinary Session), § 1 (the (a)(1)-(4) tables are indexed under (d)) + - title: A.C.A. § 26-51-201(a)(5), as amended by Act 1 of 2026 (1st Extraordinary Session), § 1 (the (a)(1)-(4) tables are indexed under (d)) href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=3 brackets: