From 3f4488d004c8dc82b64f8bc4587ee3be17f41406 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 19:26:03 -0400 Subject: [PATCH 1/7] Leave dependents' unemployment compensation off CA, RI and WI filer returns Federal AGI leaves out a tax unit dependent's income (irs_gross_income masks dependents), but California's AGI subtractions, Rhode Island's AGI additions and Wisconsin's unemployment compensation subtraction summed person-level unemployment compensation over every tax unit member, so a dependent's compensation moved the filers' state AGI. Sum only the head and spouse with tax_unit_non_dep_add. Co-Authored-By: Claude Opus 5.5 --- changelog.d/dependent-uc-state-agi.fixed.md | 1 + .../gov/states/ca/tax/income/ca_agi_subtractions.py | 6 +++++- .../gov/states/ri/tax/income/agi/ri_additions.py | 9 ++++++++- .../wi_unemployment_compensation_subtraction.py | 7 ++++++- 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 changelog.d/dependent-uc-state-agi.fixed.md diff --git a/changelog.d/dependent-uc-state-agi.fixed.md b/changelog.d/dependent-uc-state-agi.fixed.md new file mode 100644 index 00000000000..98b0973f05d --- /dev/null +++ b/changelog.d/dependent-uc-state-agi.fixed.md @@ -0,0 +1 @@ +Leave a tax unit dependent's unemployment compensation off the filers' California AGI subtractions, Rhode Island AGI additions and Wisconsin unemployment compensation subtraction. diff --git a/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py b/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py index f1500e86a22..e7bd1a59892 100644 --- a/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py +++ b/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py @@ -15,6 +15,10 @@ class ca_agi_subtractions(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.ca.tax.income.agi - total_subtractions = add(tax_unit, period, p.subtractions) + # Each subtraction takes an amount from the filers' federal return. + # Federal AGI leaves out a tax unit dependent's income, which the + # dependent reports on their own return, so only the head's and + # spouse's amounts come off. + total_subtractions = tax_unit_non_dep_add(tax_unit, period, p.subtractions) # Prevent negative subtractions from acting as additions return max_(0, total_subtractions) diff --git a/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py b/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py index 50a237ec877..e4c3ae1d0d1 100644 --- a/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py +++ b/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py @@ -9,4 +9,11 @@ class ri_additions(Variable): definition_period = YEAR reference = "https://tax.ri.gov/sites/g/files/xkgbur541/files/2022-12/2022%201041%20Schedule%20M_w.pdf#page=2" defined_for = StateCode.RI - adds = "gov.states.ri.tax.income.agi.additions.additions" + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.states.ri.tax.income.agi.additions + # The additions modify the filers' federal AGI. A tax unit + # dependent's income, including their federally excluded + # unemployment compensation, is on their own return, so person-level + # additions count only the head and spouse. + return tax_unit_non_dep_add(tax_unit, period, p.additions) diff --git a/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py b/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py index 051d59a80cf..7639424d60b 100644 --- a/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py +++ b/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py @@ -14,7 +14,12 @@ class wi_unemployment_compensation_subtraction(Variable): defined_for = StateCode.WI def formula(tax_unit, period, parameters): - us_tuc = add(tax_unit, period, ["taxable_unemployment_compensation"]) + # Worksheet line 1 is the unemployment compensation in the filers' + # federal AGI, which leaves out a tax unit dependent's; the dependent + # reports their own on their own return. + us_tuc = tax_unit_non_dep_add( + tax_unit, period, ["taxable_unemployment_compensation"] + ) us_agi = tax_unit("adjusted_gross_income", period) wi = parameters(period).gov.states.wi.tax.income p = wi.subtractions.unemployment_compensation.income_phase_out From 7cff12835d4066610d1641db6138e16e37214935 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 19:49:48 -0400 Subject: [PATCH 2/7] Test that a dependent's unemployment compensation stays off CA, RI and WI returns CA Schedule CA column B, RI's 2020 IRC 85(c) add-back and WI's Schedule SB unemployment worksheet all start from the filers' own federal return. The new cases fail on main (7 of 12) and pass with the fix. Co-Authored-By: Claude Opus 5.5 --- .../ca/tax/income/ca_agi_subtractions.yaml | 92 +++++++++++++++++++ .../income/agi/additions/ri_additions.yaml | 79 ++++++++++++++++ ...unemployment_compensation_subtraction.yaml | 57 ++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/ca_agi_subtractions.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ri/tax/income/agi/additions/ri_additions.yaml diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/ca_agi_subtractions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/ca_agi_subtractions.yaml new file mode 100644 index 00000000000..dcff23ce1e3 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/tax/income/ca_agi_subtractions.yaml @@ -0,0 +1,92 @@ +# Schedule CA (540) column B subtracts amounts from column A, which holds the +# amounts on the filers' own federal return (2024 Schedule CA (540) +# instructions, "Column A - Federal Amounts"). Federal AGI leaves out a tax +# unit dependent's income, which the dependent reports on their own return, +# so only the head's and spouse's amounts are subtracted. + +- name: Case 1, a dependent's unemployment compensation is not subtracted from the filers' CA AGI. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + employment_income: 90_000 + person2: + age: 43 + employment_income: 20_000 + person3: + age: 17 + is_tax_unit_dependent: true + unemployment_compensation: 10_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: CA + output: + # The child's compensation is taxable on the child's own federal return. + taxable_unemployment_compensation: [0, 0, 10_000] + adjusted_gross_income: 110_000 + ca_agi_subtractions: 0 + ca_agi: 110_000 + +- name: Case 2, the head's unemployment compensation is subtracted; the dependent's is not. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + employment_income: 90_000 + unemployment_compensation: 8_000 + person2: + age: 43 + employment_income: 20_000 + person3: + age: 17 + is_tax_unit_dependent: true + unemployment_compensation: 10_000 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: CA + output: + # Federal AGI: 90,000 + 20,000 wages + the head's 8,000 compensation + # (federal Schedule 1, line 7). + adjusted_gross_income: 118_000 + # Schedule CA (540), section B, line 7, column B: 8,000. + ca_agi_subtractions: 8_000 + ca_agi: 110_000 + +- name: Case 3, a dependent's state tax refund is not subtracted from the filer's CA AGI. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 50_000 + salt_refund_income: 600 + person2: + age: 19 + is_tax_unit_dependent: true + is_full_time_student: true + salt_refund_income: 400 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: CA + output: + adjusted_gross_income: 50_600 + # Schedule CA (540), section B, line 1, column B: the head's 600 only. + ca_agi_subtractions: 600 + ca_agi: 50_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ri/tax/income/agi/additions/ri_additions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ri/tax/income/agi/additions/ri_additions.yaml new file mode 100644 index 00000000000..8eb5cce9bb2 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ri/tax/income/agi/additions/ri_additions.yaml @@ -0,0 +1,79 @@ +# RIGL 44-30-12(b)(6) adds back the unemployment compensation excluded from +# the filers' federal AGI under IRC 85(c) for 2020. Federal AGI leaves out a +# tax unit dependent's income, which the dependent reports on their own +# return, so a dependent's excluded compensation is not added to the filers' +# RI AGI. + +- name: Case 1, a dependent's excluded 2020 unemployment compensation is not added to the filer's RI AGI. + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 40_000 + person2: + age: 17 + is_tax_unit_dependent: true + unemployment_compensation: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: RI + output: + # The child's 10,000 is under the 10,200 exclusion on the child's own + # federal return. + tax_exempt_unemployment_compensation: [0, 10_000] + adjusted_gross_income: 40_000 + ri_additions: 0 + ri_agi: 40_000 + +- name: Case 2, the head's excluded unemployment compensation is added back; the dependent's is not. + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 40_000 + tax_exempt_unemployment_compensation: 8_000 + person2: + age: 17 + is_tax_unit_dependent: true + tax_exempt_unemployment_compensation: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: RI + output: + ri_additions: 8_000 + +- name: Case 3, a married couple's excluded unemployment compensation is added back for each spouse. + period: 2020 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + tax_exempt_unemployment_compensation: 6_000 + person2: + age: 38 + tax_exempt_unemployment_compensation: 4_000 + person3: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: RI + output: + ri_additions: 10_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_unemployment_compensation_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_unemployment_compensation_subtraction.yaml index ff13ab35a82..133a96f3efa 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_unemployment_compensation_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_unemployment_compensation_subtraction.yaml @@ -39,3 +39,60 @@ state_code: WI output: wi_unemployment_compensation_subtraction: 0 + +# Worksheet line 1 takes the unemployment compensation from line 7 of the +# filers' federal Schedule 1 (2024 Schedule SB instructions, line 3). Federal +# AGI leaves out a tax unit dependent's compensation, which the dependent +# reports on their own return. + +- name: Case 5, a dependent's unemployment compensation is not subtracted from the filer's income. + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 35 + employment_income: 15_000 + person2: + age: 17 + is_tax_unit_dependent: true + unemployment_compensation: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: WI + output: + taxable_unemployment_compensation: [0, 10_000] + adjusted_gross_income: 15_000 + wi_unemployment_compensation_subtraction: 0 + wi_agi: 15_000 + +- name: Case 6, the head's unemployment compensation goes through the worksheet; the dependent's does not. + absolute_error_margin: 0.01 + period: 2024 + input: + people: + person1: + age: 35 + employment_income: 12_000 + unemployment_compensation: 2_000 + person2: + age: 17 + is_tax_unit_dependent: true + unemployment_compensation: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: WI + output: + # Worksheet: line 1 = 2,000; line 2 (federal AGI) = 14,000; line 3 = + # 12,000 (box D); line 7 = 14,000 - 12,000 = 2,000; line 8 = 1,000; + # line 9 = min(2,000, 1,000) = 1,000; line 10 = 2,000 - 1,000 = 1,000. + adjusted_gross_income: 14_000 + wi_unemployment_compensation_subtraction: 1_000 From 042b072820cc131f9f754a81e5f6478ba82bf52e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 19:56:45 -0400 Subject: [PATCH 3/7] Cite the forms and statutes for CA, RI and WI on the filers' own return CA: 2024 Schedule CA (540) column A takes the filers' federal amounts; the 540 booklet gives a dependent their own filing requirement (pages 4, 13). RI: RIGL 44-30-12(b)(6) and the 2020 Schedule M line 2g and instructions. WI: Wis. Stat. 71.05(6)(b)8., the 2024 Schedule SB worksheet line 1, and the 2024 Form 1 dependent filing requirement. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/ca/tax/income/ca_agi_subtractions.py | 5 +++++ .../gov/states/ri/tax/income/agi/ri_additions.py | 10 +++++++++- .../wi_unemployment_compensation_subtraction.py | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py b/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py index e7bd1a59892..56d3f306ac8 100644 --- a/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py +++ b/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py @@ -10,6 +10,11 @@ class ca_agi_subtractions(Variable): reference = ( "https://www.ftb.ca.gov/forms/2021/2021-540-ca-instructions.html", "https://www.ftb.ca.gov/forms/2022/2022-540-ca-instructions.html", + # Column A takes the amounts on the filers' own federal return. + "https://www.ftb.ca.gov/forms/2024/2024-540-ca-instructions.html", + # PDF pages 4, 13: a dependent's own filing requirement and + # standard deduction worksheet. + "https://www.ftb.ca.gov/forms/2024/2024-540-booklet.pdf#page=4", ) defined_for = StateCode.CA diff --git a/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py b/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py index e4c3ae1d0d1..c00b96ca901 100644 --- a/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py +++ b/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py @@ -7,7 +7,15 @@ class ri_additions(Variable): label = "Rhode Island AGI Additions" unit = USD definition_period = YEAR - reference = "https://tax.ri.gov/sites/g/files/xkgbur541/files/2022-12/2022%201041%20Schedule%20M_w.pdf#page=2" + reference = ( + "https://tax.ri.gov/sites/g/files/xkgbur541/files/2022-12/2022%201041%20Schedule%20M_w.pdf#page=2", + # R.I. Gen. Laws § 44-30-12(a) and (b)(6) + "https://webserver.rilegislature.gov/Statutes/TITLE44/44-30/44-II/44-30-12.htm", + # 2020 RI Schedule M line 2g + "https://tax.ri.gov/sites/g/files/xkgbur541/files/forms/2020/Income/2020-RI-Schedule-M_w.pdf#page=2", + # PDF pages 1, 9: who must file; Schedule M line 2g + "https://tax.ri.gov/sites/g/files/xkgbur541/files/forms/2020/Income/2020-Resident-Instructions_03282021.pdf#page=1", + ) defined_for = StateCode.RI def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py b/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py index 7639424d60b..946e72edbbf 100644 --- a/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py +++ b/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py @@ -8,8 +8,12 @@ class wi_unemployment_compensation_subtraction(Variable): unit = USD definition_period = YEAR reference = ( + "https://docs.legis.wisconsin.gov/statutes/statutes/71/i/05/6/b/8", "https://www.revenue.wi.gov/TaxForms2021/2021-ScheduleSB-inst.pdf#page=2", "https://www.revenue.wi.gov/TaxForms2022/2022-ScheduleSB-Inst.pdf#page=1", + "https://www.revenue.wi.gov/TaxForms2024/2024-ScheduleSB-Inst.pdf#page=2", + # A dependent's own filing requirement + "https://www.revenue.wi.gov/TaxForms2024/2024-Form1-Inst.pdf#page=3", ) defined_for = StateCode.WI From eead67ed0fd8457e53e23cc359af5ccf482ab538 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 22:22:53 -0400 Subject: [PATCH 4/7] Add invariants test for dependents' unemployment compensation in CA, RI and WI Co-Authored-By: Claude Opus 5.5 --- ...mployment_compensation_state_invariants.py | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py diff --git a/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py b/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py new file mode 100644 index 00000000000..08c96554b83 --- /dev/null +++ b/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py @@ -0,0 +1,289 @@ +"""Invariants for a tax unit dependent's unemployment compensation in CA, RI and WI. + +`irs_gross_income` leaves out every tax unit dependent's income, which the +dependent reports on their own return. California's Schedule CA subtraction +(section B line 7), Rhode Island's add-back of compensation excluded from +federal AGI (RIGL 44-30-12(b)(6)) and Wisconsin's unemployment compensation +worksheet (Schedule SB line 3, line 1 "from line 7 of federal Schedule 1") +all adjust the filers' own federal AGI, so a dependent's compensation must +not move them. + +The tests draw a seeded random population of tax units, each single or +joint, with wages, unemployment compensation and state tax refunds for the +head and spouse, and zero to two dependent children with unemployment +compensation. Every unit is placed in each of the three states, and each +scenario runs as one vectorized simulation. TY2020 is the only year with a +federal exclusion (IRC 85(c)), so Rhode Island's add-back is non-zero only +then; TY2024 checks California and Wisconsin in a year without it. In 2020 +each person's taxable compensation is an input (their compensation less a +drawn excluded part), so the properties check the state lines whatever the +federal exclusion calculation gives. + +1. Each state line, and the state AGI built on it, is the same whether or + not the dependents have unemployment compensation. +2. Each line equals a reference built from the head's and spouse's own + amounts: California subtracts their taxable compensation and state tax + refunds; Rhode Island adds their federally excluded compensation; + Wisconsin runs its worksheet on their taxable compensation. +3. For tax units without dependents each line equals the sum over every + member, as before. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation + +SEED = 20261012 +TOLERANCE = 0.01 # dollars +STATES = ["CA", "RI", "WI"] +YEARS = [2020, 2024] +N_UNITS = 40 +# Wisconsin's worksheet base (line 3) by filing status: $18,000 joint, +# $12,000 otherwise (2024 Schedule SB instructions, page 2). No unit here is +# married filing separately. +EXCLUSION_YEAR = 2020 +EXCLUSION_CAP = 10_200 +WI_BASE_JOINT = 18_000 +WI_BASE_OTHER = 12_000 +WI_RATE = 0.5 + + +def _draw_units(n, rng): + def draw(scale): + return float(round(rng.choice([0.0, rng.uniform(0, scale)]))) + + def excluded(compensation): + # The part of 2020 compensation excluded from federal AGI: none, all + # up to the cap, or part of it. + share = float(rng.choice([0.0, 1.0, 0.5])) + return float(round(min(compensation, EXCLUSION_CAP) * share)) + + units = [] + for _ in range(n): + joint = bool(rng.random() < 0.5) + # Low wages in some units keep Wisconsin's worksheet in its + # phase-out range, where line 7 is below twice the compensation. + wage_scale = float(rng.choice([20_000, 90_000])) + + def filer(): + compensation = draw(15_000) + return { + "employment_income": draw(wage_scale), + "unemployment_compensation": compensation, + "salt_refund_income": draw(1_500), + "excluded_2020": excluded(compensation), + } + + def dependent(): + compensation = draw(12_000) + return { + "age": int(rng.integers(5, 19)), + "unemployment_compensation": compensation, + "excluded_2020": excluded(compensation), + } + + units.append( + { + "joint": joint, + "head": filer(), + "spouse": filer() if joint else None, + "dependents": [dependent() for _ in range(int(rng.integers(0, 3)))], + } + ) + return units + + +def _compensation_inputs(person, year, scale=1.0): + compensation = scale * person["unemployment_compensation"] + inputs = {"unemployment_compensation": {year: compensation}} + if year == EXCLUSION_YEAR: + taxable = compensation - scale * person["excluded_2020"] + inputs["taxable_unemployment_compensation"] = {year: taxable} + return inputs + + +def _filer_inputs(filer, state, year): + # Only California subtracts state tax refunds among these lines; + # Wisconsin's worksheet counts them on line 5, which the model leaves out, + # so the refunds go only to the California units. + refunds = filer["salt_refund_income"] if state == "CA" else 0.0 + return { + "employment_income": {year: filer["employment_income"]}, + "salt_refund_income": {year: refunds}, + **_compensation_inputs(filer, year), + } + + +def _situation(units, year, *, zero_dependents=False): + people, tax_units, marital_units, spm_units = {}, {}, {}, {} + families, households = {}, {} + for state in STATES: + for i, u in enumerate(units): + key = f"{state}_{i}" + head = f"head_{key}" + members = [head] + couple = [head] + people[head] = { + "age": {year: 40}, + "is_tax_unit_head": {year: True}, + "is_tax_unit_spouse": {year: False}, + "is_tax_unit_dependent": {year: False}, + **_filer_inputs(u["head"], state, year), + } + if u["spouse"] is not None: + spouse = f"spouse_{key}" + members.append(spouse) + couple.append(spouse) + people[spouse] = { + "age": {year: 38}, + "is_tax_unit_head": {year: False}, + "is_tax_unit_spouse": {year: True}, + "is_tax_unit_dependent": {year: False}, + **_filer_inputs(u["spouse"], state, year), + } + marital_units[f"mu_{key}"] = {"members": couple} + for j, dependent in enumerate(u["dependents"]): + child = f"child_{key}_{j}" + members.append(child) + marital_units[f"mu_{key}_{j}"] = {"members": [child]} + people[child] = { + "age": {year: dependent["age"]}, + "is_tax_unit_head": {year: False}, + "is_tax_unit_spouse": {year: False}, + "is_tax_unit_dependent": {year: True}, + **_compensation_inputs( + dependent, year, scale=0.0 if zero_dependents else 1.0 + ), + } + tax_units[f"tu_{key}"] = {"members": members} + spm_units[f"spm_{key}"] = {"members": members} + families[f"fam_{key}"] = {"members": members} + households[f"hh_{key}"] = { + "members": members, + "state_name": {year: state}, + } + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "spm_units": spm_units, + "families": families, + "households": households, + } + + +def _state_slice(state): + start = STATES.index(state) * N_UNITS + return slice(start, start + N_UNITS) + + +def _filer_sum(sim, variable, year): + """Sum a person-level variable over each tax unit's head and spouse.""" + values = sim.calculate(variable, year) + filer = ~sim.calculate("is_tax_unit_dependent", year) + tax_unit = sim.populations["tax_unit"] + return tax_unit.sum(values * filer) + + +def _member_sum(sim, variable, year): + values = sim.calculate(variable, year) + return sim.populations["tax_unit"].sum(values) + + +LINES = { + "CA": ("ca_agi_subtractions", "ca_agi"), + "RI": ("ri_additions", "ri_agi"), + "WI": ("wi_unemployment_compensation_subtraction", "wi_agi"), +} + + +@pytest.fixture(scope="module", params=YEARS) +def simulations(request): + year = request.param + rng = np.random.default_rng(SEED + year) + units = _draw_units(N_UNITS, rng) + with_income = Simulation(situation=_situation(units, year)) + without_income = Simulation(situation=_situation(units, year, zero_dependents=True)) + return year, units, with_income, without_income + + +def test_dependents_compensation_leaves_state_lines_unchanged(simulations): + year, units, with_income, without_income = simulations + # The population must exercise the property: some dependents have + # compensation in every state. + assert any(d["unemployment_compensation"] for u in units for d in u["dependents"]) + for state, variables in LINES.items(): + rows = _state_slice(state) + for variable in variables: + np.testing.assert_allclose( + with_income.calculate(variable, year)[rows], + without_income.calculate(variable, year)[rows], + atol=TOLERANCE, + err_msg=f"{state} {variable} {year}", + ) + + +def test_state_lines_use_the_filers_own_amounts(simulations): + year, units, sim, _ = simulations + filer_taxable = _filer_sum(sim, "taxable_unemployment_compensation", year) + filer_excluded = _filer_sum(sim, "tax_exempt_unemployment_compensation", year) + filer_refunds = _filer_sum(sim, "salt_refund_income", year) + + ca = _state_slice("CA") + np.testing.assert_allclose( + sim.calculate("ca_agi_subtractions", year)[ca], + (filer_taxable + filer_refunds)[ca], + atol=TOLERANCE, + ) + + ri = _state_slice("RI") + np.testing.assert_allclose( + sim.calculate("ri_additions", year)[ri], + filer_excluded[ri], + atol=TOLERANCE, + ) + if year == 2020: + # The units draw enough compensation that the 2020 exclusion bites. + assert filer_excluded[ri].sum() > 0 + + wi = _state_slice("WI") + joint = np.array([u["joint"] for u in units]) + base = np.where(joint, WI_BASE_JOINT, WI_BASE_OTHER) + agi = sim.calculate("adjusted_gross_income", year)[wi] + taxable_social_security = sim.calculate("tax_unit_taxable_social_security", year)[ + wi + ] + line_1 = filer_taxable[wi] + line_8 = np.maximum(0, agi - base - taxable_social_security) * WI_RATE + expected = line_1 - np.minimum(line_1, line_8) + np.testing.assert_allclose( + sim.calculate("wi_unemployment_compensation_subtraction", year)[wi], + expected, + atol=TOLERANCE, + ) + # Some units fall in the phase-out range, where the worksheet keeps part + # of the compensation taxable. + assert ((expected > 0) & (expected < line_1)).any() + + +def test_units_without_dependents_match_the_sum_over_members(simulations): + year, units, sim, _ = simulations + no_dependents = np.array([not u["dependents"] for u in units]) + ca = _state_slice("CA") + member_ca = ( + _member_sum(sim, "taxable_unemployment_compensation", year) + + _member_sum(sim, "salt_refund_income", year) + )[ca] + np.testing.assert_allclose( + sim.calculate("ca_agi_subtractions", year)[ca][no_dependents], + member_ca[no_dependents], + atol=TOLERANCE, + ) + ri = _state_slice("RI") + member_ri = _member_sum(sim, "tax_exempt_unemployment_compensation", year)[ri] + np.testing.assert_allclose( + sim.calculate("ri_additions", year)[ri][no_dependents], + member_ri[no_dependents], + atol=TOLERANCE, + ) From 496f635cb086def9f3291377fa3a6d7d7c681d33 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 22:26:08 -0400 Subject: [PATCH 5/7] Address review: scope the CA comment, fix reference pages, widen changelog - ca_agi_subtractions: tax-unit-level items such as us_govt_interest are added as they are (#9894 makes that one filer-only). - WI Schedule SB worksheet is on file page 3 in 2021 and page 2 in 2022; the RI reference is the individual 2022 Schedule M, not the RI-1041's. - The changelog also names California's state tax refund subtraction. Co-Authored-By: Claude Opus 5.5 --- changelog.d/dependent-uc-state-agi.fixed.md | 2 +- .../gov/states/ca/tax/income/ca_agi_subtractions.py | 5 +++-- .../variables/gov/states/ri/tax/income/agi/ri_additions.py | 2 +- .../subtractions/wi_unemployment_compensation_subtraction.py | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/changelog.d/dependent-uc-state-agi.fixed.md b/changelog.d/dependent-uc-state-agi.fixed.md index 98b0973f05d..f6210a7a4fd 100644 --- a/changelog.d/dependent-uc-state-agi.fixed.md +++ b/changelog.d/dependent-uc-state-agi.fixed.md @@ -1 +1 @@ -Leave a tax unit dependent's unemployment compensation off the filers' California AGI subtractions, Rhode Island AGI additions and Wisconsin unemployment compensation subtraction. +Leave a tax unit dependent's unemployment compensation off the filers' California AGI subtractions, Rhode Island AGI additions and Wisconsin unemployment compensation subtraction, and a dependent's state tax refund off California's. diff --git a/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py b/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py index 56d3f306ac8..350c323f079 100644 --- a/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py +++ b/policyengine_us/variables/gov/states/ca/tax/income/ca_agi_subtractions.py @@ -22,8 +22,9 @@ def formula(tax_unit, period, parameters): p = parameters(period).gov.states.ca.tax.income.agi # Each subtraction takes an amount from the filers' federal return. # Federal AGI leaves out a tax unit dependent's income, which the - # dependent reports on their own return, so only the head's and - # spouse's amounts come off. + # dependent reports on their own return, so person-level amounts + # count only for the head and spouse. Tax-unit-level items, such as + # us_govt_interest, are added as they are. total_subtractions = tax_unit_non_dep_add(tax_unit, period, p.subtractions) # Prevent negative subtractions from acting as additions return max_(0, total_subtractions) diff --git a/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py b/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py index c00b96ca901..88802833c21 100644 --- a/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py +++ b/policyengine_us/variables/gov/states/ri/tax/income/agi/ri_additions.py @@ -8,7 +8,7 @@ class ri_additions(Variable): unit = USD definition_period = YEAR reference = ( - "https://tax.ri.gov/sites/g/files/xkgbur541/files/2022-12/2022%201041%20Schedule%20M_w.pdf#page=2", + "https://tax.ri.gov/sites/g/files/xkgbur541/files/2022-12/2022%20RI%20Schedule%20M_w.pdf#page=2", # R.I. Gen. Laws § 44-30-12(a) and (b)(6) "https://webserver.rilegislature.gov/Statutes/TITLE44/44-30/44-II/44-30-12.htm", # 2020 RI Schedule M line 2g diff --git a/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py b/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py index 946e72edbbf..643b0078853 100644 --- a/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py +++ b/policyengine_us/variables/gov/states/wi/tax/income/subtractions/wi_unemployment_compensation_subtraction.py @@ -9,8 +9,8 @@ class wi_unemployment_compensation_subtraction(Variable): definition_period = YEAR reference = ( "https://docs.legis.wisconsin.gov/statutes/statutes/71/i/05/6/b/8", - "https://www.revenue.wi.gov/TaxForms2021/2021-ScheduleSB-inst.pdf#page=2", - "https://www.revenue.wi.gov/TaxForms2022/2022-ScheduleSB-Inst.pdf#page=1", + "https://www.revenue.wi.gov/TaxForms2021/2021-ScheduleSB-inst.pdf#page=3", + "https://www.revenue.wi.gov/TaxForms2022/2022-ScheduleSB-Inst.pdf#page=2", "https://www.revenue.wi.gov/TaxForms2024/2024-ScheduleSB-Inst.pdf#page=2", # A dependent's own filing requirement "https://www.revenue.wi.gov/TaxForms2024/2024-Form1-Inst.pdf#page=3", From 24653845bb93e95f352ac6a1e3098b33fe9dec30 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 22:43:38 -0400 Subject: [PATCH 6/7] Cover California dependents' state tax refunds in the invariants test Co-Authored-By: Claude Opus 5.5 --- ...mployment_compensation_state_invariants.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py b/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py index 08c96554b83..e8fe09b2e9d 100644 --- a/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py +++ b/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py @@ -11,7 +11,7 @@ The tests draw a seeded random population of tax units, each single or joint, with wages, unemployment compensation and state tax refunds for the head and spouse, and zero to two dependent children with unemployment -compensation. Every unit is placed in each of the three states, and each +compensation and, in California, state tax refunds. Every unit is placed in each of the three states, and each scenario runs as one vectorized simulation. TY2020 is the only year with a federal exclusion (IRC 85(c)), so Rhode Island's add-back is non-zero only then; TY2024 checks California and Wisconsin in a year without it. In 2020 @@ -39,11 +39,11 @@ STATES = ["CA", "RI", "WI"] YEARS = [2020, 2024] N_UNITS = 40 +EXCLUSION_YEAR = 2020 +EXCLUSION_CAP = 10_200 # Wisconsin's worksheet base (line 3) by filing status: $18,000 joint, # $12,000 otherwise (2024 Schedule SB instructions, page 2). No unit here is # married filing separately. -EXCLUSION_YEAR = 2020 -EXCLUSION_CAP = 10_200 WI_BASE_JOINT = 18_000 WI_BASE_OTHER = 12_000 WI_RATE = 0.5 @@ -81,6 +81,7 @@ def dependent(): "age": int(rng.integers(5, 19)), "unemployment_compensation": compensation, "excluded_2020": excluded(compensation), + "salt_refund_income": draw(800), } units.append( @@ -143,6 +144,7 @@ def _situation(units, year, *, zero_dependents=False): **_filer_inputs(u["spouse"], state, year), } marital_units[f"mu_{key}"] = {"members": couple} + scale = 0.0 if zero_dependents else 1.0 for j, dependent in enumerate(u["dependents"]): child = f"child_{key}_{j}" members.append(child) @@ -152,9 +154,13 @@ def _situation(units, year, *, zero_dependents=False): "is_tax_unit_head": {year: False}, "is_tax_unit_spouse": {year: False}, "is_tax_unit_dependent": {year: True}, - **_compensation_inputs( - dependent, year, scale=0.0 if zero_dependents else 1.0 - ), + **_compensation_inputs(dependent, year, scale=scale), + # Wisconsin's general subtractions still take a + # dependent's refund (a separate fix), so only the + # California dependents get one. + "salt_refund_income": { + year: scale * dependent["salt_refund_income"] * (state == "CA") + }, } tax_units[f"tu_{key}"] = {"members": members} spm_units[f"spm_{key}"] = {"members": members} @@ -211,8 +217,9 @@ def simulations(request): def test_dependents_compensation_leaves_state_lines_unchanged(simulations): year, units, with_income, without_income = simulations # The population must exercise the property: some dependents have - # compensation in every state. + # compensation in every state, and some California dependents refunds. assert any(d["unemployment_compensation"] for u in units for d in u["dependents"]) + assert any(d["salt_refund_income"] for u in units for d in u["dependents"]) for state, variables in LINES.items(): rows = _state_slice(state) for variable in variables: From 208a87f7a902de127ae3e15a175e58b36967c0a4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 22:46:50 -0400 Subject: [PATCH 7/7] Reflow the invariants test docstring Co-Authored-By: Claude Opus 5.5 --- ...t_dependent_unemployment_compensation_state_invariants.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py b/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py index e8fe09b2e9d..15d023e9283 100644 --- a/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py +++ b/policyengine_us/tests/core/test_dependent_unemployment_compensation_state_invariants.py @@ -11,8 +11,9 @@ The tests draw a seeded random population of tax units, each single or joint, with wages, unemployment compensation and state tax refunds for the head and spouse, and zero to two dependent children with unemployment -compensation and, in California, state tax refunds. Every unit is placed in each of the three states, and each -scenario runs as one vectorized simulation. TY2020 is the only year with a +compensation and, in California, state tax refunds. Every unit is placed +in each of the three states, and each scenario runs as one vectorized +simulation. TY2020 is the only year with a federal exclusion (IRC 85(c)), so Rhode Island's add-back is non-zero only then; TY2024 checks California and Wisconsin in a year without it. In 2020 each person's taxable compensation is an input (their compensation less a