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
1 change: 1 addition & 0 deletions changelog.d/mi-military-survivor-spouse-only.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Restrict military survivor retirement tax exemptions to eligible surviving spouses in Arizona, Idaho, Indiana, Michigan, North Dakota, New Mexico, Rhode Island, South Carolina, and Virginia. Apply New Mexico's 2025 survivor eligibility date and use Michigan's eligible subtraction in retirement deduction worksheets.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ description: Idaho counts the following income sources under the retirement bene
values:
2021-01-01:
- public_pension_income
- military_retirement_income
- id_eligible_military_retirement_income

2025-01-01:
- public_pension_income
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ description: Michigan subtracts these sources to federal adjusted gross income w
values:
2017-01-01:
- us_govt_interest # Line 10
- military_retirement_income # Line 11
- mi_military_retirement_subtraction # Line 11
- taxable_social_security # Line 14
- military_service_income # Line 14
- section_22_income # Line 22
Expand All @@ -14,7 +14,7 @@ values:
- mi_other_subtractions
2026-01-01:
- us_govt_interest # Line 10
- military_retirement_income # Line 11
- mi_military_retirement_subtraction # Line 11

- taxable_social_security # Line 14
- military_service_income # Line 14
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ values:
- nd_ltcg_subtraction # long-term capital gains
- nd_qdiv_subtraction # qualified dividends
- taxable_social_security
- military_retirement_income
- nd_military_retirement_subtraction
- nd_529_deduction
- nd_stillborn_subtraction
- nd_other_subtractions
Expand All @@ -15,7 +15,7 @@ values:
- nd_ltcg_subtraction # long-term capital gains
- nd_qdiv_subtraction # qualified dividends
- taxable_social_security
- military_retirement_income
- nd_military_retirement_subtraction
- nd_529_deduction
- nd_stillborn_subtraction
- nd_other_subtractions
Expand All @@ -25,7 +25,7 @@ values:
- nd_ltcg_subtraction # long-term capital gains
- nd_qdiv_subtraction # qualified dividends
- taxable_social_security
- military_retirement_income
- nd_military_retirement_subtraction
- military_service_income # military pay exclusion
- nd_529_deduction
- nd_stillborn_subtraction
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
description: New Mexico allows surviving spouses to claim the armed forces retirement pay exemption when this is true.
values:
2022-01-01: false
2025-01-01: true

metadata:
label: New Mexico armed forces retirement surviving spouse eligibility
unit: bool
period: year
reference:
- title: 2024 HB 252, Sections 32 and 42 (PDF pages 102 and 147), amending NMSA 7-2-5.13 and setting applicability
href: https://www.nmlegis.gov/Sessions/24%20Regular/final/HB0252.PDF#page=102
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ values:
- ri_tuition_saving_program_contribution_subtraction
- ri_retirement_income_subtraction
- us_govt_interest
- military_retirement_income
- ri_military_retirement_subtraction
- ri_other_subtractions

metadata:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ values:
- military_retirement_income
- taxable_401k_distributions
- taxable_403b_distributions
2023-01-01:
- taxable_pension_income
- taxable_401k_distributions
- taxable_403b_distributions
metadata:
unit: list
period: year
Expand All @@ -18,3 +14,5 @@ metadata:
href: https://webserver.rilegislature.gov/Statutes/TITLE44/44-30/44-II/44-30-12.htm
- title: Rhode Island Division of Taxation Publication 2021-02 Line 5b
href: https://tax.ri.gov/sites/g/files/xkgbur541/files/notice/Pub_2021_02_pension_income_guide_04_06_21.pdf#page=5
- title: Rhode Island Division of Taxation Publication 2024-01, pension and annuity income modification
href: https://tax.ri.gov/sites/g/files/xkgbur541/files/2024-03/PUB_2024-01_retirement_income_guide.pdf#page=7
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""Non-spouse military survivor benefits stay in Michigan's tax base.

Each year uses one vectorized simulation of paired tax units. The pairs
share every input except increasing military survivor benefits, including
benefits paid to dependents. Seeded incomes span the retirement deduction
caps and ages span Michigan's retirement tiers. This catches an exemption
that incorrectly leaves taxable income flat as well as deductions that
incorrectly count taxable survivor benefits against another deduction.
"""

import numpy as np
import pytest

from policyengine_us import Simulation


SEED = 20261022
TOLERANCE = 0.05 # dollars; model calculations use float32 arrays
FILER_AGES = [20, 40, 59, 62, 65, 67, 70, 75, 80, 85]


def _draw_units(year):
rng = np.random.default_rng(SEED + year)
units = []
for i in range(24):
joint = i % 2 == 1

def filer(age):
return {
"age": age,
"employment_income": float(rng.integers(0, 80_001)),
"taxable_private_pension_income": float(rng.integers(0, 100_001)),
"military_retirement_pay": (
0.0 if i % 3 == 0 else float(rng.integers(0, 50_001))
),
"taxable_interest_income": float(rng.integers(0, 20_001)),
"retirement_benefits_from_ss_exempt_employment": (
10_000.0 if age >= 59 and i % 3 != 0 else 0.0
),
"year_of_retirement": 2008 if i % 4 == 0 else 2018,
"military_retirement_pay_survivors": float(rng.integers(0, 30_001)),
# Unchanged pairs check that units cannot affect each other.
"increment": 0.0 if i % 6 == 0 else float(rng.integers(1, 30_001)),
}

head = filer(FILER_AGES[i % len(FILER_AGES)])
spouse = filer(FILER_AGES[(i + 3) % len(FILER_AGES)]) if joint else None
dependents = [
{
"age": 8 + j * 4,
"military_retirement_pay_survivors": float(rng.integers(0, 10_001)),
"increment": 0.0 if i % 6 == 0 else float(rng.integers(1, 10_001)),
}
for j in range(i % 3)
]
units.append({"head": head, "spouse": spouse, "dependents": dependents})

# Guarantee a young filer above the zero floor, independently of the draw.
units[0] = {
"head": {
"age": 20,
"employment_income": 40_000.0,
"military_retirement_pay_survivors": 0.0,
"increment": 10_000.0,
},
"spouse": None,
"dependents": [],
}
# This pair changes only a dependent's benefits, which belong on their
# own return and must not change the filer's AGI or Michigan deductions.
units[1]["head"]["increment"] = 0.0
units[1]["spouse"]["increment"] = 0.0
units[1]["dependents"][0]["increment"] = 10_000.0
# Guarantee a pair whose small survivor increase remains below the tax
# base's zero floor, alongside the positive-base example above.
units[2]["head"] = {
"age": 20,
"employment_income": 0.0,
"military_retirement_pay_survivors": 0.0,
"increment": 1_000.0,
}
return units


def _paired_situation(units, year):
people, tax_units, marital_units = {}, {}, {}
households, families, spm_units = {}, {}, {}
for increased in (False, True):
for i, unit in enumerate(units):
key = f"{int(increased)}_{i}"
members, couple = [], []

def add_person(role, record):
name = f"{role}_{key}"
members.append(name)
person = {
variable: {year: amount}
for variable, amount in record.items()
if variable != "increment"
}
benefits = record["military_retirement_pay_survivors"]
person["military_retirement_pay_survivors"] = {
year: benefits + increased * record["increment"]
}
person.update(
{
"is_surviving_spouse": {year: False},
"is_tax_unit_head": {year: role == "head"},
"is_tax_unit_spouse": {year: role == "spouse"},
"is_tax_unit_dependent": {year: role.startswith("dependent")},
}
)
people[name] = person
return name

couple.append(add_person("head", unit["head"]))
if unit["spouse"] is not None:
couple.append(add_person("spouse", unit["spouse"]))
marital_units[f"couple_{key}"] = {"members": couple}
for j, record in enumerate(unit["dependents"]):
name = add_person(f"dependent_{j}", record)
marital_units[f"dependent_{key}_{j}"] = {"members": [name]}
tax_units[f"tax_unit_{key}"] = {"members": members}
households[f"household_{key}"] = {
"members": members,
"state_name": {year: "MI"},
}
families[f"family_{key}"] = {"members": members}
spm_units[f"spm_unit_{key}"] = {"members": members}
return {
"people": people,
"tax_units": tax_units,
"marital_units": marital_units,
"households": households,
"families": families,
"spm_units": spm_units,
}


@pytest.mark.parametrize("year", [2024, 2025, 2026])
def test_nonspouse_survivor_benefits_never_reduce_michigan_taxable_income(year):
units = _draw_units(year)
sim = Simulation(situation=_paired_situation(units, year))
count = len(units)

def paired(variable):
amounts = sim.calculate(variable, year)
return amounts[:count], amounts[count:]

increments = np.array(
[
unit["head"]["increment"]
+ (unit["spouse"]["increment"] if unit["spouse"] is not None else 0)
for unit in units
]
)
# Child benefits are excluded from the filer's federal AGI; every dollar
# paid to the head or spouse is federally taxable on this return.
agi_before, agi_after = paired("adjusted_gross_income")
np.testing.assert_allclose(
agi_after - agi_before, increments, rtol=0, atol=TOLERANCE
)
for variable in ("mi_subtractions", "mi_exemptions"):
before, after = paired(variable)
np.testing.assert_allclose(
after,
before,
rtol=0,
atol=TOLERANCE,
err_msg=f"Non-spouse survivor benefits changed {variable}",
)

before, after = paired("mi_taxable_income")
assert (after >= before - TOLERANCE).all()
above_floor = before > TOLERANCE
np.testing.assert_allclose(
after[above_floor] - before[above_floor],
increments[above_floor],
rtol=0,
atol=TOLERANCE,
err_msg="Non-spouse benefits must increase a positive Michigan tax base",
)
unchanged_filer_income = increments == 0
np.testing.assert_allclose(
after[unchanged_filer_income],
before[unchanged_filer_income],
rtol=0,
atol=TOLERANCE,
)
assert (above_floor & (increments > 0)).any()
assert ((before == 0) & (after == 0) & (increments > 0)).any()
assert unchanged_filer_income.any()
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,32 @@
state_code: AZ
output:
az_military_retirement_subtraction: 9_000

- name: Arizona excludes a surviving spouse's military survivor benefits.
period: 2024
input:
state_code: AZ
is_surviving_spouse: true
military_retirement_pay_survivors: 10_000
output:
az_military_retirement_subtraction: 10_000

- name: Arizona does not exclude a non-spouse survivor's military benefits.
period: 2024
input:
state_code: AZ
age: 20
is_surviving_spouse: false
military_retirement_pay_survivors: 10_000
output:
az_military_retirement_subtraction: 0

- name: Arizona retains own military retirement pay without excluding non-spouse survivor benefits.
period: 2024
input:
state_code: AZ
is_surviving_spouse: false
military_retirement_pay: 30_000
military_retirement_pay_survivors: 10_000
output:
az_military_retirement_subtraction: 30_000
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,36 @@
state_code: ID
output:
id_retirement_benefits_deduction: 30_000

- name: Idaho general retirement deduction excludes non-spouse military survivor benefits before 2025.
period: 2024
input:
state_code: ID
age: 65
is_surviving_spouse: false
military_retirement_pay_survivors: 10_000
output:
id_retirement_benefits_deduction_relevant_income: 0
id_retirement_benefits_deduction: 0

- name: Idaho general retirement deduction includes an unremarried surviving spouse before 2025.
period: 2024
input:
state_code: ID
age: 65
is_surviving_spouse: true
military_retirement_pay_survivors: 10_000
output:
id_retirement_benefits_deduction_relevant_income: 10_000
id_retirement_benefits_deduction: 10_000

- name: Idaho general retirement deduction retains own military pay before 2025.
period: 2024
input:
state_code: ID
age: 65
is_surviving_spouse: false
military_retirement_pay: 10_000
output:
id_retirement_benefits_deduction_relevant_income: 10_000
id_retirement_benefits_deduction: 10_000
Loading
Loading