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/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/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/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/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..49191846a24 --- /dev/null +++ b/policyengine_us/tests/fixtures/ar_indexing.py @@ -0,0 +1,53 @@ +"""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_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..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 @@ -797,3 +807,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/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 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..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 @@ -247,3 +247,328 @@ 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 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 + 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 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 + 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 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..342b24029c2 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] @@ -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): 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) 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.