Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix Wisconsin Act 15 retirement income exclusion to reduce WI income before the standard-deduction phaseout, so elderly filers with retirement income are no longer over-taxed.
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,15 @@
members: [person1]
state_code: WI
output:
# AGI = $45k → Line 17 ineligible (> $15k), Line 16 = $20k
# Standard tax ≈ $1,377, exclusion tax ≈ $474
# min($1377, $474) = $474 → exclusion path wins
# AGI = $45k → Line 17 ineligible (> $15k), Line 16 = $20k.
# The $20k Line 16 subtraction lowers WI income to $25k, where the
# standard deduction phaseout is smaller, so the standard deduction is
# $12,906 (vs $10,506 at $45k) — $2,400 larger. Exclusion taxable income
# = ($45k - $10,506 - $950 exemption) - $20k - $2,400 = $11,144;
# exclusion tax = $11,144 * 0.035 = $390.
# Standard tax ≈ $1,344 → min($1,344, $390) = $390 → exclusion path wins.
wi_retirement_income_exclusion_amount: 20_000
wi_income_tax: 474
wi_income_tax: 390

- name: E2E - Joint both 68, large pension, exclusion zeroes out tax
absolute_error_margin: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,131 @@
# exclusion_taxinc = max(0, 18000 - 15000 + 0) = 3000
# exclusion_tax = 3000 * 0.035 = 105
wi_retirement_income_exclusion_tax: 105

# Regression for issue #8817: the Line 16 subtraction must reduce WI income
# BEFORE the standard-deduction phaseout, so a large exclusion recovers
# standard deduction that the pre-subtraction income had phased out.
- name: E2E - Joint 67+ with retirement income above the standard-deduction phaseout
absolute_error_margin: 1
period: 2025
input:
people:
person1:
age: 70
taxable_pension_income: 90_000
is_tax_unit_head: true
person2:
age: 70
taxable_pension_income: 30_000
is_tax_unit_spouse: true
tax_units:
tax_unit:
members: [person1, person2]
filing_status: JOINT
households:
household:
members: [person1, person2]
state_code: WI
output:
# WI income (line 7) = $120,000; Line 16 exclusion = min($48k, $120k) = $48k.
# Standard deduction at $120k = $6,956; at ($120k - $48k) = $72k it is
# $16,449 — $9,493 larger (the phaseout is 0.19778 * $48k). Exclusion
# taxable income = $72k - $16,449 - $1,900 exemption = $53,651; joint tax
# = 19,580*0.035 + (53,651-19,580)*0.044 = $2,184. Before the fix the
# phaseout stayed keyed to $120k, giving ~$2,602 and over-taxing the couple.
wi_retirement_income_exclusion_amount: 48_000
wi_retirement_income_exclusion_tax: 2_184

# --- Boundary tests for the #8817 standard-deduction-phaseout ordering fix ---

# Below the single phaseout start ($19,550, 2025) the standard deduction is at
# its max for both full and exclusion-reduced income, so the extra deduction the
# fix unlocks is $0 (no regression vs. a plain taxinc - line16 subtraction).
- name: Exclusion path - below phaseout start, extra standard deduction is zero (no regression)
absolute_error_margin: 0.01
period: 2025
input:
age: 68
taxable_pension_income: 15_000
is_tax_unit_head: true
filing_status: SINGLE
state_code: WI
wi_taxable_income: 20_000
wi_retirement_income_subtraction: 0
output:
# wi_agi = 15,000 < 19,550 -> SD at max ($13,560) for full and reduced; extra_SD = 0.
# line16 = min(24,000, 15,000) = 15,000. exclusion_taxinc = 20,000 - 15,000 = 5,000.
# tax = 5,000 * 0.035 = 175.
wi_retirement_income_exclusion_tax: 175

# Fully phased-out top end (age 85, very high income): SD = $0 for both full and
# reduced income, so extra_SD = 0 and the exclusion is a plain subtraction.
- name: Exclusion path - fully phased-out top end, extra standard deduction is zero
absolute_error_margin: 0.01
period: 2025
input:
age: 85
taxable_pension_income: 200_000
is_tax_unit_head: true
filing_status: SINGLE
state_code: WI
wi_taxable_income: 34_000
wi_retirement_income_subtraction: 0
output:
# wi_agi = 200,000 and reduced = 176,000 are both past the single full-phaseout
# point ($132,550) -> SD = 0 for both; extra_SD = 0. line16 = 24,000.
# exclusion_taxinc = 34,000 - 24,000 = 10,000. tax = 10,000 * 0.035 = 350.
wi_retirement_income_exclusion_tax: 350

# Partial-unlock boundary: full income ($140k) is past the single full-phaseout
# point (SD_full = 0) while reduced income ($116k) lands INSIDE the phaseout, so
# the fix unlocks a positive partial deduction.
- name: Exclusion path - partial unlock (full income past phaseout end, reduced income inside it)
absolute_error_margin: 1
period: 2025
input:
age: 68
taxable_pension_income: 140_000
is_tax_unit_head: true
filing_status: SINGLE
state_code: WI
wi_taxable_income: 40_000
wi_retirement_income_subtraction: 0
output:
# SD_full(140,000) = 0; line16 = 24,000; reduced = 116,000.
# SD_reduced(116,000) = 13,560 - 0.12*(116,000-19,550) = 1,986; extra_SD = 1,986.
# exclusion_taxinc = 40,000 - 24,000 - 1,986 = 14,014. tax = 14,014 * 0.035 = 490.49.
# (Without the fix extra_SD would be 0 -> 16,000 -> ~571.88.)
wi_retirement_income_exclusion_tax: 490.49

# Joint with only ONE spouse age 67+: the exclusion uses the per-person single
# cap ($24,000), NOT the $48,000 both-eligible joint pool.
- name: Exclusion path - joint with one spouse 67+, uses the $24,000 per-person cap
absolute_error_margin: 0.01
period: 2025
input:
people:
person1:
age: 68
taxable_pension_income: 24_000
is_tax_unit_head: true
person2:
age: 60
is_tax_unit_spouse: true
tax_units:
tax_unit:
members: [person1, person2]
filing_status: JOINT
wi_taxable_income: 30_000
wi_retirement_income_subtraction: 0
households:
household:
members: [person1, person2]
state_code: WI
output:
# Only person1 (68) is eligible -> line16 = min(24,000, 24,000) = 24,000 (single
# per-person cap), not the $48k joint pool. wi_agi = 24,000 < 28,210 joint
# phaseout start -> extra_SD = 0. exclusion_taxinc = 30,000 - 24,000 = 6,000.
# tax = 6,000 * 0.035 = 210.
wi_retirement_income_exclusion_amount: 24_000
wi_retirement_income_exclusion_tax: 210
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from policyengine_us.model_api import *
from policyengine_us.variables.gov.states.wi.tax.income.wi_standard_deduction import (
wi_standard_deduction_for_income,
)


class wi_retirement_income_exclusion_tax(Variable):
Expand All @@ -21,9 +24,23 @@ def formula(tax_unit, period, parameters):
# Line 16 is claimed, so we simply subtract Line 16 here.
line16 = tax_unit("wi_retirement_income_exclusion_amount", period)
taxinc = tax_unit("wi_taxable_income", period)
exclusion_taxinc = max_(0, taxinc - line16)

# The Line 16 subtraction reduces WI income (Form 1 line 7), and the
# standard deduction (line 8) is looked up on that post-subtraction
# income. wi_taxable_income embeds the standard deduction computed on
# the higher pre-subtraction income, so add back the extra standard
# deduction the subtraction unlocks. This is algebraically equivalent
# to recomputing taxable income on the reduced WI income, and keeps
# the phaseout from staying keyed to the pre-subtraction income.
fstatus = tax_unit("filing_status", period)
wi_agi = tax_unit("wi_agi", period)
standard_deduction_full = tax_unit("wi_standard_deduction", period)
standard_deduction_reduced = wi_standard_deduction_for_income(
max_(0, wi_agi - line16), fstatus, parameters, period
)
extra_standard_deduction = standard_deduction_reduced - standard_deduction_full
exclusion_taxinc = max_(0, taxinc - line16 - extra_standard_deduction)

statuses = fstatus.possible_values
p = parameters(period).gov.states.wi.tax.income
return select(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,53 @@
from policyengine_us.model_api import *


def wi_standard_deduction_for_income(income, filing_status, parameters, period):
# Wisconsin standard deduction (Form 1 line 8) as a function of WI income.
# Factored out of the variable below so the retirement-income-exclusion
# path can look the deduction up on income reduced by the Schedule SB
# line-16 subtraction (the phaseout otherwise stays keyed to the higher
# pre-subtraction income).
deduction = parameters(period).gov.states.wi.tax.income.deductions
statuses = filing_status.possible_values
max_amount = deduction.standard.max[filing_status]
phase_out_amount = select(
[
filing_status == statuses.SINGLE,
filing_status == statuses.JOINT,
filing_status == statuses.SURVIVING_SPOUSE,
filing_status == statuses.SEPARATE,
filing_status == statuses.HEAD_OF_HOUSEHOLD,
],
[
deduction.standard.phase_out.single.calc(income),
deduction.standard.phase_out.joint.calc(income),
deduction.standard.phase_out.joint.calc(income),
deduction.standard.phase_out.separate.calc(income),
deduction.standard.phase_out.head_of_household.calc(income),
],
)
return max_(0, max_amount - phase_out_amount)


class wi_standard_deduction(Variable):
value_type = float
entity = TaxUnit
label = "Wisconsin standard deduction"
unit = USD
definition_period = YEAR
reference = (
"https://www.revenue.wi.gov/TaxForms2021/2021-Form1f.pdf"
"https://www.revenue.wi.gov/TaxForms2021/2021-Form1-Inst.pdf"
"https://www.revenue.wi.gov/TaxForms2022/2022-Form1f.pdf"
"https://www.revenue.wi.gov/TaxForms2022/2022-Form1-Inst.pdf"
"https://docs.legis.wisconsin.gov/misc/lfb/informational_papers/january_2023/0002_individual_income_tax_informational_paper_2.pdf"
"https://www.revenue.wi.gov/TaxForms2021/2021-Form1f.pdf",
"https://www.revenue.wi.gov/TaxForms2021/2021-Form1-Inst.pdf",
"https://www.revenue.wi.gov/TaxForms2022/2022-Form1f.pdf",
"https://www.revenue.wi.gov/TaxForms2022/2022-Form1-Inst.pdf",
"https://docs.legis.wisconsin.gov/misc/lfb/informational_papers/january_2023/0002_individual_income_tax_informational_paper_2.pdf",
# Standard Deduction Table (keyed to WI income, line 7) and the statute, corroborating the phaseout:
"https://www.revenue.wi.gov/TaxForms2025/2025-Form1-inst.pdf#page=35",
"https://docs.legis.wisconsin.gov/statutes/statutes/71/i/05/22",
)
defined_for = StateCode.WI

def formula(tax_unit, period, parameters):
fstatus = tax_unit("filing_status", period)
deduction = parameters(period).gov.states.wi.tax.income.deductions
max_amount = deduction.standard.max[fstatus]
agi = tax_unit("wi_agi", period)
phase_out_amount = select(
[
fstatus == fstatus.possible_values.SINGLE,
fstatus == fstatus.possible_values.JOINT,
fstatus == fstatus.possible_values.SURVIVING_SPOUSE,
fstatus == fstatus.possible_values.SEPARATE,
fstatus == fstatus.possible_values.HEAD_OF_HOUSEHOLD,
],
[
deduction.standard.phase_out.single.calc(agi),
deduction.standard.phase_out.joint.calc(agi),
deduction.standard.phase_out.joint.calc(agi),
deduction.standard.phase_out.separate.calc(agi),
deduction.standard.phase_out.head_of_household.calc(agi),
],
)
return max_(0, max_amount - phase_out_amount)
return wi_standard_deduction_for_income(agi, fstatus, parameters, period)
Loading