diff --git a/changelog.d/mi-household-resources-premiums-gambling.fixed.md b/changelog.d/mi-household-resources-premiums-gambling.fixed.md new file mode 100644 index 00000000000..d40626aef46 --- /dev/null +++ b/changelog.d/mi-household-resources-premiums-gambling.fixed.md @@ -0,0 +1 @@ +Subtract Michigan household resources premiums once (MI-1040CR line 31 leaves out the self-employed health insurance deduction on line 30), and exclude the first $300 of gambling winnings and of gifts (MCL 206.510(1)(a) and (b), lines 20 and 24), counting gifts from relatives and friends on line 24. diff --git a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources.yaml b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources.yaml index 449b7ae9cf2..6f7d5353bb5 100644 --- a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources.yaml +++ b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources.yaml @@ -28,9 +28,13 @@ values: # MI-1040CR line 19, net capital gains and losses from the filer's U.S. # Schedule D, a loss limited to $3,000 ($1,500 married filing separately). - filer_loss_limited_net_capital_gains - # MI-1040CR line 20, alimony and other taxable income. + # MI-1040CR line 20, alimony and other taxable income: "awards, prizes, + # lottery, bingo, and other gambling winnings over $300" (the first $300 + # is household_resources_gambling_exclusion) and other taxable income, + # such as miscellaneous_income (26 CFR 1.61-14), in full. - alimony_income - gambling_winnings + - miscellaneous_income # MI-1040CR line 21, Social Security, SSI and railroad retirement # benefits, including amounts received for dependents. - social_security @@ -40,8 +44,14 @@ values: - child_support_received # MI-1040CR line 23, unemployment compensation. - total_unemployment_compensation - # MI-1040CR line 24, gifts received or expenses paid on your behalf. - - miscellaneous_income + # MI-1040CR line 24, "the value over $300 in gifts of cash or merchandise + # received, or expenses paid on your behalf ... by parents, relatives, or + # friends" (the first $300 is household_resources_gift_exclusion). Gifts + # are excluded from federal gross income (IRC 102) but are total + # household resources (MCL 206.510(1)). financial_assistance is cash + # only; expenses others pay on the claimant's behalf (gifts in kind) have + # no input. + - financial_assistance # MI-1040CR line 26, veterans' benefits and workers' compensation. - veterans_benefits - workers_compensation @@ -68,9 +78,9 @@ metadata: href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2023/BOOK_MI-1040.pdf#page=31 - title: Michigan 2024 Individual Income Tax Instructions href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2024/MI-1040-Instructions.pdf#page=31 - - title: Michigan 2025 Individual Income Tax Instructions - href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2025/MI-1040-Book.pdf#page=31 - - title: Michigan 2025 Individual Income Tax Instructions, Total Household Resources + - title: "Michigan 2025 Individual Income Tax Instructions, Total Household Resources #page 26-27,31-32" href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2025/MI-1040-Book.pdf#page=26 - title: Michigan Compiled Laws Chapter 9 Section 206.508 - Definitions. (4) href: https://law.justia.com/codes/michigan/2022/chapter-206/statute-act-281-of-1967/division-281-1967-1/division-281-1967-1-9/section-206-508/ + - title: MCL 206.510(1), "Income" defined + href: https://www.legislature.mi.gov/Laws/MCL?objectName=mcl-206-510 diff --git a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_gambling_exclusion.yaml b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_gambling_exclusion.yaml new file mode 100644 index 00000000000..4c588f1622b --- /dev/null +++ b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_gambling_exclusion.yaml @@ -0,0 +1,23 @@ +description: Michigan excludes this amount of awards, prizes, lottery, bingo and other gambling winnings from household resources. +values: + # MCL 206.510(1)(b): a fixed amount in the statute, not indexed. The + # MI-1040CR counts winnings "over $300" on line 20, one amount for the + # claimant and spouse. The model applies it to gambling_winnings; it has no + # separate input for awards and prizes, and one entered as + # miscellaneous_income counts in full on line 20. + 2021-01-01: 300 +metadata: + unit: currency-USD + period: year + label: Michigan household resources gambling winnings exclusion + reference: + - title: MCL 206.510(1)(b), "The first $300.00 received from awards, prizes, lottery, bingo, or other gambling winnings" + href: https://www.legislature.mi.gov/Laws/MCL?objectName=mcl-206-510 + - title: 2022 MI-1040 Instructions, MI-1040CR line 20 + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2022/BOOK_MI-1040.pdf#page=32 + - title: 2023 MI-1040 Instructions, MI-1040CR line 20 + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2023/BOOK_MI-1040.pdf#page=32 + - title: 2024 MI-1040 Instructions, MI-1040CR line 20 + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2024/MI-1040-Instructions.pdf#page=32 + - title: "2025 MI-1040 Instructions, MI-1040CR line 20 and total household resources #page 27,32" + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2025/MI-1040-Book.pdf#page=27 diff --git a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_gift_exclusion.yaml b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_gift_exclusion.yaml new file mode 100644 index 00000000000..00bd2d3463c --- /dev/null +++ b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources_gift_exclusion.yaml @@ -0,0 +1,21 @@ +description: Michigan excludes this amount of gifts in cash or kind from nongovernmental sources from household resources. +values: + # MCL 206.510(1)(a): a fixed amount in the statute, not indexed. The + # MI-1040CR counts "the value over $300 in gifts" on line 24, one amount + # for the claimant and spouse. + 2021-01-01: 300 +metadata: + unit: currency-USD + period: year + label: Michigan household resources gift exclusion + reference: + - title: MCL 206.510(1)(a), "The first $300.00 of gifts in cash or kind from nongovernmental sources" + href: https://www.legislature.mi.gov/Laws/MCL?objectName=mcl-206-510 + - title: 2022 MI-1040 Instructions, MI-1040CR line 24 + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2022/BOOK_MI-1040.pdf#page=32 + - title: 2023 MI-1040 Instructions, MI-1040CR line 24 + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2023/BOOK_MI-1040.pdf#page=32 + - title: 2024 MI-1040 Instructions, MI-1040CR line 24 + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2024/MI-1040-Instructions.pdf#page=32 + - title: "2025 MI-1040 Instructions, MI-1040CR line 24 and total household resources #page 27,32" + href: https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2025/MI-1040-Book.pdf#page=27 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml index 9e9345ba1a7..30536916550 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml @@ -1089,6 +1089,319 @@ # Line 33: 10,000 - 1,000 - 1,000 = 8,000. mi_household_resources: 8_000 +# Line 31 (2025 MI-1040 book, pages 32 and 33): "insurance premiums you paid +# for yourself and your family ... Do not include any insurance premiums +# deducted on lines 21 or 30". Line 30 includes the self-employed health +# insurance deduction (Schedule 1 line 17), so a premium deducted there is +# not subtracted again on line 31. + +- name: A self-employed claimant's premiums are subtracted once, on line 30 + period: 2025 + input: + self_employment_income: 50_000 + is_self_employed: true + health_insurance_premiums: 5_000 + state_code: MI + output: + # Line 16: Schedule C 50,000. + # Line 30: deduction for self-employment tax, half of 50,000 x 0.9235 x + # 15.3% = 3,532.39, plus the self-employed health insurance deduction, + # 5,000 (the premiums are below both PolicyEngine's earnings limit and + # Form 7206's earnings limit after deductible self-employment tax). + # 8,532.39 in all. + # Line 31: premiums of 5,000, less the 5,000 deducted on line 30 = 0. + # Line 33: 50,000 - 8,532.39 - 0 = 41,467.61. Subtracting the premiums + # on both lines would give 36,467.61. + mi_household_resources: 41_467.61 + absolute_error_margin: 0.01 + +- name: Premiums above the self-employed health insurance deduction stay on line 31 + period: 2025 + input: + employment_income_before_lsr: 40_000 + self_employment_income: 3_000 + is_self_employed: true + health_insurance_premiums: 5_000 + state_code: MI + output: + # Line 14: 40,000. Line 16: 3,000. + # PolicyEngine's pre-existing federal simplification limits the health + # insurance deduction to net self-employment income before deductible + # self-employment tax: 3,000 here. + # Line 30 in PolicyEngine: self-employment tax deduction, half of 3,000 x + # 0.9235 x 15.3% = 211.94, plus health insurance 3,000 = 3,211.94. + # Line 31: premiums of 5,000, less the 3,000 deducted on line 30 = 2,000. + # Line 33: 43,000 - 3,211.94 - 2,000 = 37,788.06. The premiums come off + # once in all: 3,000 on line 30 and 2,000 on line 31. + # 2025 Form 7206 lines 7-8 instead limit the health insurance deduction + # to 3,000 - 211.94 = 2,788.06. Then line 30 is 3,000 and line 31 is + # 5,000 - 2,788.06 = 2,211.94: 43,000 - 3,000 - 2,211.94 = 37,788.06. + # The Michigan total is unchanged because every premium comes off once. + mi_household_resources: 37_788.06 + absolute_error_margin: 0.01 + +- name: A joint return subtracts each spouse's premiums once + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + self_employment_income: 50_000 + is_self_employed: true + health_insurance_premiums: 5_000 + person2: + age: 44 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + employment_income_before_lsr: 30_000 + health_insurance_premiums: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MI + output: + # Line 14: 30,000. Line 16: 50,000. + # Line 30: 3,532.39 for self-employment tax plus the head's 5,000 + # self-employed health insurance deduction = 8,532.39. The spouse is not + # self-employed, so the spouse's premiums are not on line 30. + # Line 31: premiums of 5,000 + 2,000 = 7,000, less the 5,000 deducted on + # line 30 = 2,000. + # Line 33: 80,000 - 8,532.39 - 2,000 = 69,467.61. + mi_household_resources: 69_467.61 + absolute_error_margin: 0.01 + +- name: Medicare premiums withheld from Social Security are subtracted once + period: 2025 + input: + age: 70 + social_security_retirement: 20_000 + health_insurance_premiums: 2_220 + state_code: MI + output: + # The form puts Medicare premiums withheld from benefits on line 21: + # "Medicare premiums reported on your Social Security or Railroad + # Retirement statement should be deducted" (page 32), so line 21 is + # 20,000 - 2,220 = 17,780 and line 31 leaves them out. MCL 206.510(1)(h) + # excludes "Amounts deducted from monthly social security or railroad + # retirement benefits for medicare premiums" from income. + # The model enters gross benefits on line 21 and the premiums on line + # 31: 20,000 - 2,220. Either way the premiums come off once. + # Line 33: 17,780. + mi_household_resources: 17_780 + +# MCL 206.510(1): income does not include "(a) The first $300.00 of gifts in +# cash or kind from nongovernmental sources" or "(b) The first $300.00 +# received from awards, prizes, lottery, bingo, or other gambling winnings". +# The MI-1040CR enters "awards, prizes, lottery, bingo, and other gambling +# winnings over $300" on line 20 and "the value over $300 in gifts of cash or +# merchandise received, or expenses paid on your behalf ... by parents, +# relatives, or friends" on line 24 (2025 MI-1040 book, page 32), each once +# for the claimant and spouse. + +- name: Gambling winnings below $300 are not household resources + period: 2025 + input: + employment_income_before_lsr: 20_000 + gambling_winnings: 200 + state_code: MI + output: + # Line 14: 20,000. Line 20: winnings over $300 = 0. + mi_household_resources: 20_000 + +- name: Gambling winnings of exactly $300 are not household resources + period: 2025 + input: + employment_income_before_lsr: 20_000 + gambling_winnings: 300 + state_code: MI + output: + # Line 20: 300 - 300 = 0. + mi_household_resources: 20_000 + +- name: Gambling winnings above $300 count over $300 + period: 2025 + input: + employment_income_before_lsr: 20_000 + gambling_winnings: 1_000 + state_code: MI + output: + # Line 20: 1,000 - 300 = 700. + # Line 33: 20,000 + 700 = 20,700. + mi_household_resources: 20_700 + +- name: The $300 gambling exclusion applies once to a married couple + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + employment_income_before_lsr: 20_000 + gambling_winnings: 200 + person2: + age: 44 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + gambling_winnings: 200 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MI + output: + # Line 20 is one amount for the claimant and spouse: winnings of + # 200 + 200 = 400, over $300 = 100. + # Line 33: 20,000 + 100 = 20,100. + mi_household_resources: 20_100 + +- name: Gifts below $300 are not household resources + period: 2025 + input: + employment_income_before_lsr: 20_000 + financial_assistance: 200 + state_code: MI + output: + # Line 24: cash gifts from relatives or friends over $300 = 0. + mi_household_resources: 20_000 + +- name: Gifts of exactly $300 are not household resources + period: 2025 + input: + employment_income_before_lsr: 20_000 + financial_assistance: 300 + state_code: MI + output: + # Line 24: 300 - 300 = 0. + mi_household_resources: 20_000 + +- name: Gifts above $300 count over $300 + period: 2025 + input: + employment_income_before_lsr: 20_000 + financial_assistance: 1_000 + state_code: MI + output: + # Line 24: 1,000 - 300 = 700. Gifts are excluded from federal gross + # income (IRC 102), and total household resources include income + # excluded from AGI (MCL 206.510(1)). + # Line 33: 20,000 + 700 = 20,700. + mi_household_resources: 20_700 + +- name: The $300 gift exclusion applies once to a married couple + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + employment_income_before_lsr: 20_000 + financial_assistance: 250 + person2: + age: 44 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + financial_assistance: 250 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MI + output: + # Line 24: gifts of 250 + 250 = 500, over $300 = 200. + # Line 33: 20,000 + 200 = 20,200. + mi_household_resources: 20_200 + +- name: Gambling winnings and gifts each have their own $300 exclusion + period: 2025 + input: + employment_income_before_lsr: 20_000 + gambling_winnings: 500 + financial_assistance: 500 + state_code: MI + output: + # Line 20: 500 - 300 = 200. Line 24: 500 - 300 = 200. + # Line 33: 20,000 + 200 + 200 = 20,400. + mi_household_resources: 20_400 + +- name: Miscellaneous taxable income counts in full on line 20 + period: 2025 + input: + employment_income_before_lsr: 20_000 + miscellaneous_income: 1_000 + state_code: MI + output: + # miscellaneous_income is other taxable income (26 CFR 1.61-14), in + # federal gross income, so it is not a gift. It is "other taxable + # income" on line 20, and the $300 exclusion covers only awards, prizes, + # lottery, bingo and other gambling winnings. + # Line 33: 20,000 + 1,000 = 21,000. + mi_household_resources: 21_000 + +- name: Supplied health insurance deduction preserves the spouse's separate premiums + period: 2025 + input: + people: + person1: + age: 45 + is_tax_unit_head: true + is_tax_unit_spouse: false + is_tax_unit_dependent: false + is_self_employed: false + employment_income_before_lsr: 40_000 + health_insurance_premiums: 5_000 + person2: + age: 44 + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_dependent: false + is_self_employed: false + employment_income_before_lsr: 30_000 + health_insurance_premiums: 2_000 + tax_units: + tax_unit: + members: [person1, person2] + self_employed_health_insurance_ald: 5_000 + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MI + output: + # The supplied deduction and all related premiums are represented, even + # without the self-employment flags used to compute the federal deduction. + # Line 14: 70,000. Line 30: 5,000. + # Line 31: 7,000 - 5,000 = 2,000. + # Line 33: 70,000 - 5,000 - 2,000 = 63,000. + mi_household_resources: 63_000 + - name: A supplied self-employed health insurance deduction comes off line 30 period: 2025 input: @@ -1098,3 +1411,27 @@ output: # Line 33: 10,000 - 1,000 = 9,000. mi_household_resources: 9_000 + +- name: Gambling winnings above $300 count over $300 in 2023 + period: 2023 + input: + employment_income_before_lsr: 20_000 + gambling_winnings: 400 + state_code: MI + output: + # 2023 MI-1040CR line 20 has the same $300 exclusion as 2025. + # Line 33: 20,000 + max(400 - 300, 0) = 20,100. + mi_household_resources: 20_100 + +- name: A self-employed claimant's premiums are subtracted once in 2023 + period: 2023 + input: + self_employment_income: 50_000 + is_self_employed: true + health_insurance_premiums: 5_000 + state_code: MI + output: + # 2023 MI-1040CR lines 30-31 exclude premiums already deducted on line 30. + # Line 33: 50,000 - (50,000 x 0.9235 x 15.3% / 2) - 5,000 = 41,467.61. + mi_household_resources: 41_467.61 + absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/test_mi_household_resources_exclusions.py b/policyengine_us/tests/test_mi_household_resources_exclusions.py new file mode 100644 index 00000000000..5d38793260d --- /dev/null +++ b/policyengine_us/tests/test_mi_household_resources_exclusions.py @@ -0,0 +1,427 @@ +"""Property tests: premiums and the $300 exclusions in Michigan household resources. + +MI-1040CR line 30 subtracts "total adjustments from your U.S. Form 1040, +Schedule 1", which include the self-employed health insurance deduction +(Schedule 1 line 17). Line 31 subtracts "insurance premiums you paid for +yourself and your family", but "Do not include any insurance premiums deducted +on lines 21 or 30" (2025 MI-1040 book, page 33). + +MCL 206.510(1) excludes from income "(a) The first $300.00 of gifts in cash or +kind from nongovernmental sources" and "(b) The first $300.00 received from +awards, prizes, lottery, bingo, or other gambling winnings". The MI-1040CR +enters gambling winnings "over $300" on line 20 and "the value over $300 in +gifts" on line 24 (page 32), each one amount for the claimant and spouse. +Gifts are financial_assistance, cash from friends or relatives outside the +household. + +So for every household: + +1. Each premium dollar is subtracted once. Zeroing every member's premiums + raises household resources by between 0 and the household's total + premiums, and by exactly that total when household resources were + positive. +2. Household resources are non-decreasing in the head's gambling winnings. + They do not change while the claimant's and spouse's winnings total at + most $300, and when they were positive they rise by the change in + winnings over $300. +3. The same holds for gifts. +4. A reform that drops the self-employed health insurance deduction from + either the federal list or Michigan's line-30 list moves those premiums + from line 30 to line 31 and leaves household resources unchanged. + +The households have a head, an optional spouse and an optional dependent, who +may be self-employed and pay premiums. SSI and the Family Independence +Program grant are inputs set to zero, so more gifts cannot lower a computed +benefit. Comparisons allow one cent or eight float32 spacings at the sum of +the household's absolute amounts, as in test_mi_household_resources_losses.py. +""" + +import numpy as np +import pytest + +from policyengine_core.periods import instant +from policyengine_core.reforms import Reform + +from policyengine_us import Simulation + +YEAR = 2025 +TOLERANCE = 0.01 +# MCL 206.510(1)(a) and (b): a fixed amount in the statute, not indexed. +EXCLUSION = 300 + +AMOUNTS = [ + "employment_income_before_lsr", + "self_employment_income", + "health_insurance_premiums", + "gambling_winnings", + "financial_assistance", +] +PREMIUMS = "health_insurance_premiums" + + +def person(**amounts): + p = {name: 0 for name in AMOUNTS} + p["is_self_employed"] = False + p.update(amounts) + return p + + +def build_situation(households): + """One Michigan tax unit per household: a head aged 45, an optional + spouse aged 44 and an optional dependent aged 16.""" + people, tax_units, marital_units, spm_units, households_out = {}, {}, {}, {}, {} + for i, h in enumerate(households): + members, couple = [], [] + roles = [("head", h["head"])] + if h.get("spouse") is not None: + roles.append(("spouse", h["spouse"])) + if h.get("dependent") is not None: + roles.append(("dependent", h["dependent"])) + for j, (role, p) in enumerate(roles): + name = f"person_{i}_{j}" + people[name] = { + "age": {YEAR: {"head": 45, "spouse": 44}.get(role, 16)}, + "is_tax_unit_head": {YEAR: role == "head"}, + "is_tax_unit_spouse": {YEAR: role == "spouse"}, + "is_tax_unit_dependent": {YEAR: role == "dependent"}, + "ssi": {YEAR: 0}, + **{name_: {YEAR: p[name_]} for name_ in [*AMOUNTS, "is_self_employed"]}, + } + members.append(name) + if role == "dependent": + marital_units[f"marital_unit_{i}_{j}"] = {"members": [name]} + else: + couple.append(name) + tax_units[f"tax_unit_{i}"] = {"members": members} + marital_units[f"marital_unit_{i}"] = {"members": couple} + spm_units[f"spm_unit_{i}"] = {"members": members, "tanf": {YEAR: 0}} + households_out[f"household_{i}"] = { + "members": members, + "state_code": {YEAR: "MI"}, + } + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "spm_units": spm_units, + "households": households_out, + } + + +def calculate(households): + simulation = Simulation(situation=build_situation(households)) + return np.asarray(simulation.calculate("mi_household_resources", YEAR)) + + +def members(h): + return [p for p in (h["head"], h.get("spouse"), h.get("dependent")) if p] + + +def filers(h): + return [p for p in (h["head"], h.get("spouse")) if p] + + +def total(people, name): + return sum(p[name] for p in people) + + +def copy(h): + return { + role: (dict(h[role]) if h.get(role) is not None else None) + for role in ("head", "spouse", "dependent") + } + + +def tolerance(h): + """One cent, or eight float32 spacings at the sum of the household's + absolute amounts.""" + scale = sum(abs(p[name]) for p in members(h) for name in AMOUNTS) + return max(TOLERANCE, 8 * float(np.spacing(np.float32(scale)))) + + +def over_exclusion(amount): + return max(0, amount - EXCLUSION) + + +def assert_premiums_subtracted_once(households): + """Property 1.""" + zeroed = [] + for h in households: + z = copy(h) + for p in members(z): + p[PREMIUMS] = 0 + zeroed.append(z) + result = calculate(households + zeroed) + n = len(households) + for i, h in enumerate(households): + tol = tolerance(h) + premiums = total(members(h), PREMIUMS) + subtracted = float(result[n + i]) - float(result[i]) + assert -tol <= subtracted <= premiums + tol, h + if result[i] > tol: + assert subtracted == pytest.approx(premiums, abs=tol), h + + +def assert_exclusion(households, name, extra): + """Properties 2 and 3: raise the head's amount of name by extra.""" + raised = [] + for h in households: + r = copy(h) + r["head"][name] += extra + raised.append(r) + result = calculate(households + raised) + n = len(households) + for i, h in enumerate(households): + tol = tolerance(raised[i]) + before = total(filers(h), name) + after = before + extra + change = float(result[n + i]) - float(result[i]) + assert change >= -tol, (h, name, extra) + if after <= EXCLUSION: + assert change == pytest.approx(0, abs=tol), (h, name, extra) + if result[i] > tol: + expected = over_exclusion(after) - over_exclusion(before) + assert change == pytest.approx(expected, abs=tol), (h, name, extra) + + +# --------------------------------------------------------------------------- +# Deterministic tests (no Hypothesis needed). +# --------------------------------------------------------------------------- + + +def test_self_employed_premiums_subtracted_once(): + """The self-employed health insurance deduction (line 30) and line 31 + together subtract each premium once: for a self-employed claimant, a + self-employed couple, premiums above the deduction's earnings limit, and + a self-employed dependent whose premium the claimant is read to pay.""" + households = [ + {"head": person(self_employment_income=50_000, **{PREMIUMS: 5_000})}, + { + "head": person( + employment_income_before_lsr=40_000, + self_employment_income=3_000, + **{PREMIUMS: 5_000}, + ) + }, + { + "head": person(self_employment_income=60_000, **{PREMIUMS: 4_000}), + "spouse": person(self_employment_income=20_000, **{PREMIUMS: 3_000}), + }, + { + "head": person(employment_income_before_lsr=40_000, **{PREMIUMS: 2_000}), + "dependent": person(self_employment_income=8_000, **{PREMIUMS: 1_000}), + }, + ] + for h in households: + for p in members(h): + p["is_self_employed"] = p["self_employment_income"] > 0 + assert_premiums_subtracted_once(households) + + +def without_self_employed_health_insurance_ald(michigan_only=False): + """Remove the deduction from the federal or Michigan line-30 list.""" + + class reform(Reform): + def apply(self): + def modify(parameters): + deductions = ( + parameters.gov.states.mi.tax.income.household_resources_adjustments + if michigan_only + else parameters.gov.irs.ald.deductions + ) + values = [ + d + for d in deductions(f"{YEAR}-01-01") + if d != "self_employed_health_insurance_ald" + ] + deductions.update( + start=instant(f"{YEAR}-01-01"), + stop=instant(f"{YEAR}-12-31"), + value=values, + ) + return parameters + + self.modify_parameters(modify) + + return reform + + +@pytest.mark.parametrize("michigan_only", [False, True]) +def test_premiums_move_to_line_31_without_the_line_30_deduction(michigan_only): + """Removing the deduction from either list moves premiums to line 31. + Removing it only from Michigan's list leaves federal AGI unchanged.""" + households = [ + {"head": person(self_employment_income=50_000, **{PREMIUMS: 5_000})}, + { + "head": person( + employment_income_before_lsr=40_000, + self_employment_income=3_000, + **{PREMIUMS: 5_000}, + ) + }, + ] + for h in households: + h["head"]["is_self_employed"] = True + situation = build_situation(households) + baseline = Simulation(situation=situation) + reformed = Simulation( + situation=situation, + reform=without_self_employed_health_insurance_ald(michigan_only), + ) + # The deduction is still computed under either list reform. + deduction = baseline.calculate("self_employed_health_insurance_ald", YEAR) + # These are PolicyEngine's federal amounts: its pre-existing earnings + # limit is before deductible self-employment tax. Form 7206 gives + # 3,000 - 211.94 = 2,788.06 in the second case. Michigan resources are + # unchanged: line 30 is 3,000 and line 31 is 2,211.94, totaling 5,211.94. + assert deduction == pytest.approx([5_000, 3_000], abs=TOLERANCE) + agi_change = reformed.calculate("adjusted_gross_income", YEAR) - baseline.calculate( + "adjusted_gross_income", YEAR + ) + expected_agi_change = np.zeros_like(deduction) if michigan_only else deduction + assert agi_change == pytest.approx(expected_agi_change, abs=TOLERANCE) + assert reformed.calculate( + "self_employed_health_insurance_ald", YEAR + ) == pytest.approx(deduction, abs=TOLERANCE) + expected = baseline.calculate("mi_household_resources", YEAR) + result = reformed.calculate("mi_household_resources", YEAR) + assert result == pytest.approx(expected, abs=TOLERANCE) + # Line 33 from the form: 50,000 - 3,532.39 - 5,000 and + # 43,000 - 211.94 - 5,000. + assert result == pytest.approx([41_467.61, 37_788.06], abs=TOLERANCE) + + +def with_exclusions(amount): + """A reform that sets both $300 exclusions to amount.""" + + class reform(Reform): + def apply(self): + def modify(parameters): + p = parameters.gov.states.mi.tax.income + for node in ( + p.household_resources_gambling_exclusion, + p.household_resources_gift_exclusion, + ): + node.update( + start=instant(f"{YEAR}-01-01"), + stop=instant(f"{YEAR}-12-31"), + value=amount, + ) + return parameters + + self.modify_parameters(modify) + + return reform + + +def test_exclusions_read_the_parameters(): + """The formula reads the two exclusion parameters, not a fixed 300: + raising both to 500 changes what counts.""" + h = { + "head": person( + employment_income_before_lsr=20_000, + gambling_winnings=400, + financial_assistance=600, + ) + } + situation = build_situation([h]) + baseline = Simulation(situation=situation) + reformed = Simulation(situation=situation, reform=with_exclusions(500)) + # $300: 100 of winnings and 300 of gifts count. + assert baseline.calculate("mi_household_resources", YEAR)[0] == pytest.approx( + 20_400, abs=TOLERANCE + ) + # $500: no winnings and 100 of gifts count. + assert reformed.calculate("mi_household_resources", YEAR)[0] == pytest.approx( + 20_100, abs=TOLERANCE + ) + + +@pytest.mark.parametrize("name", ["gambling_winnings", "financial_assistance"]) +def test_exclusion_grid(name): + """For single and joint claimants, every step from 0 to $1,000 in $100s: + nothing counts until the claimant's and spouse's total passes $300.""" + households = [] + for spouse_amount in [None, 0, 200]: + for start in range(0, 1_000, 100): + h = {"head": person(employment_income_before_lsr=20_000, **{name: start})} + if spouse_amount is not None: + h["spouse"] = person(**{name: spouse_amount}) + households.append(h) + assert_exclusion(households, name, 100) + + +def test_dependent_amounts_do_not_use_the_exclusion(): + """A dependent's own winnings and gifts are on the dependent's return: + they neither count nor use up the claimant's $300.""" + base = { + "head": person(employment_income_before_lsr=20_000), + "dependent": person(gambling_winnings=5_000, financial_assistance=5_000), + } + with_head = copy(base) + with_head["head"]["gambling_winnings"] = 500 + with_head["head"]["financial_assistance"] = 500 + result = calculate([base, with_head]) + assert result[0] == pytest.approx(20_000, abs=TOLERANCE) + assert result[1] == pytest.approx(20_000 + 200 + 200, abs=TOLERANCE) + + +# --------------------------------------------------------------------------- +# Hypothesis properties. Hypothesis is a dev extra: without it these tests +# are not collected and the deterministic tests above still run. +# --------------------------------------------------------------------------- + +try: + import hypothesis + import hypothesis.strategies as st +except ImportError: # pragma: no cover + hypothesis = None + +if hypothesis is not None: + # Each example builds a Simulation; on a loaded runner input generation + # can trip the too_slow health check, which says nothing about the model. + SLOW = [hypothesis.HealthCheck.too_slow] + INCOME = st.one_of(st.just(0), st.integers(min_value=100, max_value=150_000)) + SELF_EMPLOYMENT = st.one_of( + st.just(0), st.integers(min_value=-30_000, max_value=150_000) + ) + PREMIUM = st.one_of(st.just(0), st.integers(min_value=100, max_value=20_000)) + # Small amounts, so that many households stay within the $300. + SMALL = st.one_of(st.just(0), st.integers(min_value=1, max_value=1_000)) + + @st.composite + def a_person(draw): + return person( + employment_income_before_lsr=draw(INCOME), + self_employment_income=draw(SELF_EMPLOYMENT), + is_self_employed=draw(st.booleans()), + gambling_winnings=draw(SMALL), + financial_assistance=draw(SMALL), + **{PREMIUMS: draw(PREMIUM)}, + ) + + @st.composite + def household(draw): + return { + "head": draw(a_person()), + "spouse": draw(st.one_of(st.none(), a_person())), + "dependent": draw(st.one_of(st.none(), a_person())), + } + + @hypothesis.settings( + max_examples=40, deadline=None, derandomize=True, suppress_health_check=SLOW + ) + @hypothesis.given(st.lists(household(), min_size=1, max_size=15)) + def test_premiums_subtracted_once(households): + assert_premiums_subtracted_once(households) + + @hypothesis.settings( + max_examples=40, deadline=None, derandomize=True, suppress_health_check=SLOW + ) + @hypothesis.given( + st.lists(household(), min_size=1, max_size=15), + st.sampled_from(["gambling_winnings", "financial_assistance"]), + st.integers(min_value=1, max_value=600), + ) + def test_exclusions(households, name, extra): + assert_exclusion(households, name, extra) diff --git a/policyengine_us/tests/test_mi_household_resources_losses.py b/policyengine_us/tests/test_mi_household_resources_losses.py index b8e2af53f29..344d91f82ff 100644 --- a/policyengine_us/tests/test_mi_household_resources_losses.py +++ b/policyengine_us/tests/test_mi_household_resources_losses.py @@ -408,12 +408,16 @@ def household(draw): "other_net_gain": draw(st.one_of(st.just(0), AMOUNT, SMALL)), } - @hypothesis.settings(max_examples=25, deadline=None, suppress_health_check=SLOW) + @hypothesis.settings( + max_examples=25, deadline=None, derandomize=True, suppress_health_check=SLOW + ) @hypothesis.given(st.lists(household(), min_size=1, max_size=20)) def test_properties(households): assert_properties(households) - @hypothesis.settings(max_examples=25, deadline=None, suppress_health_check=SLOW) + @hypothesis.settings( + max_examples=25, deadline=None, derandomize=True, suppress_health_check=SLOW + ) @hypothesis.given( st.lists(household(), min_size=1, max_size=10), st.sampled_from(NO_SE_TAX_SOURCES), diff --git a/policyengine_us/tests/test_mi_household_resources_members.py b/policyengine_us/tests/test_mi_household_resources_members.py index fd9f10f8a5b..08eadb4c63a 100644 --- a/policyengine_us/tests/test_mi_household_resources_members.py +++ b/policyengine_us/tests/test_mi_household_resources_members.py @@ -16,14 +16,18 @@ Line 31 is "insurance premiums you paid for yourself and your family"; a premium on any member's record is read as one the claimant or spouse paid -for the family's coverage. +for the family's coverage. Line 31 leaves out "any insurance premiums +deducted on lines 21 or 30" (page 33), so the head's and spouse's +self-employed health insurance deduction, which is on line 30, is not +subtracted again. Lines 20 and 24 count gambling winnings and gifts "over +$300" (MCL 206.510(1)(a) and (b)), one amount for the claimant and spouse. So for every household: 1. mi_household_resources equals a reference written from the form: the head and spouse's lines 14 to 20 and 23 to 26 and their line 30 adjustments, - plus every member's lines 21, 22 and 27, less every member's premiums, - floored at zero. + plus every member's lines 21, 22 and 27, less every member's premiums not + deducted on line 30, floored at zero. 2. A dependent's own income and adjustments do not matter: zeroing them leaves household resources unchanged. 3. Amounts received for the household count wherever they are recorded: @@ -72,8 +76,10 @@ "long_term_capital_gains": 19, "alimony_income": 20, "gambling_winnings": 20, + "miscellaneous_income": 20, "unemployment_compensation": 23, - "miscellaneous_income": 24, + # Cash gifts from relatives or friends outside the household. + "financial_assistance": 24, "veterans_benefits": 26, "workers_compensation": 26, # Guaranteed income pilot payments: the recipient's own (line 25). @@ -91,6 +97,8 @@ "general_assistance": 27, } PREMIUMS = "health_insurance_premiums" +# MCL 206.510(1)(a) and (b): the first $300 of gifts and of gambling winnings. +EXCLUSION = 300 # Sources that can be negative. SIGNED = {"self_employment_income", "s_corp_income", "rental_income"} SIGNED.add("long_term_capital_gains") @@ -126,6 +134,8 @@ def build_situation(households): person = {"age": {YEAR: age}} for variable in PERSON_INPUTS: person[variable] = {YEAR: p.get(variable, 0)} + # Whether the self-employed health insurance deduction applies. + person["is_self_employed"] = {YEAR: p.get("is_self_employed", False)} # Roles are set for everyone: an adult dependent would otherwise # be the spouse, and a variable input for some people gives the # others its default value, not its formula. @@ -170,13 +180,16 @@ def calculate(households): result = np.asarray(simulation.calculate("mi_household_resources", YEAR)) # Each person's model amounts for the line 30 self-employment # adjustments, in situation order. - adjustments = sum( - np.asarray(simulation.calculate(name, YEAR)) for name in PERSON_ADJUSTMENTS - ) + adjustments = { + name: np.asarray(simulation.calculate(name, YEAR)) + for name in PERSON_ADJUSTMENTS + } per_household, k = [], 0 for h in households: n = len(everyone(h)) - per_household.append(adjustments[k : k + n]) + per_household.append( + {name: values[k : k + n] for name, values in adjustments.items()} + ) k += n return result, per_household @@ -198,14 +211,31 @@ def reference(h, person_adjustments): line_17 = max(0, amount(own, ["rental_income"])) line_18 = amount(own, by_line(CLAIMANT_LINES, 18)) line_19 = max(-CAPITAL_LOSS_LIMIT, amount(own, ["long_term_capital_gains"])) - lines_20_to_26 = amount(own, by_line(CLAIMANT_LINES, 20, 23, 24, 25, 26)) + excluded = ["gambling_winnings", "financial_assistance"] + lines_20_to_26 = amount( + own, + [ + name + for name in by_line(CLAIMANT_LINES, 20, 23, 24, 25, 26) + if name not in excluded + ], + ) + # Lines 20 and 24: gambling winnings and gifts over $300. + for name in excluded: + lines_20_to_26 += max(0, amount(own, [name]) - EXCLUSION) received = amount(everyone(h), list(HOUSEHOLD_LINES)) # Line 27: the household's Family Independence Program grant, in full. received += h.get("tanf", 0) # Line 30: the head and spouse come first in each household's slice. - line_30 = float(sum(person_adjustments[: len(own)])) + filer_adjustments = { + name: float(sum(values[: len(own)])) + for name, values in person_adjustments.items() + } + line_30 = sum(filer_adjustments.values()) line_30 += amount(own, ["early_withdrawal_penalty"]) + # Line 31: every member's premiums, less those deducted on line 30. line_31 = amount(everyone(h), [PREMIUMS]) + line_31 -= filer_adjustments["self_employed_health_insurance_ald_person"] total = line_14 + line_15 + line_16 + line_17 + line_18 + line_19 total += lines_20_to_26 + received return max(0, total - line_30 - line_31) @@ -441,6 +471,7 @@ def person(draw): names = draw(st.lists(st.sampled_from(PERSON_INPUTS), max_size=5, unique=True)) for name in names: p[name] = draw(SIGNED_AMOUNT if name in SIGNED else AMOUNT) + p["is_self_employed"] = draw(st.booleans()) return p @st.composite @@ -457,12 +488,16 @@ def household(draw): "tanf": draw(st.one_of(st.just(0), AMOUNT)), } - @hypothesis.settings(max_examples=50, deadline=None, suppress_health_check=SLOW) + @hypothesis.settings( + max_examples=50, deadline=None, derandomize=True, suppress_health_check=SLOW + ) @hypothesis.given(st.lists(household(), min_size=1, max_size=15)) def test_properties(households): assert_properties(households) - @hypothesis.settings(max_examples=30, deadline=None, suppress_health_check=SLOW) + @hypothesis.settings( + max_examples=30, deadline=None, derandomize=True, suppress_health_check=SLOW + ) @hypothesis.given( st.lists(household(), min_size=1, max_size=10), st.sampled_from(list(HOUSEHOLD_LINES)), diff --git a/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py b/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py index 103dffc9cdc..d4301481468 100644 --- a/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py +++ b/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py @@ -25,16 +25,11 @@ class mi_household_resources(Variable): "taxes/-/media/Project/Websites/taxes/Forms/IIT/TY2025/" "MI-1040CR-7-Book.pdf?rev=e4ba4fedc63942e48442037517312d4a" "&hash=AF396E577B1932DF55BFD7F22D7EF06A#page=8", - # 2025 MI-1040 book: "Total Household Resources" (page 26) and - # MI-1040CR lines 14 to 17 (page 31) and 18 to 31 (pages 32 and 33). + # 2025 MI-1040 book: "Total Household Resources" (pages 26 and 27) + # and MI-1040CR lines 14 to 17 (page 31) and 18 to 31 (pages 32 and 33). + # PDF pages 26-27, 31-33. "https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/" "Forms/IIT/TY2025/MI-1040-Book.pdf#page=26", - "https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/" - "Forms/IIT/TY2025/MI-1040-Book.pdf#page=31", - "https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/" - "Forms/IIT/TY2025/MI-1040-Book.pdf#page=32", - "https://www.michigan.gov/taxes/-/media/Project/Websites/taxes/" - "Forms/IIT/TY2025/MI-1040-Book.pdf#page=33", ) def formula(tax_unit, period, parameters): @@ -60,6 +55,16 @@ def formula(tax_unit, period, parameters): "farm_rent_income", } + # MCL 206.510(1): income does not include "(a) The first $300.00 of + # gifts in cash or kind from nongovernmental sources" or "(b) The + # first $300.00 received from awards, prizes, lottery, bingo, or + # other gambling winnings". The MI-1040CR enters gambling winnings + # "over $300" on line 20 and "the value over $300 in gifts" on line + # 24 (2025 MI-1040 book, page 32), one amount for the claimant and + # spouse, so the exclusion applies once to their total. + winnings_sources = {"gambling_winnings"} + gift_sources = {"financial_assistance"} + # MCL 206.508(3): "'Household' means a claimant and spouse", and # (4) counts "all income received by all persons of a household". # The MI-1040CR says "Include all taxable and nontaxable income you @@ -78,6 +83,8 @@ def formula(tax_unit, period, parameters): business_income = 0 rental_income = 0 + winnings = 0 + gifts = 0 other_income = 0 for source in p.household_resources: entity = tax_unit.entity.get_variable(source).entity @@ -90,9 +97,19 @@ def formula(tax_unit, period, parameters): business_income += amount elif source in rental_sources: rental_income += amount + elif source in winnings_sources: + winnings += amount + elif source in gift_sources: + gifts += amount else: other_income += amount - total = other_income + max_(business_income, 0) + max_(rental_income, 0) + total = ( + other_income + + max_(business_income, 0) + + max_(rental_income, 0) + + max_(winnings - p.household_resources_gambling_exclusion, 0) + + max_(gifts - p.household_resources_gift_exclusion, 0) + ) # Line 30: "Enter total adjustments from your U.S. Form 1040, # Schedule 1." These are the Schedule 1 Part II adjustments, counted @@ -117,19 +134,33 @@ def formula(tax_unit, period, parameters): # does. A tax-unit-level adjustment already describes the filer's return, # including educator_expense_ald, capped per eligible educator. ald = parameters(period).gov.irs.ald - adjustments = tax_unit_non_dep_add( - tax_unit, - period, - [ - deduction - for deduction in p.household_resources_adjustments - if deduction in ald.deductions - ], - ) + deductions = [ + deduction + for deduction in p.household_resources_adjustments + if deduction in ald.deductions + ] + adjustments = tax_unit_non_dep_add(tax_unit, period, deductions) # Line 31: "insurance premiums you paid for yourself and your # family". MCL 206.510(1) lets a person deduct "the amount that # person paid in premiums ... for that insurance plan for the # person's family". A premium on any member's record is read as one # the claimant or spouse paid for the family's coverage. - health_insurance_premiums = add(tax_unit, period, ["health_insurance_premiums"]) - return max_(0, total - adjustments - health_insurance_premiums) + premiums = add(tax_unit, period, ["health_insurance_premiums"]) + # "Do not include any insurance premiums deducted on lines 21 or 30" + # (2025 MI-1040 book, page 33). Line 30 includes the self-employed + # health insurance deduction (Schedule 1 line 17), so those premiums + # leave line 31 and each premium dollar is subtracted once. + # A supplied tax-unit deduction is assumed to cover premiums also + # represented in health_insurance_premiums. The aggregate inputs do + # not identify the overlap if the deducted plan's premiums are omitted. + # Line 21 nets out Medicare premiums withheld from Social Security or + # railroad retirement benefits (MCL 206.510(1)(h)). Here line 21 has + # gross benefits and line 31 the premiums, which subtracts them once + # as well. + premiums_on_line_30 = ( + tax_unit("self_employed_health_insurance_ald", period) + if "self_employed_health_insurance_ald" in deductions + else 0 + ) + line_31 = max_(premiums - premiums_on_line_30, 0) + return max_(0, total - adjustments - line_31)