diff --git a/changelog.d/fix-de-military-pension-cap-2027.fixed.md b/changelog.d/fix-de-military-pension-cap-2027.fixed.md new file mode 100644 index 00000000000..ee481f85fb5 --- /dev/null +++ b/changelog.d/fix-de-military-pension-cap-2027.fixed.md @@ -0,0 +1 @@ +Raise Delaware's military pension exclusion cap to $15,000 for 2027, $20,000 for 2028 and $25,000 from 2029, let filers age 60 or older take the greater of the $12,500 pension exclusion and the military pension exclusion from 2027, and apply the domicile requirement for filers 60 or older from 2027, under 30 Del. C. § 1106(b)(3) as 85 Del. Laws c. 426 amended it. diff --git a/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/cap/military.yaml b/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/cap/military.yaml index d10e75fcaa0..a0001a21a34 100644 --- a/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/cap/military.yaml +++ b/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/cap/military.yaml @@ -1,6 +1,11 @@ description: Delaware caps the military pension income exclusion at this amount. values: + # 30 Del. C. 1106(b)(3)b, for 2022 through 2026. 2022-01-01: 12_500 + # 1106(b)(3)c, d and e. + 2027-01-01: 15_000 + 2028-01-01: 20_000 + 2029-01-01: 25_000 metadata: unit: currency-USD period: year @@ -17,5 +22,7 @@ metadata: href: https://revenuefiles.delaware.gov/2022/PIT-RES_TY22_2022-02_Instructions.pdf#page=6 - title: The Delaware Code - Title 30 - Chapter 11 - Subchapter II - § 1106 - (b) - (3) href: https://delcode.delaware.gov/title30/c011/sc02/index.html + - title: 30 Del. C. § 1106(b)(3)c-e, military pension caps for 2027, 2028 and 2029 on + href: https://delcode.delaware.gov/title30/c011/sc02/index.html - title: Delaware Individual Income Tax Return Line 6 href: https://revenuefiles.delaware.gov/2023/PIT-RES_TY23_2023-01_Instructions.pdf#page=6 diff --git a/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/domicile_requirement_applies.yaml b/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/domicile_requirement_applies.yaml new file mode 100644 index 00000000000..bcf3e17ea77 --- /dev/null +++ b/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/domicile_requirement_applies.yaml @@ -0,0 +1,19 @@ +description: Delaware limits the pension exclusion for filers at or above the age threshold to those who meet its domicile requirement, if this is true. +values: + 2007-01-01: false + # 30 Del. C. 1106(b)(3)f.4, added by 85 Del. Laws c. 426 (Senate Bill 219 + # with Senate Amendment 1). The act took effect when the Governor signed it + # on August 17, 2026 and names no first tax year for this rule; its fiscal + # note applies the act from tax year 2027. + 2027-01-01: true +metadata: + unit: bool + period: year + label: Delaware pension exclusion domicile requirement applies + reference: + - title: 30 Del. C. § 1106(b)(3)f.4 + href: https://delcode.delaware.gov/title30/c011/sc02/index.html + - title: Senate Amendment 1 to Senate Bill 219, 153rd General Assembly + href: https://legis.delaware.gov/json/BillDetail/GenerateHtmlDocument?legislationId=143006&legislationTypeId=5&docTypeId=2&legislationName=SA1 + - title: Senate Bill 219 fiscal note, assumption 1 + href: https://legis.delaware.gov/json/BillDetail/GetPdfDocument?fileAttachmentId=653814#page=1 diff --git a/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/military_retirement_exclusion_available_at_age_threshold.yaml b/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/military_retirement_exclusion_available_at_age_threshold.yaml new file mode 100644 index 00000000000..728c55d75c9 --- /dev/null +++ b/policyengine_us/parameters/gov/states/de/tax/income/subtractions/exclusions/pension/military_retirement_exclusion_available_at_age_threshold.yaml @@ -0,0 +1,15 @@ +description: Delaware lets filers at or above the pension exclusion age threshold take the greater of the pension exclusion and the military pension exclusion, if this is true. +values: + # 30 Del. C. 1106(b)(3)b.2: for 2022 through 2026, filers age 60 or older + # exclude up to $12,500 of pensions and eligible retirement income. + 2022-01-01: false + # 1106(b)(3)c.2, d.2 and e.2: from 2027 they exclude the greater of that + # amount and their United States military pension, up to the military cap. + 2027-01-01: true +metadata: + unit: bool + period: year + label: Delaware military pension exclusion available at or above the age threshold + reference: + - title: 30 Del. C. § 1106(b)(3)b-e + href: https://delcode.delaware.gov/title30/c011/sc02/index.html diff --git a/policyengine_us/tests/core/test_de_military_pension_exclusion_properties.py b/policyengine_us/tests/core/test_de_military_pension_exclusion_properties.py new file mode 100644 index 00000000000..f9c66ceb46a --- /dev/null +++ b/policyengine_us/tests/core/test_de_military_pension_exclusion_properties.py @@ -0,0 +1,139 @@ +"""Properties of Delaware's pension exclusion for military pensions, 2022-2031. + +30 Del. C. 1106(b)(3) subtracts, for 2022 through 2026, the greater of +pensions up to $2,000 and a United States military pension up to $12,500 for +a person under 60, and pensions and eligible retirement income up to $12,500 +for a person 60 or older. For 2027, 2028 and from 2029 ((b)(3)c, d and e) the +military cap is $15,000, $20,000 and $25,000, and a person 60 or older takes +the greater of the $12,500 exclusion and the military pension up to that cap. +From 2027 ((b)(3)f.4), a person 60 or older who fails the domicile requirement +gets no subtraction. + +The reference below restates those rules from the statute, not from the +model's parameters. Hypothesis draws batches of single filers (any age; +military retired pay, survivor benefits, other pensions, IRA distributions, +interest, capital gains or losses, and the domicile requirement), runs each +batch as one simulation with the same inputs in every year, and checks for +every filer and year: + +1. The exclusion equals the reference amount. +2. It is never negative and never exceeds the military pension plus the + pension basket floored at zero. +3. From 2027, a filer 60 or older who meets the domicile requirement never + gets less than the 2026 rule gives. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEARS = tuple(range(2022, 2032)) +MILITARY_CAP = {2027: 15_000, 2028: 20_000} + + +def military_cap(year): + if year <= 2026: + return 12_500 + return MILITARY_CAP.get(year, 25_000) + + +def basket(filer): + """Pensions and eligible retirement income, with the $3,000 loss limit.""" + gains = max(filer["long_term_capital_gains"], -3_000) + return ( + filer["military_retirement_pay"] + + filer["military_retirement_pay_survivors"] + + filer["taxable_pension_income"] + + filer["taxable_ira_distributions"] + + filer["taxable_interest_income"] + + gains + ) + + +def reference(year, filer): + military = min(military_cap(year), filer["military_retirement_pay"]) + if ( + year >= 2027 + and filer["age"] >= 60 + and not filer["de_pension_exclusion_domicile_requirement_met"] + ): + return 0 + if filer["age"] < 60: + pensions = ( + filer["taxable_pension_income"] + + filer["military_retirement_pay"] + + filer["military_retirement_pay_survivors"] + ) + return max(min(2_000, max(pensions, 0)), military) + general = min(12_500, max(basket(filer), 0)) + if year <= 2026: + return general + return max(general, military) + + +filers = st.fixed_dictionaries( + { + "age": st.integers(30, 90), + "military_retirement_pay": st.sampled_from( + [0, 1_000, 12_500, 15_000, 18_000, 20_000, 25_000, 40_000] + ), + "military_retirement_pay_survivors": st.sampled_from([0, 3_000]), + "taxable_pension_income": st.sampled_from([0, 1_500, 20_000]), + "taxable_ira_distributions": st.sampled_from([0, 5_000, 20_000]), + "taxable_interest_income": st.sampled_from([0, 2_000]), + "long_term_capital_gains": st.sampled_from([-10_000, -2_000, 0, 4_000]), + "de_pension_exclusion_domicile_requirement_met": st.booleans(), + } +) + + +def simulate(batch): + people, tax_units, households = {}, {}, {} + groups = {"marital_units": {}, "spm_units": {}, "families": {}} + for i, filer in enumerate(batch): + name = f"filer_{i}" + people[name] = {k: {y: v for y in YEARS} for k, v in filer.items()} + tax_units[f"tax_unit_{i}"] = { + "members": [name], + "filing_status": {y: "SINGLE" for y in YEARS}, + } + households[f"household_{i}"] = { + "members": [name], + "state_code": {y: "DE" for y in YEARS}, + } + for group in groups: + groups[group][f"{group}_{i}"] = {"members": [name]} + return Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "households": households, + **groups, + } + ) + + +@settings( + max_examples=6, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(batch=st.lists(filers, min_size=10, max_size=30)) +def test_military_pension_exclusion(batch): + simulation = simulate(batch) + for year in YEARS: + exclusion = np.asarray(simulation.calculate("de_pension_exclusion", year)) + for filer, amount in zip(batch, exclusion): + assert amount == reference(year, filer), (year, filer, amount) + assert ( + 0 <= amount <= filer["military_retirement_pay"] + max(basket(filer), 0) + ) + if ( + year >= 2027 + and filer["age"] >= 60 + and filer["de_pension_exclusion_domicile_requirement_met"] + ): + assert amount >= reference(2026, filer) diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion.yaml index 935d7bd10f1..a18acdbd733 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion.yaml @@ -153,3 +153,226 @@ state_code: DE output: de_pension_exclusion: 12_500 + +# 30 Del. C. 1106(b)(3)c-e: the military pension cap rises to $15,000 for +# 2027, $20,000 for 2028 and $25,000 from 2029, and from 2027 filers age 60 or +# older take the greater of the $12,500 pension exclusion and the military +# pension exclusion. + +- name: DE under-60 military pension cap stays $12,500 in 2026. + period: 2026 + input: + state_code: DE + age: 45 + military_retirement_pay: 30_000 + output: + de_pension_exclusion: 12_500 + +- name: DE under-60 military pension cap is $15,000 in 2027. + period: 2027 + input: + state_code: DE + age: 45 + military_retirement_pay: 30_000 + output: + de_pension_exclusion: 15_000 + +- name: DE under-60 military pension cap is $20,000 in 2028. + period: 2028 + input: + state_code: DE + age: 45 + military_retirement_pay: 30_000 + output: + de_pension_exclusion: 20_000 + +- name: DE under-60 military pension cap is $25,000 in 2029. + period: 2029 + input: + state_code: DE + age: 45 + military_retirement_pay: 30_000 + output: + de_pension_exclusion: 25_000 + +- name: DE under-60 military pension cap stays $25,000 after 2029. + period: 2031 + input: + state_code: DE + age: 45 + military_retirement_pay: 30_000 + output: + de_pension_exclusion: 25_000 + +- name: DE military pension under the cap is excluded in full in 2029. + period: 2029 + input: + state_code: DE + age: 45 + military_retirement_pay: 18_000 + output: + de_pension_exclusion: 18_000 + +- name: DE 60+ filer excludes $12,500 of military pension in 2026. + period: 2026 + input: + state_code: DE + age: 65 + military_retirement_pay: 30_000 + output: + de_pension_exclusion: 12_500 + +- name: DE 60+ filer takes the $15,000 military pension exclusion in 2027. + period: 2027 + input: + state_code: DE + age: 65 + military_retirement_pay: 30_000 + output: + de_pension_exclusion: 15_000 + +- name: DE 60+ filer takes the $25,000 military pension exclusion in 2029. + period: 2029 + input: + state_code: DE + age: 65 + military_retirement_pay: 30_000 + output: + de_pension_exclusion: 25_000 + +- name: DE 60+ filer keeps the $12,500 pension exclusion when it is larger in 2029. + period: 2029 + input: + state_code: DE + age: 65 + military_retirement_pay: 10_000 + taxable_ira_distributions: 20_000 + output: + # The greater of min(12,500, 30,000) and min(25,000, 10,000). + de_pension_exclusion: 12_500 + +- name: DE 60+ filer without military pension keeps the $12,500 cap in 2029. + period: 2029 + input: + state_code: DE + age: 65 + taxable_pension_income: 30_000 + output: + de_pension_exclusion: 12_500 + +- name: DE 60+ filer with a capital loss takes military pension alone from 2027. + period: 2027 + input: + state_code: DE + age: 75 + military_retirement_pay: 10_000 + long_term_capital_gains: -3_000 + output: + # The basket nets to 7,000; the military pension exclusion gives 10,000. + de_pension_exclusion: 10_000 + +- name: DE 60+ filer with a capital loss keeps the netted basket in 2026. + period: 2026 + input: + state_code: DE + age: 75 + military_retirement_pay: 10_000 + long_term_capital_gains: -3_000 + output: + de_pension_exclusion: 7_000 + +- name: DE under-60 survivor benefits stay in the $2,000 pension exclusion in 2029. + period: 2029 + input: + state_code: DE + age: 50 + military_retirement_pay_survivors: 30_000 + output: + de_pension_exclusion: 2_000 + +- name: DE joint return gives each spouse the 2029 cap on their own military pension. + period: 2029 + input: + people: + head: + age: 65 + military_retirement_pay: 30_000 + spouse: + age: 58 + military_retirement_pay: 22_000 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + households: + household: + members: [head, spouse] + state_code: DE + output: + de_pension_exclusion: [25_000, 22_000] + +# 30 Del. C. 1106(b)(3)f.4 (85 Del. Laws c. 426, Senate Amendment 1): from +# 2027, a person 60 or older claims the subtraction only after 3 years of +# Delaware domicile, or 5 years for a person domiciled in Delaware from 2027. + +- name: DE 60+ filer who fails the domicile requirement gets no exclusion in 2027. + period: 2027 + input: + state_code: DE + age: 65 + military_retirement_pay: 30_000 + de_pension_exclusion_domicile_requirement_met: false + output: + de_pension_exclusion: 0 + +- name: DE domicile requirement also removes the 60+ pension exclusion in 2029. + period: 2029 + input: + state_code: DE + age: 70 + taxable_ira_distributions: 20_000 + de_pension_exclusion_domicile_requirement_met: false + output: + de_pension_exclusion: 0 + +- name: DE domicile requirement does not apply in 2026. + period: 2026 + input: + state_code: DE + age: 65 + military_retirement_pay: 30_000 + de_pension_exclusion_domicile_requirement_met: false + output: + de_pension_exclusion: 12_500 + +- name: DE domicile requirement does not apply under 60. + period: 2027 + input: + state_code: DE + age: 45 + military_retirement_pay: 30_000 + de_pension_exclusion_domicile_requirement_met: false + output: + de_pension_exclusion: 15_000 + +- name: DE joint return applies the domicile requirement to each spouse. + period: 2029 + input: + people: + head: + age: 65 + military_retirement_pay: 30_000 + de_pension_exclusion_domicile_requirement_met: false + spouse: + age: 62 + military_retirement_pay: 22_000 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + households: + household: + members: [head, spouse] + state_code: DE + output: + de_pension_exclusion: [0, 22_000] diff --git a/policyengine_us/variables/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion.py b/policyengine_us/variables/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion.py index a1d22553862..4efc909831c 100644 --- a/policyengine_us/variables/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion.py +++ b/policyengine_us/variables/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion.py @@ -10,6 +10,7 @@ class de_pension_exclusion(Variable): reference = ( "https://revenuefiles.delaware.gov/2022/PIT-RES_TY22_2022-02_Instructions.pdf#page=6", "https://delcode.delaware.gov/title30/c011/sc02/index.html", + "https://revenuefiles.delaware.gov/2025/PITForms_Instructions/Instructions/PIT-RES_Instructions_2025-01.pdf#page=6", ) defined_for = StateCode.DE @@ -59,15 +60,33 @@ def formula(person, period, parameters): capped_military_retirement_pay = min_( p.cap.military, military_retirement_pay ) - younger_amount = max_( + # Filers under the age threshold take the greater of the pension + # exclusion and the military pension exclusion from 2022 + # (30 Del. C. 1106(b)(3)b.1), and filers at or above it from 2027 + # (1106(b)(3)c.2, d.2 and e.2). + greater_amount = max_( capped_military_retirement_pay, capped_eligible_pension_income ) - # Filers under the age threshold, post 2022, can subtract their military retirement - # income which is capped at a larger amount - return where( + if p.military_retirement_exclusion_available_at_age_threshold: + older_amount = greater_amount + else: + older_amount = capped_eligible_pension_income + exclusion = where( eligible_for_pension_exclusion_income, - capped_eligible_pension_income, - younger_amount, + older_amount, + greater_amount, ) else: - return capped_eligible_pension_income + exclusion = capped_eligible_pension_income + if p.domicile_requirement_applies: + # A person at or above the age threshold claims the exclusion only + # after 3 years of Delaware domicile, or 5 years for a person + # domiciled in Delaware from 2027 (30 Del. C. 1106(b)(3)f.4). + meets_domicile_requirement = person( + "de_pension_exclusion_domicile_requirement_met", period + ) + older_exclusion = where(meets_domicile_requirement, exclusion, 0) + return where( + eligible_for_pension_exclusion_income, older_exclusion, exclusion + ) + return exclusion diff --git a/policyengine_us/variables/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion_domicile_requirement_met.py b/policyengine_us/variables/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion_domicile_requirement_met.py new file mode 100644 index 00000000000..067039dd76e --- /dev/null +++ b/policyengine_us/variables/gov/states/de/tax/income/subtractions/pension_exclusion/de_pension_exclusion_domicile_requirement_met.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class de_pension_exclusion_domicile_requirement_met(Variable): + value_type = bool + entity = Person + label = "Meets Delaware's domicile requirement for the pension exclusion at age 60 or older" + documentation = ( + "Whether this person has lived in Delaware long enough to claim the " + "pension exclusion at age 60 or older: at least 3 years of Delaware " + "domicile for a person domiciled in Delaware before January 1, 2027, " + "and at least 5 years for one domiciled there on or after that date. " + "A person counts as domiciled in Delaware while a resident individual " + "under 30 Del. C. 1103. PolicyEngine assumes the requirement is met " + "unless this input says otherwise." + ) + definition_period = YEAR + default_value = True + reference = ( + "https://delcode.delaware.gov/title30/c011/sc02/index.html", + "https://legis.delaware.gov/json/BillDetail/GenerateHtmlDocument?legislationId=143006&legislationTypeId=5&docTypeId=2&legislationName=SA1", + )