diff --git a/changelog.d/mo-spouse-fagi-division.fixed.md b/changelog.d/mo-spouse-fagi-division.fixed.md new file mode 100644 index 00000000000..e30e5bb3f04 --- /dev/null +++ b/changelog.d/mo-spouse-fagi-division.fixed.md @@ -0,0 +1 @@ +Divide federal adjusted gross income between spouses on a Missouri combined return the way the Form MO-1040 Line 1 worksheet and 12 CSR 10-2.010 do. Each spouse's own federal adjustments and losses stay in their own column, and capital losses follow 12 CSR 10-2.010(2). From 2024, a negative federal AGI is replaced by zero, and when joint federal AGI is positive the other spouse enters the whole joint amount (12 CSR 10-2.710). A spouse's Missouri AGI can be negative: Line 6 adds the two columns, and only the Line 7 income percentage is floored. diff --git a/policyengine_us/parameters/gov/states/mo/tax/income/federal_agi/negative_floor_applies.yaml b/policyengine_us/parameters/gov/states/mo/tax/income/federal_agi/negative_floor_applies.yaml new file mode 100644 index 00000000000..9bb6bf86293 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mo/tax/income/federal_agi/negative_floor_applies.yaml @@ -0,0 +1,31 @@ +description: Missouri starts a filer's Missouri adjusted gross income from zero instead of a negative federal adjusted gross income, and gives the whole joint federal adjusted gross income to the spouse whose separate amount is positive, if this is true. +metadata: + unit: bool + period: year + label: Missouri negative federal adjusted gross income floor applies + reference: + - title: 12 CSR 10-2.710(1), amended effective July 30, 2024 + href: https://www.law.cornell.edu/regulations/missouri/12-CSR-10-2-710 + - title: 12 CSR 10-2.010(5), amended effective April 30, 2026 + href: https://dor.mo.gov/resources/official-final-rules/documents/12_CSR_10-2_010_Income_Tax_of_Current_or_Former_Spouses.pdf#page=4 + - title: 2025 MO-1040 Book - Individual Income Tax Long Form, Negative Federal Adjusted Gross Income + href: https://dor.mo.gov/forms/MO-1040%20Instructions_2025.pdf#page=6 + - title: 2024 MO-1040 Book - Individual Income Tax Long Form, Line 1 + href: https://dor.mo.gov/forms/MO-1040%20Instructions_2024.pdf#page=6 + - title: 2023 MO-1040 Book - Individual Income Tax Long Form, Line 7 note on negative income + href: https://dor.mo.gov/forms/MO-1040%20Instructions_2023.pdf#page=7 + +values: + # Through 2023 a spouse's column on the combined return could be negative: + # the 2023 instructions' Line 7 note covers "one spouse has negative income". + 2019-01-01: false + # The 2024 instructions are the first to say "If the amount you would + # otherwise report as your federal adjusted gross income would be a negative + # number, enter $0 on Line 1", without saying how a combined return applies + # it. The 2025 instructions apply the same 12 CSR 10-2.710(1) to combined + # returns: both spouses enter $0 when joint federal AGI is negative, and when + # it is positive the spouse with a negative separate amount enters $0 and the + # other spouse enters the whole joint amount. That reading also keeps the 2024 + # worksheet's rule that the two columns add up to joint federal AGI, so it is + # applied from 2024. + 2024-01-01: true diff --git a/policyengine_us/parameters/gov/states/mo/tax/income/subtractions/plan_529_contributions/limited_to_own_agi.yaml b/policyengine_us/parameters/gov/states/mo/tax/income/subtractions/plan_529_contributions/limited_to_own_agi.yaml new file mode 100644 index 00000000000..2aac3769288 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mo/tax/income/subtractions/plan_529_contributions/limited_to_own_agi.yaml @@ -0,0 +1,22 @@ +description: Missouri limits each filer's 529 plan contribution subtraction to their Missouri adjusted gross income before it, if this is true. +metadata: + unit: bool + period: year + label: Missouri 529 plan contribution subtraction limited to the filer's Missouri adjusted gross income + reference: + - title: 12 CSR 10-2.010(4)(A)3, amended effective February 29, 2024 + href: https://dor.mo.gov/resources/official-final-rules/documents/12_CSR_10-2_010.pdf#page=5 + - title: 12 CSR 10-2.010(4)(A)2, amended effective April 30, 2026 + href: https://dor.mo.gov/resources/official-final-rules/documents/12_CSR_10-2_010_Income_Tax_of_Current_or_Former_Spouses.pdf#page=4 + +values: + # The 2019 version of 12 CSR 10-2.010 covers only capital losses, and RSMo + # 166.435 sets no limit beyond the dollar cap. + 2019-01-01: false + # The version effective February 29, 2024 gives a spouse with $1,000 of + # federal AGI and a $5,000 MOST contribution a Missouri AGI of zero "because + # the MOST subtraction is limited to the spouse's Missouri adjusted gross + # income". It governs the 2024 and 2025 returns. + 2024-01-01: true + # The version effective April 30, 2026 gives the same spouse -$4,000. + 2026-01-01: false diff --git a/policyengine_us/tests/core/test_mo_combined_return_federal_agi_properties.py b/policyengine_us/tests/core/test_mo_combined_return_federal_agi_properties.py new file mode 100644 index 00000000000..3a3faf6bd4e --- /dev/null +++ b/policyengine_us/tests/core/test_mo_combined_return_federal_agi_properties.py @@ -0,0 +1,351 @@ +"""Properties of Missouri's division of federal AGI between spouses. + +Form MO-1040 Line 1 splits a couple's joint federal AGI between their two +columns with the Line 1 worksheet: each spouse's own income or loss, less +their own federal adjustments, and the columns add up to joint federal AGI +(2023 instructions, pages 6-7). 12 CSR 10-2.010(2) gives the deductible +excess of capital losses to "the spouse responsible for the excess", pro rata +when both are. From 2024 a negative amount is replaced by zero: when joint +federal AGI is zero or less both columns are zero, and when it is positive a +spouse with a negative separate amount enters zero and the other spouse the +whole joint amount (2025 instructions, page 6; 12 CSR 10-2.710(1); 12 CSR +10-2.010(5)). Line 5 subtracts each spouse's Missouri subtractions without a +floor, Line 6 adds the columns, and the Line 7 income percentage gives a +negative column 0%. + +The YAML cases pin the published examples. This test checks what YAML cannot, +for every drawn couple, and for each couple both ways round: as drawn and with +the head and spouse labels exchanged. Hypothesis draws batches of Missouri +tax units (single filers and married couples filing jointly) with wages, +self-employment and rental income or losses, short- and long-term capital +gains or losses, and the spouse's own IRA, educator, early withdrawal, +alimony and health savings account deductions, U.S. obligation interest and +529 contributions. A seeded population adds breadth in one year of each +regime. Each batch is one vectorized simulation. + +1. Accounting: the filers' Line 1 amounts add up to federal AGI through 2023, + and to federal AGI floored at zero from 2024. A dependent's is zero. +2. Differential: each filer's Line 1 equals an independent numpy statement of + the worksheet built from the inputs, with the 12 CSR 10-2.010(2) + capital-loss rule and, from 2024, the negative-AGI rule. Line 5 equals + Line 1 less the person's subtractions, with the MOST subtraction limited to + what is left in 2024 and 2025 (12 CSR 10-2.010(4)(A)3 as effective + February 29, 2024). +3. Labels: exchanging the head and spouse labels changes no person's Line 1, + Line 5 or Missouri taxable income, and no tax unit's Missouri income tax. +4. Line 7: no one's Missouri taxable income is negative; a person whose Line + 5 is zero or less has none; the couple's taxable income is at most Line 6 + floored at zero; and when both columns are positive it is split in the + ratio of the columns. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.05 # dollars; values are stored in float32 +CAPITAL_LOSS_LIMIT = 3_000 # 26 U.S.C. 1211(b)(1), joint return + +PERSON_INPUTS = [ + "employment_income", + "self_employment_income", + "rental_income", + "short_term_capital_gains", + "long_term_capital_gains", + "traditional_ira_contributions", + "educator_expense", + "early_withdrawal_penalty", + "alimony_expense", + "health_savings_account_ald_person", + "us_govt_interest_person", + "investment_in_529_plan_indv", +] + + +def money(low, high): + return st.integers(low, high).map(float) + + +def maybe(strategy): + return st.one_of(st.just(0.0), strategy) + + +@st.composite +def person_amounts(draw): + return { + "employment_income": draw(maybe(money(1, 120_000))), + "self_employment_income": draw(maybe(money(-60_000, 60_000))), + "rental_income": draw(maybe(money(-20_000, 20_000))), + "short_term_capital_gains": draw(maybe(money(-10_000, 10_000))), + "long_term_capital_gains": draw(maybe(money(-20_000, 30_000))), + "traditional_ira_contributions": draw(maybe(money(1, 7_000))), + # The $300 educator expense cap applies per educator. + "educator_expense": draw(maybe(money(1, 300))), + "early_withdrawal_penalty": draw(maybe(money(1, 500))), + # Alimony under a pre-2019 decree is deductible. + "alimony_expense": draw(maybe(money(1, 15_000))), + "health_savings_account_ald_person": draw(maybe(money(1, 4_000))), + "us_govt_interest_person": draw(maybe(money(1, 5_000))), + # Up to $8,000 each, so a couple stays within the $16,000 cap. + "investment_in_529_plan_indv": draw(maybe(money(1, 8_000))), + } + + +@st.composite +def tax_units(draw): + joint = draw(st.booleans()) + return { + "head": draw(person_amounts()), + "spouse": draw(person_amounts()) if joint else None, + } + + +SEED = 20261008 + + +def _seeded_units(n=150): + rng = np.random.default_rng(SEED) + + def some(low, high, p): + return float(rng.integers(low, high + 1)) if rng.random() < p else 0.0 + + def amounts(): + return { + "employment_income": some(1, 120_000, 0.7), + "self_employment_income": some(-60_000, 60_000, 0.4), + "rental_income": some(-20_000, 20_000, 0.2), + "short_term_capital_gains": some(-10_000, 10_000, 0.25), + "long_term_capital_gains": some(-20_000, 30_000, 0.3), + "traditional_ira_contributions": some(1, 7_000, 0.3), + "educator_expense": some(1, 300, 0.2), + "early_withdrawal_penalty": some(1, 500, 0.1), + "alimony_expense": some(1, 15_000, 0.1), + "health_savings_account_ald_person": some(1, 4_000, 0.2), + "us_govt_interest_person": some(1, 5_000, 0.15), + "investment_in_529_plan_indv": some(1, 8_000, 0.15), + } + + return [ + {"head": amounts(), "spouse": amounts() if rng.random() < 0.8 else None} + for _ in range(n) + ] + + +HEAD = dict(is_tax_unit_head=True, is_tax_unit_spouse=False) +SPOUSE = dict(is_tax_unit_head=False, is_tax_unit_spouse=True) + + +def _person(amounts, role): + return { + "age": 45, + **role, + **amounts, + # U.S. obligation interest is part of taxable interest. + "taxable_interest_income": amounts["us_govt_interest_person"], + "divorce_year": 2015, + } + + +def _situation(units, year): + """Each unit twice: as drawn, then with the labels exchanged. + + People are listed in the same order in both copies, so person k of copy + one is the same drawn person as person k of copy two. + """ + people = {} + groups = {"tax_units": {}, "marital_units": {}, "households": {}} + for copy in (0, 1): + for i, u in enumerate(units): + a, b = f"a_{copy}_{i}", f"b_{copy}_{i}" + members = [a] + if u["spouse"] is None: + people[a] = _person(u["head"], HEAD) + else: + first, second = (HEAD, SPOUSE) if copy == 0 else (SPOUSE, HEAD) + people[a] = _person(u["head"], first) + people[b] = _person(u["spouse"], second) + members.append(b) + hsa = sum(people[m]["health_savings_account_ald_person"] for m in members) + name = f"{copy}_{i}" + groups["tax_units"][name] = { + "members": members, + "health_savings_account_ald": hsa, + } + groups["marital_units"][name] = {"members": members} + groups["households"][name] = {"members": members, "state_code": "MO"} + + def by_year(values): + return {k: (v if k == "members" else {year: v}) for k, v in values.items()} + + return { + "people": {name: by_year(values) for name, values in people.items()}, + **{ + plural: {name: by_year(values) for name, values in entities.items()} + for plural, entities in groups.items() + }, + } + + +PERSON_OUTPUTS = [ + "mo_federal_adjusted_gross_income", + "mo_adjusted_gross_income", + "mo_taxable_income", + "mo_agi_subtractions", + "mo_529_deduction", + "self_employment_tax_ald_person", + "is_tax_unit_dependent", +] + + +def _run(units, year): + sim = Simulation(situation=_situation(units, year)) + person = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in PERSON_OUTPUTS + PERSON_INPUTS + } + person["unit"] = sim.populations["tax_unit"].members_entity_id + unit = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in ["adjusted_gross_income", "mo_income_tax"] + } + return person, unit + + +def _unit_sum(person, values, n): + return np.bincount(person["unit"], weights=values, minlength=n) + + +def _reference_line_1(person, n, floor_applies): + """Line 1 from the worksheet: each spouse's own income or loss less their + own adjustments, with 12 CSR 10-2.010(2) for a net capital loss.""" + own = ( + person["employment_income"] + + person["self_employment_income"] + + person["rental_income"] + + person["us_govt_interest_person"] + - person["self_employment_tax_ald_person"] + - person["traditional_ira_contributions"] + - person["educator_expense"] + - person["early_withdrawal_penalty"] + - person["alimony_expense"] + - person["health_savings_account_ald_person"] + ) + short = person["short_term_capital_gains"] + long = person["long_term_capital_gains"] + net = _unit_sum(person, short + long, n)[person["unit"]] + # Each character's net loss, attributed to the spouses with losses of + # that character (Example No. 2). + responsibility = 0.0 + for amount in (short, long): + character_net = _unit_sum(person, amount, n)[person["unit"]] + own_loss = np.maximum(0, -amount) + unit_loss = _unit_sum(person, own_loss, n)[person["unit"]] + share = np.divide( + own_loss, unit_loss, out=np.zeros_like(own_loss), where=unit_loss > 0 + ) + responsibility = responsibility + np.maximum(0, -character_net) * share + unit_responsibility = _unit_sum(person, responsibility, n)[person["unit"]] + loss_share = np.divide( + responsibility, + unit_responsibility, + out=np.zeros_like(responsibility), + where=unit_responsibility > 0, + ) + deductible_loss = np.minimum(CAPITAL_LOSS_LIMIT, np.maximum(0, -net)) + # With a net gain each spouse's own gain or loss is in their column; with + # a net loss the gains drop out and each takes their share of the + # deductible loss. + capital = np.where(net >= 0, short + long, -deductible_loss * loss_share) + separate = own + capital + if not floor_applies: + return separate + joint = _unit_sum(person, separate, n)[person["unit"]] + has_negative = _unit_sum(person, (separate < 0).astype(float), n)[person["unit"]] + line_1 = np.where(separate < 0, 0, np.where(has_negative > 0, joint, separate)) + return np.where(joint > 0, line_1, 0) + + +def _check(units, year): + person, unit = _run(units, year) + n = len(unit["adjusted_gross_income"]) + line_1 = person["mo_federal_adjusted_gross_income"] + line_5 = person["mo_adjusted_gross_income"] + taxable = person["mo_taxable_income"] + # The years come from the sources, not from the model's parameters: + # 12 CSR 10-2.710(1) and the 2024 and 2025 instructions from 2024, and + # the MOST limit under the 12 CSR 10-2.010 version effective February 29, + # 2024 for 2024 and 2025. + floor_applies = year >= 2024 + most_limited = 2024 <= year <= 2025 + + # 1. Accounting. + agi = unit["adjusted_gross_income"] + expected_total = np.maximum(0, agi) if floor_applies else agi + np.testing.assert_allclose( + _unit_sum(person, line_1, n), expected_total, atol=TOLERANCE + ) + assert (line_1[person["is_tax_unit_dependent"] > 0] == 0).all() + + # 2. Differential against the worksheet. + np.testing.assert_allclose( + line_1, _reference_line_1(person, n, floor_applies), atol=TOLERANCE + ) + most = person["mo_529_deduction"] + other = person["mo_agi_subtractions"] - most + if most_limited: + most = np.minimum(most, np.maximum(0, line_1 - other)) + np.testing.assert_allclose(line_5, line_1 - other - most, atol=TOLERANCE) + + # 3. Labels: the second half of the people and tax units is the first + # half with the labels exchanged. + half_people, half_units = len(line_1) // 2, n // 2 + for values in (line_1, line_5, taxable): + np.testing.assert_allclose( + values[:half_people], values[half_people:], atol=TOLERANCE + ) + tax = unit["mo_income_tax"] + np.testing.assert_allclose(tax[:half_units], tax[half_units:], atol=TOLERANCE) + + # 4. Line 7. + assert (taxable >= -TOLERANCE).all() + assert (np.abs(taxable[line_5 <= 0]) <= TOLERANCE).all() + line_6 = _unit_sum(person, line_5, n) + assert (_unit_sum(person, taxable, n) <= np.maximum(0, line_6) + TOLERANCE).all() + other_line_5 = _unit_sum(person, line_5, n)[person["unit"]] - line_5 + other_taxable = _unit_sum(person, taxable, n)[person["unit"]] - taxable + both_positive = (line_5 > 0) & (other_line_5 > 0) + np.testing.assert_allclose( + (taxable * other_line_5)[both_positive], + (other_taxable * line_5)[both_positive], + rtol=1e-4, + atol=1.0, + ) + + +# Each example is one vectorized batch of tax units, so a few examples cover +# many units; the seeded population runs once per year. +SETTINGS = dict( + max_examples=3, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given( + st.lists(tax_units(), min_size=10, max_size=40), + st.sampled_from([2022, 2024, 2025, 2026]), +) +def test_mo_federal_agi_division_properties(units, year): + _check(units, year) + + +# One year in each regime: columns may be negative (2023); the negative-AGI +# rule with the MOST limit (2025); the negative-AGI rule without it (2026). +@pytest.mark.parametrize("year", [2023, 2025, 2026]) +def test_seeded_population(year): + _check(_seeded_units(), year) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/adjusted_gross_income/mo_adjusted_gross_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/adjusted_gross_income/mo_adjusted_gross_income.yaml index 4a0f71ad81f..d9c22af1764 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/adjusted_gross_income/mo_adjusted_gross_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/adjusted_gross_income/mo_adjusted_gross_income.yaml @@ -46,7 +46,9 @@ mo_qualified_health_insurance_premiums: 3_000 state_code: MO output: - mo_adjusted_gross_income: 0 # AGI cannot be negative + # Form MO-1040 Line 5, "Subtract Line 4 from Line 3", has no floor; the + # floor is on the joint subtotal, Line 26. + mo_adjusted_gross_income: -1_000 - name: Dependent's self-employment tax deduction neither reduces nor raises the parent's Missouri AGI period: 2025 @@ -75,3 +77,186 @@ # -706.48. adjusted_gross_income: 40_000 mo_adjusted_gross_income: [40_000, 0] + +- name: 12 CSR 10-2.010(4)(A)2, a spouse's subtraction for U.S. obligation interest exceeds their federal AGI + period: 2024 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 38_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + taxable_interest_income: 5_000 + us_govt_interest_person: 5_000 + self_employment_income: -4_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # Spouse 2's federal AGI is 1,000 (5,000 of interest less a 4,000 + # business loss) and the U.S. obligation interest subtraction is 5,000. + # Regulation (effective February 29, 2024): "The combined Missouri + # adjusted gross income equals thirty-four thousand dollars ($34,000) + # (thirty-eight thousand dollars ($38,000) plus negative four thousand + # dollars (-$4,000))." (main floored spouse 2 at 0 and split the loss by + # gross income share: 34,465.12 and 0.) + mo_adjusted_gross_income: [38_000, -4_000] + +- name: 12 CSR 10-2.010(4)(A)3, the MOST subtraction is limited to the spouse's Missouri AGI in 2024 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 38_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + employment_income: 1_000 + investment_in_529_plan_indv: 5_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # Regulation (effective February 29, 2024): "thirty-eight thousand + # dollars ($38,000) plus zero ... because the MOST subtraction is limited + # to the spouse's Missouri adjusted gross income." + mo_adjusted_gross_income: [38_000, 0] + +- name: The MOST limit also applies in 2025 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 38_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + employment_income: 1_000 + investment_in_529_plan_indv: 5_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + mo_adjusted_gross_income: [38_000, 0] + +- name: 12 CSR 10-2.010(4)(A)2 as amended effective April 30, 2026, the MOST subtraction is not limited + period: 2026 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 38_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + employment_income: 1_000 + investment_in_529_plan_indv: 5_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # Regulation (effective April 30, 2026): "thirty-eight thousand dollars + # ($38,000) plus negative four thousand dollars (-$4,000)". + mo_adjusted_gross_income: [38_000, -4_000] + +- name: Before 2024 the MOST subtraction is not limited + period: 2023 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 38_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + employment_income: 1_000 + investment_in_529_plan_indv: 5_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # The 2019 version of 12 CSR 10-2.010 covers only capital losses, and + # RSMo 166.435 caps the subtraction only by dollar amount. + mo_adjusted_gross_income: [38_000, -4_000] + +- name: A negative federal AGI column is replaced by zero before subtractions from 2024 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 5_000 + self_employment_income: -15_000 + us_govt_interest_person: 1_000 + taxable_interest_income: 1_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + employment_income: 100_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # Spouse 1's separate federal AGI is 6,000 - 15,000 = -9,000, so Line 1 + # is 0 for spouse 1 and the joint 91,000 for spouse 2 (2025 instructions, + # page 6). Spouse 1's Line 5 is 0 - 1,000 = -1,000. + mo_federal_adjusted_gross_income: [0, 91_000] + mo_adjusted_gross_income: [-1_000, 91_000] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/adjusted_gross_income/mo_federal_adjusted_gross_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/adjusted_gross_income/mo_federal_adjusted_gross_income.yaml new file mode 100644 index 00000000000..7d96b23dd1b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/adjusted_gross_income/mo_federal_adjusted_gross_income.yaml @@ -0,0 +1,549 @@ +# Form MO-1040 Line 1 for each filer. On a combined return the Line 1 +# worksheet puts each spouse's own income, losses and federal adjustments in +# their column, and the columns add up to joint federal AGI (2023 +# instructions, pages 6-7). From 2024 a negative amount is replaced by zero: +# 12 CSR 10-2.710(1), the 2025 instructions' Negative Federal Adjusted Gross +# Income table (page 6) and 12 CSR 10-2.010(5). + +- name: Single filer with negative federal AGI keeps it through 2023 + period: 2023 + absolute_error_margin: 0.01 + input: + people: + filer: + age: 40 + self_employment_income: -12_000 + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: MO + output: + adjusted_gross_income: -12_000 + mo_federal_adjusted_gross_income: -12_000 + +- name: Single filer with negative federal AGI enters zero from 2024 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + filer: + age: 40 + self_employment_income: -12_000 + tax_units: + tax_unit: + members: [filer] + households: + household: + members: [filer] + state_code: MO + output: + # 2024 instructions, Line 1: "If the amount you would otherwise report as + # your federal adjusted gross income would be a negative number, enter $0 + # on Line 1." + adjusted_gross_income: -12_000 + mo_federal_adjusted_gross_income: 0 + +- name: Combined return through 2023, one spouse's column negative + period: 2023 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 5_000 + self_employment_income: -15_000 + secondary: + age: 40 + is_tax_unit_spouse: true + employment_income: 100_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # Primary: 5,000 of wages and a 15,000 business loss = -10,000. + # Secondary: 100,000. The columns add up to joint federal AGI, 90,000. + # (main split the loss by gross income share: 4,285.71 and 85,714.29.) + adjusted_gross_income: 90_000 + mo_federal_adjusted_gross_income: [-10_000, 100_000] + +- name: 2025 table row 1, joint federal AGI 90,000 = (10,000) + 100,000 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 5_000 + self_employment_income: -15_000 + secondary: + age: 40 + is_tax_unit_spouse: true + employment_income: 100_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # Table: Line 1Y (primary) $0.00, Line 1S (secondary) $90,000. + adjusted_gross_income: 90_000 + mo_federal_adjusted_gross_income: [0, 90_000] + +- name: 2025 table row 1 with the head and spouse labels exchanged + period: 2025 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_spouse: true + employment_income: 5_000 + self_employment_income: -15_000 + secondary: + age: 40 + is_tax_unit_head: true + employment_income: 100_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + adjusted_gross_income: 90_000 + mo_federal_adjusted_gross_income: [0, 90_000] + +- name: 2024 applies the same rule to a combined return + period: 2024 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 5_000 + self_employment_income: -15_000 + secondary: + age: 40 + is_tax_unit_spouse: true + employment_income: 100_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # The 2024 instructions say only "enter $0 on Line 1". Giving the + # secondary spouse the whole 90,000 keeps the worksheet's rule that the + # columns add up to joint federal AGI, as the 2025 instructions do under + # the same 12 CSR 10-2.710(1). + mo_federal_adjusted_gross_income: [0, 90_000] + +- name: 2025 table row 2, joint federal AGI (50,000) = 200,000 + (250,000) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 200_000 + secondary: + age: 40 + is_tax_unit_spouse: true + self_employment_income: -250_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # Table: $0.00 and $0.00. + adjusted_gross_income: -50_000 + mo_federal_adjusted_gross_income: [0, 0] + +- name: The same couple through 2023 keeps both columns + period: 2023 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 200_000 + secondary: + age: 40 + is_tax_unit_spouse: true + self_employment_income: -250_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + mo_federal_adjusted_gross_income: [200_000, -250_000] + +- name: 2025 table row 3, joint federal AGI (17,000) = (53,000) + 36,000 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + self_employment_income: -53_000 + secondary: + age: 40 + is_tax_unit_spouse: true + employment_income: 36_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # Table: $0.00 and $0.00. + adjusted_gross_income: -17_000 + mo_federal_adjusted_gross_income: [0, 0] + +- name: 2025 table row 4, joint federal AGI (24,000) = (14,000) + (10,000) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + self_employment_income: -14_000 + secondary: + age: 40 + is_tax_unit_spouse: true + self_employment_income: -10_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # Table: $0.00 and $0.00. + adjusted_gross_income: -24_000 + mo_federal_adjusted_gross_income: [0, 0] + +- name: Each spouse's IRA, student loan interest and educator expenses stay in their column + period: 2023 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 40_000 + traditional_ira_contributions: 7_000 + student_loan_interest: 2_500 + educator_expense: 300 + secondary: + age: 40 + is_tax_unit_spouse: true + employment_income: 8_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # Primary: 40,000 - 7,000 - 2,500 - 300 = 30,200. Secondary: 8,000. + # (main split the 9,800 by gross income share: 31,833.33 and 6,366.67.) + adjusted_gross_income: 38_200 + mo_federal_adjusted_gross_income: [30_200, 8_000] + +- name: Alimony paid under a 2015 decree stays with the payer + period: 2023 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 50 + is_tax_unit_head: true + employment_income: 60_000 + alimony_expense: 12_000 + divorce_year: 2015 + secondary: + age: 50 + is_tax_unit_spouse: true + employment_income: 20_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # Primary: 60,000 - 12,000 = 48,000. Secondary: 20,000. + adjusted_gross_income: 68_000 + mo_federal_adjusted_gross_income: [48_000, 20_000] + +- name: A health savings account deduction goes to the account holder + period: 2025 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 40_000 + secondary: + age: 40 + is_tax_unit_spouse: true + employment_income: 20_000 + health_savings_account_ald_person: 4_000 + tax_units: + tax_unit: + members: [primary, secondary] + health_savings_account_ald: 4_000 + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + adjusted_gross_income: 56_000 + mo_federal_adjusted_gross_income: [40_000, 16_000] + +- name: A health savings account deduction recorded only for the tax unit is divided equally + period: 2025 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 40_000 + secondary: + age: 40 + is_tax_unit_spouse: true + employment_income: 20_000 + tax_units: + tax_unit: + members: [primary, secondary] + health_savings_account_ald: 4_000 + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # 26 U.S.C. 223(b)(5) divides a married couple's limit equally unless + # they agree otherwise: 2,000 each. + mo_federal_adjusted_gross_income: [38_000, 18_000] + +- name: 12 CSR 10-2.010(2) Example No. 1, both spouses responsible for the excess capital loss + period: 2023 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 10_000 + long_term_capital_gains: -2_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + employment_income: 5_000 + long_term_capital_gains: -3_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # The $3,000 Section 1211 deduction pro rata: 2/5 and 3/5. + # Regulation: MAGI $8,800 and $3,200, total $12,000. + adjusted_gross_income: 12_000 + mo_federal_adjusted_gross_income: [8_800, 3_200] + +- name: 12 CSR 10-2.010(2) Example No. 2, short-term and long-term netting + period: 2023 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 10_000 + short_term_capital_gains: -200 + long_term_capital_gains: -8_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + employment_income: 5_000 + short_term_capital_gains: -300 + long_term_capital_gains: 3_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # The 5,500 excess is 200 + 5,000 for spouse 1 (short-term and the net + # long-term loss) and 300 for spouse 2. The regulation's allocation: + # "Spouse 1 (5,200/5,500 x 3,000) and Spouse 2 (300/5,500 x 3,000)", + # 2,836.36 and 163.64. Its table rounds them to 2,850 and 150. + # Spouse 2's long-term gain nets against spouse 1's losses and is in + # neither column. + adjusted_gross_income: 12_000 + mo_federal_adjusted_gross_income: [7_163.64, 4_836.36] + +- name: 12 CSR 10-2.010(2) Example No. 3, one spouse responsible for the excess + period: 2023 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 10_000 + short_term_capital_gains: 1_000 + long_term_capital_gains: -8_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + employment_income: 5_000 + short_term_capital_gains: -1_000 + long_term_capital_gains: 3_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # Regulation: MAGI $7,000 and $5,000. + adjusted_gross_income: 12_000 + mo_federal_adjusted_gross_income: [7_000, 5_000] + +- name: With a net capital gain each spouse keeps their own gain or loss + period: 2023 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + employment_income: 20_000 + long_term_capital_gains: 10_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + employment_income: 20_000 + long_term_capital_gains: -4_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # The worksheet's "Capital gain or loss" line by owner: 30,000 and + # 16,000. + adjusted_gross_income: 46_000 + mo_federal_adjusted_gross_income: [30_000, 16_000] + +- name: A dependent's column is zero + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 40_000 + child: + age: 17 + employment_income: 5_000 + self_employment_income: -2_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: MO + output: + mo_federal_adjusted_gross_income: [40_000, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/income_tax/mo_income_tax_before_credits.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/income_tax/mo_income_tax_before_credits.yaml index 72fd515e064..ac0c6201362 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/income_tax/mo_income_tax_before_credits.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/income_tax/mo_income_tax_before_credits.yaml @@ -113,3 +113,80 @@ # (2% + 2.5% + 3% + 3.5% + 4% + 4.5%) x 1,348 + 4.7% x (12,300 - 9,436) # = 262.86 + 134.61 (the chart shows $263 + $134.61 = $397.61) mo_income_tax_before_credits: 0.195 * 1_348 + 0.047 * (12_300 - 9_436) + +# A combined return where one spouse's own business loss exceeds their wages. +# The Line 1 worksheet puts the loss in that spouse's column, so the other +# spouse's column carries the whole Missouri taxable income. +- name: Combined return, 2023, one spouse's column negative + period: 2023 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 5_000 + self_employment_income: -15_000 + secondary: + age: 40 + is_tax_unit_spouse: true + employment_income: 100_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # Line 1 (and 5): -10,000 and 100,000; Line 6: 90,000. + # Federal tax: 90,000 - 27,700 = 62,300 taxable; + # 10% x 22,000 + 12% x 40,300 = 7,036. + # Line 12: 15% (Line 6 from 50,001 to 100,000); Line 13: 1,055.40. + # Line 26: 90,000 - 27,700 - 1,055.40 = 61,244.60. + # Line 7: 0% and 100%, so Line 27 is 0 and 61,244.60. + # Tax: 0.195 x 1,207 + 4.95% x (61,244.60 - 8,449) = 2,848.75. + # (main split the loss by gross income share and taxed 4.76% of Line 26 + # in the primary's low brackets: 36.70 + 2,704.39 = 2,741.09.) + mo_federal_income_tax_deduction: 1_055.40 + mo_taxable_income: [0, 61_244.60] + mo_income_tax_before_credits: [0, 2_848.75] + +- name: Combined return, 2025, one spouse's separate federal AGI negative + period: 2025 + absolute_error_margin: 0.01 + input: + people: + primary: + age: 40 + is_tax_unit_head: true + employment_income: 5_000 + self_employment_income: -15_000 + secondary: + age: 40 + is_tax_unit_spouse: true + employment_income: 100_000 + tax_units: + tax_unit: + members: [primary, secondary] + marital_units: + marital_unit: + members: [primary, secondary] + households: + household: + members: [primary, secondary] + state_code: MO + output: + # Line 1: 0 and the joint 90,000 (2025 instructions, page 6). + # Federal tax: 90,000 - 31,500 = 58,500 taxable; + # 10% x 23,850 + 12% x 34,650 = 6,543. + # Line 13: 15% x 6,543 = 981.45. + # Line 26: 90,000 - 31,500 - 981.45 = 57,518.55, all the secondary's. + # Tax: 0.195 x 1,313 + 4.7% x (57,518.55 - 9,191) = 2,527.43. + # (main: 29.08 + 2,398.70 = 2,427.78.) + mo_federal_income_tax_deduction: 981.45 + mo_taxable_income: [0, 57_518.55] + mo_income_tax_before_credits: [0, 2_527.43] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/taxable_income/mo_taxable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/taxable_income/mo_taxable_income.yaml index 7d272cfa8cd..742288358b8 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/taxable_income/mo_taxable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/taxable_income/mo_taxable_income.yaml @@ -55,3 +55,37 @@ output: # 51,830 - 14,600 = 37,230 (no HoH exemption) mo_taxable_income: 37_230 + +- name: Line 7 gives a spouse with negative Missouri AGI zero percent + period: 2023 + absolute_error_margin: 0.01 + input: + people: + spouse_1: + age: 40 + is_tax_unit_head: true + mo_adjusted_gross_income: -15_000 + spouse_2: + age: 40 + is_tax_unit_spouse: true + mo_adjusted_gross_income: 30_000 + tax_units: + tax_unit: + members: [spouse_1, spouse_2] + mo_federal_income_tax_deduction: 0 + tax_unit_itemizes: false + standard_deduction: 0 + mo_itemized_deductions: 0 + marital_units: + marital_unit: + members: [spouse_1, spouse_2] + households: + household: + members: [spouse_1, spouse_2] + state_code: MO + output: + # The 2023 instructions' example (page 7): "your income is -$15,000 and + # your spouse's income is $30,000 ... enter zero percent on Line 7Y and + # 100 percent on Line 7S." Line 6 adds the columns: 15,000. With no + # deductions Line 26 is 15,000, all of it spouse 2's. + mo_taxable_income: [0, 15_000] diff --git a/policyengine_us/variables/gov/states/mo/tax/income/adjusted_gross_income/mo_adjusted_gross_income.py b/policyengine_us/variables/gov/states/mo/tax/income/adjusted_gross_income/mo_adjusted_gross_income.py index 2c10fab998c..61c4016a488 100644 --- a/policyengine_us/variables/gov/states/mo/tax/income/adjusted_gross_income/mo_adjusted_gross_income.py +++ b/policyengine_us/variables/gov/states/mo/tax/income/adjusted_gross_income/mo_adjusted_gross_income.py @@ -5,56 +5,40 @@ class mo_adjusted_gross_income(Variable): value_type = float entity = Person label = "Missouri adjusted gross income" + documentation = ( + "Each filer's Missouri adjusted gross income on Form MO-1040, Line 5: " + "their federal adjusted gross income less their Missouri " + "subtractions. A spouse's amount can be negative; Line 6 adds the two " + "columns." + ) unit = USD definition_period = YEAR reference = ( "https://dor.mo.gov/forms/MO-1040%20Fillable%20Calculating_2021.pdf", + # PDF pages 7, 22: the Line 7 note on a spouse with negative income; + # Line 5, "Subtract Line 4 from Line 3", and Line 6, "Add columns 5Y + # and 5S". + "https://dor.mo.gov/forms/MO-1040%20Instructions_2023.pdf#page=7", + # 12 CSR 10-2.010(4)(A)2 (effective February 29, 2024): a spouse's + # Missouri AGI of -$4,000 after a subtraction. + "https://dor.mo.gov/resources/official-final-rules/documents/12_CSR_10-2_010.pdf#page=5", "https://revisor.mo.gov/main/OneSection.aspx?section=143.121", ) defined_for = StateCode.MO def formula(person, period, parameters): - gross_income = person("irs_gross_income", period) - # subtract federal above-the-line deductions (ALDs) by person - # ... subtract some ALDs explicitly by person - PERSONAL_ALDS = [ - "self_employment_tax_ald_person", - "self_employed_health_insurance_ald_person", - "self_employed_pension_contribution_ald_person", - ] - tax_unit = person.tax_unit - # A tax unit dependent's deductions are on their own return, and are - # left out of above_the_line_deductions, as their income is left out - # of irs_gross_income. - not_dependent = ~person("is_tax_unit_dependent", period) - ind_total_personal_alds = not_dependent * add(person, period, PERSONAL_ALDS) - unit_total_personal_alds = tax_unit.sum(ind_total_personal_alds) - # ... subtract remaining ALDs by adhoc allocation between spouses - unit_total_alds = tax_unit("above_the_line_deductions", period) - unit_remaining_alds = unit_total_alds - unit_total_personal_alds - filing_status = person.tax_unit("filing_status", period) - is_married = filing_status == filing_status.possible_values.JOINT - is_head = person("is_tax_unit_head", period) - is_spouse = person("is_tax_unit_spouse", period) - # Allocate remaining ALDs proportionally based on gross income - # to avoid losing deductions when one spouse has no income. - # Use np.divide with mask to avoid divide-by-zero warnings. - unit_gross_income = tax_unit.sum(gross_income) - mask = unit_gross_income > 0 - # Default: head gets 100%, others get 0% (used when unit has no income) - default_share = where(is_head, 1.0, 0.0) - allocation_share = np.divide( - gross_income, - unit_gross_income, - out=default_share.copy(), # Copy to avoid modifying default_share - where=mask, - ) - allocated_alds = where( - is_head | is_spouse, - unit_remaining_alds * allocation_share, - 0, - ) - fed_agi = gross_income - ind_total_personal_alds - allocated_alds - # return MO AGI including MO additions and MO subtractions + federal_agi = person("mo_federal_adjusted_gross_income", period) + # Missouri additions (Line 2) are not modeled. subtractions = person("mo_agi_subtractions", period) - return max_(0, fed_agi - subtractions) + p = parameters(period).gov.states.mo.tax.income.subtractions + if p.plan_529_contributions.limited_to_own_agi: + # 12 CSR 10-2.010(4)(A)3 (effective February 29, 2024): "the MOST + # subtraction is limited to the spouse's Missouri adjusted gross + # income". It is taken after the other subtractions. + most = person("mo_529_deduction", period) + other_subtractions = max_(0, subtractions - most) + allowed_most = min_(most, max_(0, federal_agi - other_subtractions)) + subtractions = other_subtractions + allowed_most + # A tax unit dependent's Missouri AGI is on their own return. + filer = ~person("is_tax_unit_dependent", period) + return filer * (federal_agi - subtractions) diff --git a/policyengine_us/variables/gov/states/mo/tax/income/adjusted_gross_income/mo_federal_adjusted_gross_income.py b/policyengine_us/variables/gov/states/mo/tax/income/adjusted_gross_income/mo_federal_adjusted_gross_income.py new file mode 100644 index 00000000000..af5745bdc94 --- /dev/null +++ b/policyengine_us/variables/gov/states/mo/tax/income/adjusted_gross_income/mo_federal_adjusted_gross_income.py @@ -0,0 +1,173 @@ +from policyengine_us.model_api import * + + +# Person-level sources whose losses make up the business part of loss_ald. +BUSINESS_LOSS_SOURCES = [ + "total_self_employment_income", + "farm_operations_income", + "rental_income", + "farm_rent_income", + "estate_income", + "partnership_s_corp_income", +] +CAPITAL_GAIN_CHARACTERS = ["short_term_capital_gains", "long_term_capital_gains"] + + +class mo_federal_adjusted_gross_income(Variable): + value_type = float + entity = Person + label = "Missouri federal adjusted gross income" + documentation = ( + "Each filer's federal adjusted gross income on Form MO-1040, Line 1. " + "On a combined return each spouse's column holds their own income, " + "losses and federal adjustments to income, and the two columns add up " + "to the joint federal adjusted gross income. From 2024 a negative " + "amount is replaced by zero." + ) + unit = USD + definition_period = YEAR + reference = ( + # PDF pages 6-7: Line 1 and its worksheet. "The combined income for + # you and your spouse must equal the total federal adjusted gross + # income you reported on your federal return." + "https://dor.mo.gov/forms/MO-1040%20Instructions_2023.pdf#page=6", + # Line 1: "enter $0" for a negative federal adjusted gross income. + "https://dor.mo.gov/forms/MO-1040%20Instructions_2024.pdf#page=6", + # Negative Federal Adjusted Gross Income, citing 12 CSR 10-2.710. + "https://dor.mo.gov/forms/MO-1040%20Instructions_2025.pdf#page=6", + # PDF pages 1-4: 12 CSR 10-2.010(2), capital losses go to "the + # spouse responsible", and (4), each spouse's federal AGI as if on a + # separate federal return (effective February 29, 2024). + "https://dor.mo.gov/resources/official-final-rules/documents/12_CSR_10-2_010.pdf#page=1", + # 12 CSR 10-2.010(5), effective April 30, 2026. + "https://dor.mo.gov/resources/official-final-rules/documents/12_CSR_10-2_010_Income_Tax_of_Current_or_Former_Spouses.pdf#page=4", + "https://www.law.cornell.edu/regulations/missouri/12-CSR-10-2-710", + "https://revisor.mo.gov/main/OneSection.aspx?section=143.121", + ) + defined_for = StateCode.MO + + def formula(person, period, parameters): + tax_unit = person.tax_unit + # The head and spouse are the filers; a tax unit dependent's income + # and deductions are on their own return, as in irs_gross_income. + filer = ~person("is_tax_unit_dependent", period) + # Items that belong to the couple rather than to one spouse are + # divided equally between them. + equal_share = filer / max_(tax_unit.sum(filer), 1) + + def attribute(own, amount): + # Divide a tax-unit amount in proportion to the filers' own + # amounts, or equally when they have none. Use a mask rather than + # where to avoid a divide-by-zero warning. + own = filer * own + total = tax_unit.sum(own) + share = np.array(equal_share, dtype=float) + mask = total > 0 + share[mask] = own[mask] / total[mask] + return amount * share + + # Federal adjustments to income (Line 1 worksheet, "Subtract: federal + # adjustments to income"), each in the column of the spouse it belongs + # to. 12 CSR 10-2.010(4), effective February 29, 2024, puts each + # spouse's column on a separate-return basis. + p = parameters(period).gov.irs.ald + adjustments = 0 + for deduction in p.deductions: + if deduction == "loss_ald": + continue + if person.entity.get_variable( + deduction, check_existence=True + ).entity.is_person: + own = person(deduction, period) + adjustments = adjustments + filer * own + if deduction in p.filer_amounts_recorded_on_dependents: + # The filers' amounts recorded on a dependent. + adjustments = adjustments + equal_share * tax_unit.sum(~filer * own) + elif person.entity.get_variable(f"{deduction}_person") is not None: + adjustments = adjustments + attribute( + person(f"{deduction}_person", period), tax_unit(deduction, period) + ) + else: + adjustments = adjustments + equal_share * tax_unit(deduction, period) + + # Business and capital losses (loss_ald). The worksheet reports each + # spouse's own business, rental, farm and capital gains or losses. + loss_ald = tax_unit("loss_ald", period) + allowed_against_gains = tax_unit("capital_losses_allowed_against_gains", period) + limited_capital_loss = tax_unit("limited_capital_loss", period) + business_loss = max_(0, loss_ald - allowed_against_gains - limited_capital_loss) + # A Form 4797 loss is recorded for the tax unit, and its gain is + # divided equally in other_net_gain_gross_income. + own_business_loss = equal_share * max_(0, -tax_unit("other_net_gain", period)) + for source in BUSINESS_LOSS_SOURCES: + own_business_loss = own_business_loss + max_(0, -person(source, period)) + own_capital_gain = max_(0, person("capital_gains", period)) + max_( + 0, person("non_sch_d_capital_gains", period) + ) + own_capital_loss = person("capital_losses", period) + # 12 CSR 10-2.010(2): when the couple's capital losses exceed their + # gains, the excess deduction goes to "the spouse responsible for the + # excess", pro rata when both are. In Examples 1-3 the gains drop out + # of both columns, so the losses netted against them go to the + # spouse with the gains. Responsibility follows the short-term and + # long-term netting of Example 2. Otherwise each spouse's losses + # netted against gains are their own. + net_capital_loss = filer * own_capital_loss + net_capital_gain = filer * own_capital_gain + losses_exceed_gains = tax_unit.sum(net_capital_loss) > tax_unit.sum( + net_capital_gain + ) + allowed_share = where( + losses_exceed_gains, + attribute(own_capital_gain, allowed_against_gains), + attribute(own_capital_loss, allowed_against_gains), + ) + responsibility = 0 + for character in CAPITAL_GAIN_CHARACTERS: + amount = filer * person(character, period) + if character == "long_term_capital_gains": + # Capital gain distributions are long-term gains. + amount = amount + filer * max_( + 0, person("non_sch_d_capital_gains", period) + ) + excess = max_(0, -tax_unit.sum(amount)) + own_loss = max_(0, -amount) + unit_loss = tax_unit.sum(own_loss) + share = np.zeros_like(unit_loss) + mask = unit_loss > 0 + share[mask] = own_loss[mask] / unit_loss[mask] + responsibility = responsibility + excess * share + # Without a short-term and long-term split, each spouse's net loss. + responsibility = where( + tax_unit.sum(responsibility) > 0, responsibility, own_capital_loss + ) + limited_share = attribute(responsibility, limited_capital_loss) + own_losses = ( + attribute(own_business_loss, business_loss) + allowed_share + limited_share + ) + # Scale to loss_ald, in case it is set directly. + parts = business_loss + allowed_against_gains + limited_capital_loss + loss_share = np.array(equal_share * loss_ald, dtype=float) + mask = parts > 0 + loss_share[mask] = own_losses[mask] * loss_ald[mask] / parts[mask] + + gross_income = person("irs_gross_income", period) + separate_agi = filer * gross_income - loss_share - adjustments + if parameters(period).gov.contrib.ubi_center.basic_income.taxable: + separate_agi = separate_agi + equal_share * tax_unit("basic_income", period) + + p = parameters(period).gov.states.mo.tax.income.federal_agi + if not p.negative_floor_applies: + return separate_agi + # 2025 MO-1040 instructions, page 6, and 12 CSR 10-2.010(5): when + # joint federal AGI is zero or less, both spouses enter $0. When it is + # positive, a spouse whose separate amount is negative enters $0 and + # the other spouse enters the whole joint amount. + joint_agi = tax_unit.sum(separate_agi) + has_negative = tax_unit.any(separate_agi < 0) + line_1 = where( + separate_agi < 0, + 0, + where(has_negative, joint_agi, separate_agi), + ) + return filer * where(joint_agi > 0, line_1, 0) diff --git a/policyengine_us/variables/gov/states/mo/tax/income/taxable_income/mo_taxable_income.py b/policyengine_us/variables/gov/states/mo/tax/income/taxable_income/mo_taxable_income.py index 7d066360e0c..cb34468f2b0 100644 --- a/policyengine_us/variables/gov/states/mo/tax/income/taxable_income/mo_taxable_income.py +++ b/policyengine_us/variables/gov/states/mo/tax/income/taxable_income/mo_taxable_income.py @@ -11,13 +11,16 @@ class mo_taxable_income(Variable): "https://dor.mo.gov/forms/MO-A_2021.pdf", "https://dor.mo.gov/forms/MO-1040%20Instructions_2021.pdf#page=8", "https://dor.mo.gov/forms/MO-1040%20Instructions_2022.pdf#page=8", + # Line 7 note on negative income. + "https://dor.mo.gov/forms/MO-1040%20Instructions_2023.pdf#page=7", "https://dor.mo.gov/forms/MO-1040%20Instructions_2024.pdf#page=8", "https://www.revisor.mo.gov/main/OneSection.aspx?section=143.111&bid=7201&hl=", ) defined_for = StateCode.MO def formula(person, period, parameters): - # calculate tax unit MO AGI + # calculate tax unit MO AGI: Form MO-1040 Line 6 adds columns 5Y and + # 5S, either of which can be negative tax_unit = person.tax_unit mo_agi = person("mo_adjusted_gross_income", period) unit_mo_agi = tax_unit.sum(mo_agi) @@ -50,10 +53,15 @@ def formula(person, period, parameters): unit_taxinc = max_(0, unit_mo_agi - unit_mo_deductions) # allocate tax unit taxable income by each individual's share of - # unit AGI (use a mask rather than where to avoid a divide-by-zero + # unit AGI (Line 7). The 2023 instructions (page 7) say: "If one + # spouse has negative income and the other spouse has positive + # income ... enter zero percent on Line 7Y and 100 percent on Line + # 7S." (use a mask rather than where to avoid a divide-by-zero # warning with default share value being zero) - ind_agi_share = np.zeros_like(unit_mo_agi) - mask = unit_mo_agi > 0 - ind_agi_share[mask] = mo_agi[mask] / unit_mo_agi[mask] + positive_mo_agi = max_(0, mo_agi) + unit_positive_mo_agi = tax_unit.sum(positive_mo_agi) + ind_agi_share = np.zeros_like(unit_positive_mo_agi) + mask = unit_positive_mo_agi > 0 + ind_agi_share[mask] = positive_mo_agi[mask] / unit_positive_mo_agi[mask] return ind_agi_share * unit_taxinc