From f994b5b7181894f6c502c7df1e503221f8f9dcd2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 8 Nov 2026 03:51:19 -0500 Subject: [PATCH] Apply Idaho's 2025 eligibility tests and cap to military retired pay Idaho Code 63-3022A(1)(b), as amended by 2025 House Bills 40 and 479 (retroactive to January 1, 2025), counts military retirement benefits toward the retirement benefits deduction for a recipient who is disabled, is 62 or older by the end of the year, or worked enough to have to file a federal return. The benefits share the deduction's cap, which the taxpayer's and spouse's Social Security and Railroad Retirement benefits reduce (63-3022A(2) and (3); Form 39R line 8). From 2025 the model subtracted all of a head's or spouse's military retired pay through a separate id_military_retirement_deduction, at any age and with no cap. Military pay now runs through id_retirement_benefits_deduction with its own eligibility test, and the separate deduction and its applies parameter are removed. Co-Authored-By: Claude Opus 5.5 --- ...-id-military-retirement-deduction.fixed.md | 1 + ...d-military-retirement-deduction.removed.md | 1 + .../retirement_benefits/income_sources.yaml | 9 +- .../military/age_eligibility/disabled.yaml | 23 ++ .../military/age_eligibility/main.yaml | 23 ++ .../military/employment/in_effect.yaml | 18 + .../military/employment/income_sources.yaml | 24 ++ .../military_retirement/applies.yaml | 14 - .../tax/income/subtractions/subtractions.yaml | 14 - ...ilitary_retirement_deduction_invariants.py | 384 ++++++++++++++++++ ...d_eligible_military_retirement_income.yaml | 103 +++++ ...ilitary_retirement_benefits_deduction.yaml | 198 +++++++++ ...ts_deduction_military_eligible_person.yaml | 109 +++++ ...eduction_military_employment_eligible.yaml | 201 +++++++++ .../id_military_retirement_deduction.yaml | 186 --------- ..._military_retirement_hb40_integration.yaml | 156 ------- .../id_eligible_military_retirement_income.py | 0 ...fits_deduction_military_eligible_person.py | 41 ++ ..._deduction_military_employment_eligible.py | 79 ++++ ...ment_benefits_deduction_relevant_income.py | 28 +- .../id_military_retirement_deduction.py | 27 -- 21 files changed, 1234 insertions(+), 405 deletions(-) create mode 100644 changelog.d/fix-id-military-retirement-deduction.fixed.md create mode 100644 changelog.d/fix-id-military-retirement-deduction.removed.md create mode 100644 policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/age_eligibility/disabled.yaml create mode 100644 policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/age_eligibility/main.yaml create mode 100644 policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/employment/in_effect.yaml create mode 100644 policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/employment/income_sources.yaml delete mode 100644 policyengine_us/parameters/gov/states/id/tax/income/subtractions/military_retirement/applies.yaml create mode 100644 policyengine_us/tests/core/test_id_military_retirement_deduction_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_eligible_military_retirement_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_military_retirement_benefits_deduction.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_eligible_person.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_employment_eligible.yaml delete mode 100644 policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_deduction.yaml delete mode 100644 policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_hb40_integration.yaml rename policyengine_us/variables/gov/states/id/tax/income/{subtractions/military_retirement => deductions/retirement_benefits}/id_eligible_military_retirement_income.py (100%) create mode 100644 policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_eligible_person.py create mode 100644 policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_employment_eligible.py delete mode 100644 policyengine_us/variables/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_deduction.py diff --git a/changelog.d/fix-id-military-retirement-deduction.fixed.md b/changelog.d/fix-id-military-retirement-deduction.fixed.md new file mode 100644 index 00000000000..bc38df4153e --- /dev/null +++ b/changelog.d/fix-id-military-retirement-deduction.fixed.md @@ -0,0 +1 @@ +Apply Idaho's 2025 eligibility tests to military retirement benefits (disabled, age 62 or older, or working enough to file a federal return) and count them within the retirement benefits deduction's cap and Social Security offset, under Idaho Code 63-3022A as amended by House Bills 40 and 479. diff --git a/changelog.d/fix-id-military-retirement-deduction.removed.md b/changelog.d/fix-id-military-retirement-deduction.removed.md new file mode 100644 index 00000000000..09d119cadf5 --- /dev/null +++ b/changelog.d/fix-id-military-retirement-deduction.removed.md @@ -0,0 +1 @@ +Remove `id_military_retirement_deduction` and its parameter `gov.states.id.tax.income.subtractions.military_retirement.applies`; military retirement benefits now count in `id_retirement_benefits_deduction`. diff --git a/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/income_sources.yaml b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/income_sources.yaml index f4a8c8f7f81..cd06633d930 100644 --- a/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/income_sources.yaml +++ b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/income_sources.yaml @@ -1,11 +1,10 @@ -description: Idaho counts the following income sources under the retirement benefits deduction. +description: Idaho counts these income sources, other than military retirement benefits, under the retirement benefits deduction for recipients who meet its age test. values: + # Idaho Code 63-3022A(1)(a). Military retirement benefits + # (id_eligible_military_retirement_income) have their own eligibility + # test, in id_retirement_benefits_deduction_military_eligible_person. 2021-01-01: - public_pension_income - - id_eligible_military_retirement_income - - 2025-01-01: - - public_pension_income metadata: unit: list label: Idaho retirement benefits deduction income sources diff --git a/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/age_eligibility/disabled.yaml b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/age_eligibility/disabled.yaml new file mode 100644 index 00000000000..d3538163e60 --- /dev/null +++ b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/age_eligibility/disabled.yaml @@ -0,0 +1,23 @@ +description: Idaho limits the retirement benefits deduction for military retirement benefits to disabled recipients this age or older. +values: + # Before 2025, a disabled recipient qualified at 62, as for the other + # retirement benefits in Idaho Code 63-3022A(1). + 2021-01-01: 62 + # From 2025, 63-3022A(1)(b)(i) admits a recipient who "Is classified as + # disabled", at any age. + 2025-01-01: 0 +metadata: + unit: year + period: year + label: Idaho military retirement benefits deduction disabled age threshold + reference: + - title: Idaho Code 63-3022A(1)(b) and (4) + href: https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/ + - title: 2025 Idaho House Bill 40, section 2 (63-3022A as in effect for 2024, with the 2025 amendment) + href: https://legislature.idaho.gov/wp-content/uploads/sessioninfo/2025/legislation/H0040.pdf#page=5 + - title: 2025 Idaho House Bill 479, section 1 + href: https://legislature.idaho.gov/wp-content/uploads/sessioninfo/2025/legislation/H0479.pdf#page=2 + - title: Idaho Individual Income Tax Instructions Packet 2024, Form 39R line 8 + href: https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_10-23-2024.pdf#page=31 + - title: Idaho Individual Income Tax Instructions Packet 2025, Form 39R line 8e ("Classified as disabled (any age)") + href: https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=45 diff --git a/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/age_eligibility/main.yaml b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/age_eligibility/main.yaml new file mode 100644 index 00000000000..17a53423f2c --- /dev/null +++ b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/age_eligibility/main.yaml @@ -0,0 +1,23 @@ +description: Idaho limits the retirement benefits deduction for military retirement benefits to recipients this age or older. +values: + # Before 2025, military retirement benefits shared the age test of the + # other retirement benefits in Idaho Code 63-3022A(1). + 2021-01-01: 65 + # From 2025, 63-3022A(1)(b)(ii): "Has attained the age of sixty-two (62) + # years by the end of the tax year". + 2025-01-01: 62 +metadata: + unit: year + period: year + label: Idaho military retirement benefits deduction age threshold + reference: + - title: Idaho Code 63-3022A(1)(b) + href: https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/ + - title: 2025 Idaho House Bill 40, section 2 (63-3022A as in effect for 2024, with the 2025 amendment) + href: https://legislature.idaho.gov/wp-content/uploads/sessioninfo/2025/legislation/H0040.pdf#page=5 + - title: 2025 Idaho House Bill 479, section 1 + href: https://legislature.idaho.gov/wp-content/uploads/sessioninfo/2025/legislation/H0479.pdf#page=2 + - title: Idaho Individual Income Tax Instructions Packet 2024, Form 39R line 8 + href: https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_10-23-2024.pdf#page=31 + - title: Idaho Individual Income Tax Instructions Packet 2025, Form 39R line 8e + href: https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=45 diff --git a/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/employment/in_effect.yaml b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/employment/in_effect.yaml new file mode 100644 index 00000000000..02563433496 --- /dev/null +++ b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/employment/in_effect.yaml @@ -0,0 +1,18 @@ +description: Idaho extends the retirement benefits deduction to the military retirement benefits of a recipient whose employment income requires a federal return, if this is true. +values: + 2021-01-01: false + # 2025 House Bills 40 and 479, retroactive to January 1, 2025. + 2025-01-01: true +metadata: + unit: bool + period: year + label: Idaho military retirement benefits deduction employment test in effect + reference: + - title: Idaho Code 63-3022A(1)(b)(iii) + href: https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/ + - title: 2025 Idaho House Bill 40, sections 2 and 5 + href: https://legislature.idaho.gov/wp-content/uploads/sessioninfo/2025/legislation/H0040.pdf#page=5 + - title: 2025 Idaho House Bill 479, sections 1 and 9 + href: https://legislature.idaho.gov/wp-content/uploads/sessioninfo/2025/legislation/H0479.pdf#page=2 + - title: Idaho Individual Income Tax Instructions Packet 2025, Form 39R line 8e + href: https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=45 diff --git a/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/employment/income_sources.yaml b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/employment/income_sources.yaml new file mode 100644 index 00000000000..e8b914ff5a1 --- /dev/null +++ b/policyengine_us/parameters/gov/states/id/tax/income/deductions/retirement_benefits/military/employment/income_sources.yaml @@ -0,0 +1,24 @@ +description: Idaho counts these income sources as income from employment in the employment test of the military retirement benefits deduction. +values: + # The list applies from 2021 so the formula can read it in every year; + # in_effect.yaml turns the employment test on from 2025. + 2021-01-01: + # Wages: Form 1040 line 1a, as in the federal gross income test. + - irs_employment_income + # Idaho Code 63-3022A(1)(b)(iii) ("was employed during the tax year and + # received sufficient income from such employment") doesn't define + # employment, and the Form 39R line 8e instructions repeat its words. + # PolicyEngine counts business and farm profits as income from + # employment. + - self_employment_income + - sstb_self_employment_income + - farm_operations_income +metadata: + unit: list + period: year + label: Idaho military retirement benefits deduction employment income sources + reference: + - title: Idaho Code 63-3022A(1)(b)(iii) + href: https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/ + - title: Idaho Individual Income Tax Instructions Packet 2025, Form 39R line 8e + href: https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=45 diff --git a/policyengine_us/parameters/gov/states/id/tax/income/subtractions/military_retirement/applies.yaml b/policyengine_us/parameters/gov/states/id/tax/income/subtractions/military_retirement/applies.yaml deleted file mode 100644 index 7124a2ea60b..00000000000 --- a/policyengine_us/parameters/gov/states/id/tax/income/subtractions/military_retirement/applies.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: Idaho provides a separate military retirement deduction without age requirements. -values: - 2021-01-01: false - 2025-01-01: true - -metadata: - unit: bool - period: year - label: Idaho military retirement deduction applies - reference: - - title: Idaho Code 63-3022A - Deduction of Certain Retirement Benefits - href: https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/ - - title: Idaho Individual Income Tax Instructions Packet 2025 - href: https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=45 diff --git a/policyengine_us/parameters/gov/states/id/tax/income/subtractions/subtractions.yaml b/policyengine_us/parameters/gov/states/id/tax/income/subtractions/subtractions.yaml index 1051f9780fb..b04045bca2f 100644 --- a/policyengine_us/parameters/gov/states/id/tax/income/subtractions/subtractions.yaml +++ b/policyengine_us/parameters/gov/states/id/tax/income/subtractions/subtractions.yaml @@ -13,20 +13,6 @@ values: - id_529_plan_subtraction # Idaho Code 63-3022(n) - id_health_insurance_premiums_subtraction # Idaho Code 63-3022P - id_other_subtractions - 2025-01-01: - - salt_refund_income # (2) - - us_govt_interest # (3)(a) - - military_service_income # (4) - - taxable_social_security # (5) - - id_household_and_dependent_care_expense_deduction # (6) - - id_aged_or_disabled_deduction # (8) - - id_capital_gains_deduction # (10) - - workers_compensation # (13) - - id_retirement_benefits_deduction # (15) - - id_military_retirement_deduction # (15) per HB 40 - - id_529_plan_subtraction # Idaho Code 63-3022(n) - - id_health_insurance_premiums_subtraction # Idaho Code 63-3022P - - id_other_subtractions metadata: diff --git a/policyengine_us/tests/core/test_id_military_retirement_deduction_invariants.py b/policyengine_us/tests/core/test_id_military_retirement_deduction_invariants.py new file mode 100644 index 00000000000..a80071c2408 --- /dev/null +++ b/policyengine_us/tests/core/test_id_military_retirement_deduction_invariants.py @@ -0,0 +1,384 @@ +"""Invariants of Idaho's retirement benefits deduction on military retired pay. + +Idaho Code 63-3022A(1)(b), as amended by 2025 House Bills 40 and 479 with +effect from January 1, 2025, counts retirement benefits the United States pays +to a retired member of the military services, or to the member's unremarried +widow or widower, who: + + (i) is classified as disabled; + (ii) has attained the age of 62 by the end of the tax year; or + (iii) was employed during the tax year and received sufficient income from + such employment to be required to file a federal return under + 26 U.S.C. 6012(a)(1). + +Before 2025 the benefits had the age test of 63-3022A(1)(a): age 65, or age 62 +and classified as disabled. In every year they share the deduction's cap, +63-3022A(2) ($48,216 single and $72,324 joint for 2025, Form 39R line 8a, and +nothing on a separate return), which the Social Security and Railroad +Retirement benefits of the taxpayer and spouse reduce, 63-3022A(3). + +The reference rules below restate the law from published amounts, not from +the model's parameters. `test_grid` runs one simulation over 2024 to 2026 for +every filing status; ages on both sides of 62 and 65; disability and +blindness; wages or business profit a dollar under, at and over each year's +filing threshold; and military retired pay, Social Security and a public +pension: + +1. Differential: on a return whose only gross income is the retiree's wages + or business profit, the employment test holds exactly when + tax_unit_is_required_to_file does and the retiree has such income, in + 2025 and 2026. Both also match the published thresholds: section 6012(f) + for 2025 (more than the standard deduction, which adds amounts for age and + blindness) and section 6012(a)(1)(A) and (B) for 2026 (at least the basic + standard deduction plus amounts for age alone). +2. Eligibility: military retired pay counts for exactly the people the + statute's tests admit, restated here, in every year. +3. Cap and offset: in 2025 the deduction equals the lesser of the counted + benefits and the published cap less the head's and spouse's Social + Security benefits, and never falls below zero. +4. Monotonicity: more military retired pay never lowers the deduction and + never raises it by more than the added pay. For a recipient the tests + don't admit, it doesn't change the deduction. + +`test_drawn_units` checks properties 2 to 4 on batches of units Hypothesis +draws, with any mix of these inputs and business losses. +""" + +from itertools import product + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEARS = (2024, 2025, 2026) +STATUSES = ("SINGLE", "HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE", "SEPARATE", "JOINT") +UNMARRIED = ("SINGLE", "HEAD_OF_HOUSEHOLD") + +# Basic standard deduction and the additional standard deduction for each of +# age 65 and blindness. 2025: Chart A of the 2025 Form 1040 instructions, +# https://www.irs.gov/pub/irs-prior/i1040gi--2025.pdf. 2026: Rev. Proc. +# 2025-32, sec. 4.14, https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=18 +BASIC = { + 2025: { + "SINGLE": 15_750, + "HEAD_OF_HOUSEHOLD": 23_625, + "SURVIVING_SPOUSE": 31_500, + "SEPARATE": 15_750, + "JOINT": 31_500, + }, + 2026: { + "SINGLE": 16_100, + "HEAD_OF_HOUSEHOLD": 24_150, + "SURVIVING_SPOUSE": 32_200, + "SEPARATE": 16_100, + "JOINT": 32_200, + }, +} +ADDITIONAL = { + 2025: {status: 2_000 if status in UNMARRIED else 1_600 for status in STATUSES}, + 2026: {status: 2_050 if status in UNMARRIED else 1_650 for status in STATUSES}, +} +# Form 39R line 8a for 2025: $48,216 single, $72,324 joint. 63-3022A(2)(b) +# and (c) give unmarried taxpayers and unremarried widows and widowers the +# single amount; (2)(e) gives a married taxpayer filing separately nothing. +# https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=45 +CAP_2025 = { + "SINGLE": 48_216, + "HEAD_OF_HOUSEHOLD": 48_216, + "SURVIVING_SPOUSE": 48_216, + "SEPARATE": 0, + "JOINT": 72_324, +} + + +def employment_test(year, status, income, aged_count, blind_count): + """63-3022A(1)(b)(iii) with the published filing thresholds.""" + if year < 2025 or income <= 0: + return False + if status == "SEPARATE": + # Any gross income requires a separate return, in both years. + return True + if year == 2025: + # Section 6012(f): gross income over the standard deduction. + threshold = BASIC[year][status] + ADDITIONAL[year][status] * ( + aged_count + blind_count + ) + return income > threshold + # Section 6012(a)(1)(A) and (B): blindness doesn't raise the threshold. + threshold = BASIC[year][status] + ADDITIONAL[year][status] * aged_count + return income >= threshold + + +def general_age_test(age, disabled): + """63-3022A(1)(a): age 65, or age 62 and classified as disabled.""" + return age >= 65 or (disabled and age >= 62) + + +def military_test(year, age, disabled, employed): + """Whether military retired pay counts toward the deduction.""" + if year < 2025: + return general_age_test(age, disabled) + return disabled or age >= 62 or employed + + +def simulate(units): + """One simulation of independent Idaho tax units. + + Each unit is {"filing_status": name, "adults": [inputs, ...]}; the first + adult is the retiree. Inputs are the same in every year. + """ + people, tax_units, households = {}, {}, {} + groups = {"marital_units": {}, "spm_units": {}, "families": {}} + for index, unit in enumerate(units): + members = [f"adult_{index}_{n}" for n in range(len(unit["adults"]))] + for name, inputs in zip(members, unit["adults"]): + people[name] = { + variable: {year: value for year in YEARS} + for variable, value in inputs.items() + } + tax_units[f"tax_unit_{index}"] = { + "members": members, + "filing_status": {year: unit["filing_status"] for year in YEARS}, + } + households[f"household_{index}"] = { + "members": members, + "state_code": {year: "ID" for year in YEARS}, + } + for name in ("marital_units", "spm_units", "families"): + groups[name][f"{name}_{index}"] = {"members": members} + return Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "households": households, + **groups, + } + ) + + +def adult(age, disabled=False, blind=False, wages=0, profit=0, military=0, ss=0): + return { + "age": age, + "is_disabled": disabled, + "is_blind": blind, + "employment_income": wages, + "self_employment_income": profit, + "military_retirement_pay": military, + "social_security_retirement": ss, + } + + +def unit(status, retiree, spouse_age=None, pension=0): + adults = [dict(retiree, public_pension_income=pension)] + if status == "JOINT": + adults.append(adult(spouse_age)) + return {"filing_status": status, "adults": adults} + + +def first_rows(units): + sizes = [len(u["adults"]) for u in units] + return np.cumsum([0] + sizes[:-1]) + + +def results(simulation, units, year): + """Person results for the retiree, or every adult, and tax unit results.""" + rows = first_rows(units) + + def calculate(variable): + return np.asarray(simulation.calculate(variable, year)) + + relevant = calculate("id_retirement_benefits_deduction_relevant_income") + return { + "employment": calculate( + "id_retirement_benefits_deduction_military_employment_eligible" + )[rows], + "military": calculate( + "id_retirement_benefits_deduction_military_eligible_person" + ), + "relevant": np.add.reduceat(relevant, rows), + "deduction": calculate("id_retirement_benefits_deduction"), + "required": calculate("tax_unit_is_required_to_file"), + } + + +def reference_counted(year, u, employed): + """Benefits the statute counts toward the deduction, restated.""" + retiree = u["adults"][0] + counted = 0 + if military_test(year, retiree["age"], retiree["is_disabled"], employed): + counted += retiree["military_retirement_pay"] + if general_age_test(retiree["age"], retiree["is_disabled"]): + counted += retiree["public_pension_income"] + return counted + + +def unit_employment_reference(year, u): + retiree = u["adults"][0] + income = max(retiree["employment_income"], 0) + max( + retiree["self_employment_income"], 0 + ) + aged = sum(a["age"] >= 65 for a in u["adults"]) + blind = sum(a["is_blind"] for a in u["adults"]) + return employment_test(year, u["filing_status"], income, aged, blind) + + +def check_units(simulation, units, check_cap=True): + """Properties 2 to 4 for every unit and year.""" + rows = first_rows(units) + for year in YEARS: + r = results(simulation, units, year) + employed = [unit_employment_reference(year, u) for u in units] + np.testing.assert_array_equal(r["employment"], employed, err_msg=str(year)) + # Property 2, for every adult: the spouse has no income of their own. + expected_military = [] + for u, e in zip(units, employed): + for n, a in enumerate(u["adults"]): + own_employment = e if n == 0 else False + expected_military.append( + military_test(year, a["age"], a["is_disabled"], own_employment) + ) + np.testing.assert_array_equal( + r["military"], expected_military, err_msg=str(year) + ) + counted = np.array( + [reference_counted(year, u, e) for u, e in zip(units, employed)] + ) + np.testing.assert_allclose(r["relevant"], counted, err_msg=str(year)) + assert (r["deduction"] >= 0).all() + assert (r["deduction"] <= counted + 1e-6).all() + if year == 2025 and check_cap: + ss = np.array( + [ + sum(a["social_security_retirement"] for a in u["adults"]) + for u in units + ] + ) + cap = np.array([CAP_2025[u["filing_status"]] for u in units]) + expected = np.minimum(counted, np.maximum(cap - ss, 0)) + np.testing.assert_allclose(r["deduction"], expected, err_msg="2025 cap") + return rows + + +AGES = (45, 61, 62, 64, 65, 67) +MILITARY = (0, 30_000, 60_000) + + +def grid_units(): + """Units for properties 2 to 4, in groups that differ only in military pay.""" + units, groups = [], [] + shapes = product( + STATUSES, AGES, (False, True), (0, 10_000, 60_000), (0, 20_000), (0, 20_000) + ) + for status, age, disabled, wages, ss, pension in shapes: + group = [] + for military in MILITARY: + retiree = adult( + age, disabled=disabled, wages=wages, military=military, ss=ss + ) + group.append(len(units)) + units.append(unit(status, retiree, spouse_age=43, pension=pension)) + groups.append(group) + return units, groups + + +def differential_units(): + """Returns whose only gross income is the retiree's wages or profit.""" + units = [] + for status, age, spouse_age, blind, kind in product( + STATUSES, (45, 61), (43, 66), (False, True), ("wages", "profit") + ): + if status != "JOINT" and spouse_age == 66: + continue + aged = int(status == "JOINT" and spouse_age >= 65) + amounts = {0, 1} + for year in (2025, 2026): + threshold = BASIC[year][status] + ADDITIONAL[year][status] * ( + aged + (blind if year == 2025 else 0) + ) + amounts |= {threshold - 1, threshold, threshold + 1} + for amount in sorted(amounts): + retiree = adult(age, blind=blind, **{kind: amount}) + units.append(unit(status, retiree, spouse_age=spouse_age)) + return units + + +def test_grid(): + grid, groups = grid_units() + differential = differential_units() + units = grid + differential + simulation = simulate(units) + check_units(simulation, units) + + # Property 1, on the returns with only employment income. + offset = len(grid) + for year in (2025, 2026): + r = results(simulation, units, year) + for i, u in enumerate(differential): + retiree = u["adults"][0] + income = retiree["employment_income"] + retiree["self_employment_income"] + required = bool(r["required"][offset + i]) and income > 0 + assert bool(r["employment"][offset + i]) == required, (year, u) + assert required == unit_employment_reference(year, u), (year, u) + + # Property 4, across each group's military pay. + for year in YEARS: + r = results(simulation, units, year) + employed = [unit_employment_reference(year, u) for u in units] + for group in groups: + deductions = r["deduction"][group] + pay = np.array(MILITARY) + assert (np.diff(deductions) >= -1e-6).all(), (year, units[group[0]]) + assert (np.diff(deductions) <= np.diff(pay) + 1e-6).all() + retiree = units[group[0]]["adults"][0] + if not military_test( + year, retiree["age"], retiree["is_disabled"], employed[group[0]] + ): + assert np.ptp(deductions) == 0, (year, units[group[0]]) + + +@st.composite +def drawn_unit(draw): + status = draw(st.sampled_from(STATUSES)) + retiree = adult( + draw(st.integers(30, 90)), + disabled=draw(st.booleans()), + blind=draw(st.booleans()), + wages=draw(st.sampled_from([0, 5_000, 16_100, 32_200, 33_850, 90_000])), + profit=draw(st.integers(-20_000, 60_000)), + military=draw(st.integers(0, 90_000)), + ss=draw(st.sampled_from([0, 12_000, 30_000, 80_000])), + ) + spouse_age = draw(st.integers(30, 90)) + pension = draw(st.sampled_from([0, 20_000])) + military_extra = draw(st.integers(1, 40_000)) + base = unit(status, retiree, spouse_age=spouse_age, pension=pension) + more = unit( + status, + dict( + retiree, + military_retirement_pay=retiree["military_retirement_pay"] + military_extra, + ), + spouse_age=spouse_age, + pension=pension, + ) + return base, more, military_extra + + +@settings( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(drawn=st.lists(drawn_unit(), min_size=8, max_size=20)) +def test_drawn_units(drawn): + units = [u for base, more, _ in drawn for u in (base, more)] + simulation = simulate(units) + check_units(simulation, units) + for year in YEARS: + deductions = results(simulation, units, year)["deduction"] + for i, (base, _, extra) in enumerate(drawn): + low, high = deductions[2 * i], deductions[2 * i + 1] + assert low - 1e-6 <= high <= low + extra + 1e-6, (year, base) diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_eligible_military_retirement_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_eligible_military_retirement_income.yaml new file mode 100644 index 00000000000..4b4d9511357 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_eligible_military_retirement_income.yaml @@ -0,0 +1,103 @@ +# Idaho Code 63-3022A(1)(b) covers retirement benefits paid to a retired +# member of the military services or to the member's unremarried widow or +# widower. + +- name: An unremarried surviving spouse counts military survivor benefits. + period: 2025 + input: + state_code: ID + age: 65 + is_surviving_spouse: true + military_retirement_pay_survivors: 25_000 + output: + id_eligible_military_retirement_income: 25_000 + id_retirement_benefits_deduction: 25_000 + +- name: A survivor who is not a surviving spouse does not count survivor benefits. + period: 2025 + input: + state_code: ID + age: 65 + is_surviving_spouse: false + military_retirement_pay_survivors: 25_000 + output: + id_eligible_military_retirement_income: 0 + id_retirement_benefits_deduction: 0 + +- name: A recipient keeps own retired pay when non-spouse survivor benefits are present. + period: 2025 + input: + state_code: ID + age: 65 + is_surviving_spouse: false + military_retirement_pay: 30_000 + military_retirement_pay_survivors: 25_000 + output: + id_eligible_military_retirement_income: 30_000 + id_retirement_benefits_deduction: 30_000 + +- name: A remarried survivor filing separately does not count survivor benefits. + period: 2025 + input: + people: + survivor: + age: 65 + is_surviving_spouse: true + military_retirement_pay_survivors: 25_000 + new_spouse: + age: 65 + families: + family: + members: [survivor, new_spouse] + marital_units: + couple: + members: [survivor, new_spouse] + tax_units: + survivor_return: + members: [survivor] + filing_status: SEPARATE + spouse_return: + members: [new_spouse] + filing_status: SEPARATE + households: + household: + members: [survivor, new_spouse] + state_code: ID + output: + id_eligible_military_retirement_income: [0, 0] + id_retirement_benefits_deduction: [0, 0] + +- name: An unmarried widow stays eligible when a married adult child shares the family. + period: 2025 + input: + people: + survivor: + age: 65 + is_surviving_spouse: true + military_retirement_pay_survivors: 25_000 + adult_child: + age: 40 + child_spouse: + age: 40 + families: + family: + members: [survivor, adult_child, child_spouse] + marital_units: + widow: + members: [survivor] + couple: + members: [adult_child, child_spouse] + tax_units: + survivor_return: + members: [survivor] + filing_status: SINGLE + child_return: + members: [adult_child, child_spouse] + filing_status: JOINT + households: + household: + members: [survivor, adult_child, child_spouse] + state_code: ID + output: + id_eligible_military_retirement_income: [25_000, 0, 0] + id_retirement_benefits_deduction: [25_000, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_military_retirement_benefits_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_military_retirement_benefits_deduction.yaml new file mode 100644 index 00000000000..d2aac61086f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_military_retirement_benefits_deduction.yaml @@ -0,0 +1,198 @@ +# Military retirement benefits in the Idaho retirement benefits deduction +# (Idaho Code 63-3022A; Form 39R line 8). From 2025, 63-3022A(1)(b) admits +# them for a recipient who is disabled, is 62 or older, or works enough to +# have to file a federal return. They share the deduction's cap ($48,216 +# single and $72,324 joint for 2025, $0 on a separate return), which Social +# Security and Railroad Retirement benefits reduce (63-3022A(2) and (3)). + +- name: 2024 a 40-year-old cannot deduct military retired pay. + period: 2024 + input: + state_code: ID + age: 40 + military_retirement_pay: 30_000 + output: + id_retirement_benefits_deduction_eligible_person: false + id_retirement_benefits_deduction_military_eligible_person: false + id_retirement_benefits_deduction_relevant_income: 0 + id_retirement_benefits_deduction: 0 + +- name: 2024 a 66-year-old deducts military retired pay. + period: 2024 + input: + state_code: ID + age: 66 + military_retirement_pay: 30_000 + output: + id_retirement_benefits_deduction_military_eligible_person: true + id_retirement_benefits_deduction_relevant_income: 30_000 + id_retirement_benefits_deduction: 30_000 + +- name: 2025 a 40-year-old without a job or disability cannot deduct military retired pay. + period: 2025 + input: + state_code: ID + age: 40 + military_retirement_pay: 30_000 + output: + id_retirement_benefits_deduction_military_eligible_person: false + id_retirement_benefits_deduction_relevant_income: 0 + id_retirement_benefits_deduction: 0 + +- name: 2025 a working 40-year-old deducts military retired pay. + period: 2025 + input: + state_code: ID + age: 40 + employment_income: 50_000 + military_retirement_pay: 30_000 + output: + id_retirement_benefits_deduction_military_eligible_person: true + id_retirement_benefits_deduction_relevant_income: 30_000 + id_retirement_benefits_deduction: 30_000 + +- name: 2025 a disabled 40-year-old deducts military retired pay. + period: 2025 + input: + state_code: ID + age: 40 + is_disabled: true + military_retirement_pay: 30_000 + output: + id_retirement_benefits_deduction_relevant_income: 30_000 + id_retirement_benefits_deduction: 30_000 + +- name: 2025 a 40-year-old with a public pension still needs the general age test. + period: 2025 + input: + state_code: ID + age: 40 + employment_income: 50_000 + public_pension_income: 20_000 + output: + id_retirement_benefits_deduction_relevant_income: 0 + id_retirement_benefits_deduction: 0 + +- name: 2025 military retired pay and a public pension share the $48,216 single cap. + period: 2025 + input: + state_code: ID + age: 66 + military_retirement_pay: 40_000 + public_pension_income: 20_000 + output: + id_retirement_benefits_deduction_relevant_income: 60_000 + id_retirement_benefits_deduction: 48_216 + +- name: 2025 Social Security benefits reduce the cap on military retired pay. + period: 2025 + input: + state_code: ID + age: 66 + military_retirement_pay: 40_000 + social_security_retirement: 20_000 + output: + # 48,216 - 20,000 + id_retirement_benefits_deduction: 28_216 + +- name: 2025 working couple deducts all of the retiree's $30,000. + period: 2025 + input: + people: + retiree: + age: 45 + employment_income: 60_000 + military_retirement_pay: 30_000 + spouse: + age: 43 + tax_units: + tax_unit: + members: [retiree, spouse] + filing_status: JOINT + households: + household: + members: [retiree, spouse] + state_code: ID + output: + id_retirement_benefits_deduction_relevant_income: [30_000, 0] + id_retirement_benefits_deduction: 30_000 + +- name: 2025 retired couple's Social Security reduces the $72,324 joint cap. + period: 2025 + input: + people: + retiree: + age: 67 + military_retirement_pay: 45_000 + social_security_retirement: 30_000 + spouse: + age: 65 + tax_units: + tax_unit: + members: [retiree, spouse] + filing_status: JOINT + households: + household: + members: [retiree, spouse] + state_code: ID + output: + id_retirement_benefits_deduction_relevant_income: [45_000, 0] + # 72,324 - 30,000 + id_retirement_benefits_deduction: 42_324 + +- name: 2025 a married retiree filing separately cannot deduct military retired pay. + period: 2025 + input: + people: + retiree: + age: 66 + military_retirement_pay: 30_000 + spouse: + age: 66 + marital_units: + couple: + members: [retiree, spouse] + tax_units: + retiree_return: + members: [retiree] + filing_status: SEPARATE + spouse_return: + members: [spouse] + filing_status: SEPARATE + households: + household: + members: [retiree, spouse] + state_code: ID + output: + id_retirement_benefits_deduction: [0, 0] + +- name: 2025 an unremarried widow under 62 without a job or disability cannot deduct survivor benefits. + period: 2025 + input: + state_code: ID + age: 50 + is_surviving_spouse: true + military_retirement_pay_survivors: 25_000 + output: + id_retirement_benefits_deduction: 0 + +- name: 2025 an unremarried widow aged 62 deducts survivor benefits. + period: 2025 + input: + state_code: ID + age: 62 + is_surviving_spouse: true + military_retirement_pay_survivors: 25_000 + output: + id_retirement_benefits_deduction: 25_000 + +- name: 2025 the deduction reduces Idaho AGI. + period: 2025 + input: + state_code: ID + age: 40 + employment_income: 50_000 + military_retirement_pay: 30_000 + output: + id_retirement_benefits_deduction: 30_000 + id_subtractions: 30_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_eligible_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_eligible_person.yaml new file mode 100644 index 00000000000..7fb50b72a3d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_eligible_person.yaml @@ -0,0 +1,109 @@ +# Idaho Code 63-3022A(1)(b). Before 2025, military retirement benefits had +# the age test of 63-3022A(1)(a): age 65, or age 62 and disabled. From 2025: +# disabled at any age, age 62 by the end of the year, or the employment test. + +- name: 2024 a 64-year-old does not qualify. + period: 2024 + input: + state_code: ID + age: 64 + output: + id_retirement_benefits_deduction_military_eligible_person: false + +- name: 2024 a 65-year-old qualifies. + period: 2024 + input: + state_code: ID + age: 65 + output: + id_retirement_benefits_deduction_military_eligible_person: true + +- name: 2024 a disabled 62-year-old qualifies. + period: 2024 + input: + state_code: ID + age: 62 + is_disabled: true + output: + id_retirement_benefits_deduction_military_eligible_person: true + +- name: 2024 a disabled 61-year-old does not qualify. + period: 2024 + input: + state_code: ID + age: 61 + is_disabled: true + output: + id_retirement_benefits_deduction_military_eligible_person: false + +- name: 2024 a working 45-year-old does not qualify. + period: 2024 + input: + state_code: ID + age: 45 + employment_income: 100_000 + output: + id_retirement_benefits_deduction_military_eligible_person: false + +- name: 2025 a 62-year-old qualifies. + period: 2025 + input: + state_code: ID + age: 62 + output: + id_retirement_benefits_deduction_military_eligible_person: true + +- name: 2025 a 61-year-old without a job or disability does not qualify. + period: 2025 + input: + state_code: ID + age: 61 + output: + id_retirement_benefits_deduction_military_eligible_person: false + +- name: 2025 a disabled 40-year-old qualifies. + period: 2025 + input: + state_code: ID + age: 40 + is_disabled: true + output: + id_retirement_benefits_deduction_military_eligible_person: true + +- name: 2025 a 45-year-old whose wages require a federal return qualifies. + period: 2025 + input: + state_code: ID + age: 45 + employment_income: 60_000 + output: + id_retirement_benefits_deduction_military_eligible_person: true + +- name: 2025 a 45-year-old whose wages do not require a federal return does not qualify. + period: 2025 + input: + state_code: ID + age: 45 + employment_income: 10_000 + output: + id_retirement_benefits_deduction_military_eligible_person: false + +- name: 2025 a dependent does not qualify, whatever the age. + period: 2025 + input: + people: + head: + age: 45 + parent: + age: 70 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, parent] + filing_status: SINGLE + households: + household: + members: [head, parent] + state_code: ID + output: + id_retirement_benefits_deduction_military_eligible_person: [false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_employment_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_employment_eligible.yaml new file mode 100644 index 00000000000..cf7e06505ce --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_employment_eligible.yaml @@ -0,0 +1,201 @@ +# Idaho Code 63-3022A(1)(b)(iii): employed, with enough income from that +# employment to have to file a federal return. 2025 applies section 6012(f): +# gross income over the standard deduction ($15,750 single and $31,500 joint, +# plus $2,000 unmarried or $1,600 married for each of age 65 and blindness). +# 2026 applies section 6012(a)(1): gross income of at least the basic +# standard deduction ($16,100 single and $32,200 joint, Rev. Proc. 2025-32), +# plus $2,050 unmarried or $1,650 married for each spouse aged 65 or older. + +- name: 2025 single filer with wages equal to the standard deduction does not meet the test. + period: 2025 + input: + state_code: ID + age: 45 + employment_income: 15_750 + output: + id_retirement_benefits_deduction_military_employment_eligible: false + +- name: 2025 single filer with wages a dollar over the standard deduction meets the test. + period: 2025 + input: + state_code: ID + age: 45 + employment_income: 15_751 + output: + id_retirement_benefits_deduction_military_employment_eligible: true + +- name: 2025 blindness raises the single threshold to $17,750. + period: 2025 + input: + state_code: ID + age: 45 + is_blind: true + employment_income: 17_750 + output: + id_retirement_benefits_deduction_military_employment_eligible: false + +- name: 2026 single filer with wages a dollar under the basic standard deduction does not meet the test. + period: 2026 + input: + state_code: ID + age: 45 + employment_income: 16_099 + output: + id_retirement_benefits_deduction_military_employment_eligible: false + +- name: 2026 single filer with wages equal to the basic standard deduction meets the test. + period: 2026 + input: + state_code: ID + age: 45 + employment_income: 16_100 + output: + id_retirement_benefits_deduction_military_employment_eligible: true + +- name: 2026 blindness does not raise the threshold. + period: 2026 + input: + state_code: ID + age: 45 + is_blind: true + employment_income: 16_100 + output: + id_retirement_benefits_deduction_military_employment_eligible: true + +- name: 2026 joint return uses the joint threshold, and a dollar under it fails. + period: 2026 + input: + people: + retiree: + age: 45 + employment_income: 32_199 + spouse: + age: 43 + tax_units: + tax_unit: + members: [retiree, spouse] + filing_status: JOINT + households: + household: + members: [retiree, spouse] + state_code: ID + output: + id_retirement_benefits_deduction_military_employment_eligible: [false, false] + +- name: 2026 joint return with wages equal to the joint threshold meets the test for the employed spouse only. + period: 2026 + input: + people: + retiree: + age: 45 + employment_income: 32_200 + spouse: + age: 43 + tax_units: + tax_unit: + members: [retiree, spouse] + filing_status: JOINT + households: + household: + members: [retiree, spouse] + state_code: ID + output: + id_retirement_benefits_deduction_military_employment_eligible: [true, false] + +- name: 2026 a spouse aged 65 raises the joint threshold to $33,850. + period: 2026 + input: + people: + retiree: + age: 45 + employment_income: 33_849 + spouse: + age: 66 + tax_units: + tax_unit: + members: [retiree, spouse] + filing_status: JOINT + households: + household: + members: [retiree, spouse] + state_code: ID + output: + id_retirement_benefits_deduction_military_employment_eligible: [false, false] + +- name: 2026 joint return with wages at the $33,850 threshold meets the test. + period: 2026 + input: + people: + retiree: + age: 45 + employment_income: 33_850 + spouse: + age: 66 + tax_units: + tax_unit: + members: [retiree, spouse] + filing_status: JOINT + households: + household: + members: [retiree, spouse] + state_code: ID + output: + id_retirement_benefits_deduction_military_employment_eligible: [true, false] + +- name: 2025 a spouse aged 65 raises the joint standard deduction to $33,100. + period: 2025 + input: + people: + retiree: + age: 45 + employment_income: 33_100 + spouse: + age: 66 + tax_units: + tax_unit: + members: [retiree, spouse] + filing_status: JOINT + households: + household: + members: [retiree, spouse] + state_code: ID + output: + id_retirement_benefits_deduction_military_employment_eligible: [false, false] + +- name: Self-employment profit counts as income from employment. + period: 2026 + input: + state_code: ID + age: 45 + self_employment_income: 20_000 + output: + id_retirement_benefits_deduction_military_employment_eligible: true + +- name: A business loss does not reduce wages. + period: 2026 + input: + state_code: ID + age: 45 + employment_income: 20_000 + self_employment_income: -10_000 + output: + id_retirement_benefits_deduction_military_employment_eligible: true + +- name: Pension and investment income are not income from employment. + period: 2026 + input: + state_code: ID + age: 45 + military_retirement_pay: 30_000 + taxable_interest_income: 20_000 + output: + id_retirement_benefits_deduction_military_employment_eligible: false + +- name: The employment test does not apply before 2025. + period: 2024 + input: + state_code: ID + age: 45 + employment_income: 100_000 + output: + id_retirement_benefits_deduction_military_employment_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_deduction.yaml deleted file mode 100644 index 3bb0e02ec91..00000000000 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_deduction.yaml +++ /dev/null @@ -1,186 +0,0 @@ -- name: Military retirement deduction does not apply before 2025 - period: 2024 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 50_000 - age: 40 - tax_units: - tax_unit: - members: [person1] - filing_status: SINGLE - households: - household: - members: [person1] - state_code: ID - output: - id_military_retirement_deduction: 0 - -- name: Military retirement fully exempt for 2025 without age requirement - period: 2025 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 50_000 - age: 40 - tax_units: - tax_unit: - members: [person1] - filing_status: SINGLE - households: - household: - members: [person1] - state_code: ID - output: - id_military_retirement_deduction: 50_000 - -- name: Military retirement exempt for joint filers 2025 - period: 2025 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 30_000 - age: 35 - person2: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 25_000 - age: 38 - tax_units: - tax_unit: - members: [person1, person2] - filing_status: JOINT - households: - household: - members: [person1, person2] - state_code: ID - output: - id_military_retirement_deduction: 55_000 - -- name: Only head or spouse military retirement is exempt - period: 2025 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 30_000 - age: 35 - person2: - is_tax_unit_head_or_spouse: false - military_retirement_pay: 20_000 - age: 60 - tax_units: - tax_unit: - members: [person1, person2] - filing_status: HEAD_OF_HOUSEHOLD - households: - household: - members: [person1, person2] - state_code: ID - output: - id_military_retirement_deduction: 30_000 - -- name: Military retirement survivor benefits for qualifying survivor aged 65 under Idaho Code ยง 63-3022A(1)(b) - period: 2025 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - is_surviving_spouse: true - military_retirement_pay_survivors: 25_000 - age: 65 - tax_units: - tax_unit: - members: [person1] - filing_status: SINGLE - households: - household: - members: [person1] - state_code: ID - output: - id_military_retirement_deduction: 25_000 - -- name: Idaho military retirement deduction excludes a non-spouse survivor. - period: 2025 - input: - state_code: ID - age: 65 - is_surviving_spouse: false - military_retirement_pay_survivors: 25_000 - output: - id_military_retirement_deduction: 0 - -- name: Idaho military retirement deduction keeps own pay when non-spouse survivor benefits are present. - period: 2025 - input: - state_code: ID - age: 65 - is_surviving_spouse: false - military_retirement_pay: 30_000 - military_retirement_pay_survivors: 25_000 - output: - id_military_retirement_deduction: 30_000 - -- name: Idaho military survivor deduction excludes a remarried survivor filing separately. - period: 2025 - input: - people: - survivor: - is_surviving_spouse: true - military_retirement_pay_survivors: 25_000 - new_spouse: {} - families: - family: - members: [survivor, new_spouse] - marital_units: - couple: - members: [survivor, new_spouse] - tax_units: - survivor_return: - members: [survivor] - filing_status: SEPARATE - spouse_return: - members: [new_spouse] - filing_status: SEPARATE - households: - household: - members: [survivor, new_spouse] - state_code: ID - output: - id_military_retirement_deduction: [0, 0] - -- name: Idaho unmarried widow remains eligible when a married adult child shares the family. - period: 2025 - input: - people: - survivor: - age: 65 - is_surviving_spouse: true - military_retirement_pay_survivors: 25_000 - adult_child: - age: 40 - child_spouse: - age: 40 - families: - family: - members: [survivor, adult_child, child_spouse] - marital_units: - widow: - members: [survivor] - couple: - members: [adult_child, child_spouse] - tax_units: - survivor_return: - members: [survivor] - filing_status: SINGLE - child_return: - members: [adult_child, child_spouse] - filing_status: JOINT - households: - household: - members: [survivor, adult_child, child_spouse] - state_code: ID - output: - id_military_retirement_deduction: [25_000, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_hb40_integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_hb40_integration.yaml deleted file mode 100644 index 26495a91af9..00000000000 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_hb40_integration.yaml +++ /dev/null @@ -1,156 +0,0 @@ -- name: Pre-2025 military retirement requires age 65+ for general retirement deduction - period: 2024 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 30_000 - public_pension_income: 0 - social_security_retirement: 0 - age: 40 - is_disabled: false - tax_units: - tax_unit: - members: [person1] - filing_status: SINGLE - households: - household: - members: [person1] - state_code: ID - output: - # Age requirement (65+) not met, so no deduction from general retirement - id_retirement_benefits_deduction_eligible_person: false - id_retirement_benefits_deduction_relevant_income: 0 - id_retirement_benefits_deduction: 0 - # Separate military deduction does not apply until 2025 - id_military_retirement_deduction: 0 - -- name: Pre-2025 military retirement deductible if age 65+ - period: 2024 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 30_000 - public_pension_income: 0 - social_security_retirement: 0 - age: 66 - is_disabled: false - tax_units: - tax_unit: - members: [person1] - filing_status: SINGLE - households: - household: - members: [person1] - state_code: ID - output: - id_retirement_benefits_deduction_eligible_person: true - id_retirement_benefits_deduction_relevant_income: 30_000 - id_retirement_benefits_deduction: 30_000 - id_military_retirement_deduction: 0 - -- name: 2025 military retirement fully exempt regardless of age via separate deduction - period: 2025 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 30_000 - public_pension_income: 0 - social_security_retirement: 0 - age: 40 - is_disabled: false - tax_units: - tax_unit: - members: [person1] - filing_status: SINGLE - households: - household: - members: [person1] - state_code: ID - output: - # Age requirement not met, so no general retirement deduction - id_retirement_benefits_deduction_eligible_person: false - # Military retirement excluded from relevant income for 2025 - id_retirement_benefits_deduction_relevant_income: 0 - id_retirement_benefits_deduction: 0 - # But military retirement fully deductible via separate HB 40 deduction - id_military_retirement_deduction: 30_000 - -- name: 2025 public pension still requires age 65+ for general retirement deduction - period: 2025 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 0 - public_pension_income: 20_000 - social_security_retirement: 0 - age: 40 - is_disabled: false - tax_units: - tax_unit: - members: [person1] - filing_status: SINGLE - households: - household: - members: [person1] - state_code: ID - output: - id_retirement_benefits_deduction_eligible_person: false - id_retirement_benefits_deduction_relevant_income: 0 - id_retirement_benefits_deduction: 0 - id_military_retirement_deduction: 0 - -- name: 2025 public pension deductible if age 65+ - period: 2025 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 0 - public_pension_income: 20_000 - social_security_retirement: 0 - age: 66 - is_disabled: false - tax_units: - tax_unit: - members: [person1] - filing_status: SINGLE - households: - household: - members: [person1] - state_code: ID - output: - id_retirement_benefits_deduction_eligible_person: true - id_retirement_benefits_deduction_relevant_income: 20_000 - id_retirement_benefits_deduction: 20_000 - id_military_retirement_deduction: 0 - -- name: 2025 combined military and public pension with age 66 - period: 2025 - input: - people: - person1: - is_tax_unit_head_or_spouse: true - military_retirement_pay: 25_000 - public_pension_income: 20_000 - social_security_retirement: 0 - age: 66 - is_disabled: false - tax_units: - tax_unit: - members: [person1] - filing_status: SINGLE - households: - household: - members: [person1] - state_code: ID - output: - id_retirement_benefits_deduction_eligible_person: true - # Only public pension in relevant income for 2025 - id_retirement_benefits_deduction_relevant_income: 20_000 - id_retirement_benefits_deduction: 20_000 - # Military retirement fully deductible separately - id_military_retirement_deduction: 25_000 diff --git a/policyengine_us/variables/gov/states/id/tax/income/subtractions/military_retirement/id_eligible_military_retirement_income.py b/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_eligible_military_retirement_income.py similarity index 100% rename from policyengine_us/variables/gov/states/id/tax/income/subtractions/military_retirement/id_eligible_military_retirement_income.py rename to policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_eligible_military_retirement_income.py diff --git a/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_eligible_person.py b/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_eligible_person.py new file mode 100644 index 00000000000..05dcb0029fb --- /dev/null +++ b/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_eligible_person.py @@ -0,0 +1,41 @@ +from policyengine_us.model_api import * + + +class id_retirement_benefits_deduction_military_eligible_person(Variable): + value_type = bool + entity = Person + label = "Eligible person for the Idaho retirement benefits deduction on military retirement benefits" + documentation = """ + Idaho Code 63-3022A(1)(b) lets a retired member of the military, or the + member's unremarried widow or widower, count military retirement benefits + toward the retirement benefits deduction if the recipient is classified + as disabled, has reached age 62 by the end of the tax year, or was + employed during the tax year with enough income from that employment to + have to file a federal return. Before 2025 these benefits had the age test + of the other retirement benefits in 63-3022A(1): age 65, or age 62 and + classified as disabled. The age parameters carry both rules. + """ + definition_period = YEAR + defined_for = StateCode.ID + reference = ( + "https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/", + "https://legislature.idaho.gov/wp-content/uploads/sessioninfo/2025/legislation/H0040.pdf#page=5", + "https://legislature.idaho.gov/wp-content/uploads/sessioninfo/2025/legislation/H0479.pdf#page=2", + "https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=45", + ) + + def formula(person, period, parameters): + p = parameters( + period + ).gov.states.id.tax.income.deductions.retirement_benefits.military + age_threshold = where( + person("is_disabled", period), + p.age_eligibility.disabled, + p.age_eligibility.main, + ) + age_eligible = person("age", period) >= age_threshold + employment_eligible = person( + "id_retirement_benefits_deduction_military_employment_eligible", period + ) + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + return head_or_spouse & (age_eligible | employment_eligible) diff --git a/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_employment_eligible.py b/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_employment_eligible.py new file mode 100644 index 00000000000..a99dd7756ae --- /dev/null +++ b/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_military_employment_eligible.py @@ -0,0 +1,79 @@ +from policyengine_us.model_api import * + + +class id_retirement_benefits_deduction_military_employment_eligible(Variable): + value_type = bool + entity = Person + label = "Employment income requires a federal return, for the Idaho military retirement benefits deduction" + documentation = """ + Idaho Code 63-3022A(1)(b)(iii) admits the military retirement benefits of + a recipient who "was employed during the tax year and received sufficient + income from such employment to be required to file a federal return under + section 6012(a)(1) of the Internal Revenue Code". This is true when the + person's income from employment, by itself, meets the gross income test + of the federal filing requirement for the person's return, as + tax_unit_is_required_to_file applies it to a return nobody can claim: + section 6012(f), more than the standard deduction, for 2018 to 2025, and + section 6012(a)(1)(A) and (B), at least the basic standard deduction plus + the additions for age, in other years. On a joint return the threshold is + the joint one. As in the federal gross income, a loss from one source + doesn't reduce income from another. + """ + definition_period = YEAR + defined_for = StateCode.ID + reference = ( + "https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/", + "https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=45", + "https://www.law.cornell.edu/uscode/text/26/6012", + ) + + def formula(person, period, parameters): + p = parameters( + period + ).gov.states.id.tax.income.deductions.retirement_benefits.military.employment + employment_income = 0 + for source in p.income_sources: + employment_income = employment_income + max_( + 0, add(person, period, [source]) + ) + employed = employment_income > 0 + + # The thresholds of tax_unit_is_required_to_file, for a return + # nobody can claim. + p_irs = parameters(period).gov.irs + exemption_amount = ( + 0 if p_irs.income.exemption.suspended else p_irs.income.exemption.amount + ) + tax_unit = person.tax_unit + filing_status = tax_unit("filing_status", period) + separate = filing_status == filing_status.possible_values.SEPARATE + if p_irs.income.filing_requirement.standard_deduction_rule.applies: + # Section 6012(f): gross income over the standard deduction, and + # any gross income on a separate return. + threshold = where( + separate, + exemption_amount, + tax_unit("standard_deduction", period) + exemption_amount, + ) + requires_return = employment_income > threshold + else: + # Section 6012(a)(1)(A) and (B): gross income of at least the + # exemption amount for each spouse plus the basic standard + # deduction, raised by one additional standard deduction for + # each spouse aged 65 or older. A separate return, or one whose + # standard deduction is zero under section 63(c)(6), has no + # exception. + aged_count = tax_unit("aged_head", period).astype(int) + tax_unit( + "aged_spouse", period + ).astype(int) + basic_threshold = ( + exemption_amount * tax_unit("head_spouse_count", period) + + tax_unit("basic_standard_deduction", period) + + tax_unit("bonus_guaranteed_deduction", period) + + aged_count + * p_irs.deductions.standard.aged_or_blind.amount[filing_status] + ) + no_exception = separate | tax_unit("separate_filer_itemizes", period) + threshold = where(no_exception, exemption_amount, basic_threshold) + requires_return = employment_income >= threshold + return p.in_effect & employed & requires_return diff --git a/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_relevant_income.py b/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_relevant_income.py index 6ddfb492735..0b4e802b046 100644 --- a/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_relevant_income.py +++ b/policyengine_us/variables/gov/states/id/tax/income/deductions/retirement_benefits/id_retirement_benefits_deduction_relevant_income.py @@ -5,9 +5,31 @@ class id_retirement_benefits_deduction_relevant_income(Variable): value_type = float entity = Person label = "Idaho retirement benefits deduction income sources" + documentation = """ + The person's retirement benefits that count toward the Idaho retirement + benefits deduction (Form 39R line 8e): the benefits in Idaho Code + 63-3022A(1)(a) if the person meets that paragraph's age test, and + military retirement benefits if the person meets the test of + 63-3022A(1)(b). + """ unit = USD - reference = "https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/" + reference = ( + "https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/", + "https://tax.idaho.gov/wp-content/uploads/forms/EIN00046/EIN00046_03-02-2026.pdf#page=45", + ) definition_period = YEAR - defined_for = "id_retirement_benefits_deduction_eligible_person" + defined_for = StateCode.ID - adds = "gov.states.id.tax.income.deductions.retirement_benefits.income_sources" + def formula(person, period, parameters): + p = parameters(period).gov.states.id.tax.income.deductions.retirement_benefits + general_benefits = add(person, period, p.income_sources) + general_eligible = person( + "id_retirement_benefits_deduction_eligible_person", period + ) + military_benefits = person("id_eligible_military_retirement_income", period) + military_eligible = person( + "id_retirement_benefits_deduction_military_eligible_person", period + ) + return ( + general_benefits * general_eligible + military_benefits * military_eligible + ) diff --git a/policyengine_us/variables/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_deduction.py b/policyengine_us/variables/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_deduction.py deleted file mode 100644 index 1551022b6f8..00000000000 --- a/policyengine_us/variables/gov/states/id/tax/income/subtractions/military_retirement/id_military_retirement_deduction.py +++ /dev/null @@ -1,27 +0,0 @@ -from policyengine_us.model_api import * - - -class id_military_retirement_deduction(Variable): - value_type = float - entity = TaxUnit - label = "Idaho military retirement deduction" - unit = USD - reference = ( - "https://legislature.idaho.gov/sessioninfo/2025/legislation/H0040/", - "https://legislature.idaho.gov/statutesrules/idstat/title63/t63ch30/sect63-3022a/", - ) - definition_period = YEAR - defined_for = StateCode.ID - - def formula(tax_unit, period, parameters): - p = parameters(period).gov.states.id.tax.income.subtractions.military_retirement - person = tax_unit.members - head_or_spouse = person("is_tax_unit_head_or_spouse", period) - military_retirement_income = person( - "id_eligible_military_retirement_income", period - ) - eligible_military_pay = military_retirement_income * head_or_spouse - - # Multiply by applies parameter (False when military retirement - # is already handled in general retirement benefits deduction) - return tax_unit.sum(eligible_military_pay) * p.applies