diff --git a/changelog.d/fix-dependent-ald-filer-agi.fixed.md b/changelog.d/fix-dependent-ald-filer-agi.fixed.md new file mode 100644 index 00000000000..99fa68113f2 --- /dev/null +++ b/changelog.d/fix-dependent-ald-filer-agi.fixed.md @@ -0,0 +1 @@ +Keep tax unit dependents' above-the-line deductions (the deductible part of self-employment tax, self-employed health insurance and retirement plans, IRA contributions, early withdrawal penalties, educator expenses and alimony paid) off the filer's AGI, Social Security and unemployment MAGIs, per-person AGI and student loan interest MAGI, and off the Missouri, Massachusetts, Minnesota property tax refund and Medicaid incomes built from them. Employer adoption assistance and education savings bond interest stay on the filer's return wherever they are recorded. A dependent's own deductions now reduce the dependent's Medicaid AGI, Massachusetts gross income no longer subtracts dependents' business losses, and Mississippi subtracts each person's own self-employed health insurance and retirement plan deductions instead of the tax unit's total for every member. diff --git a/policyengine_us/parameters/gov/irs/ald/filer_amounts_recorded_on_dependents.yaml b/policyengine_us/parameters/gov/irs/ald/filer_amounts_recorded_on_dependents.yaml new file mode 100644 index 00000000000..51da800dcc9 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/ald/filer_amounts_recorded_on_dependents.yaml @@ -0,0 +1,26 @@ +description: Above-the-line amounts that belong to the filer even when they are recorded on a tax unit dependent, so they are summed over every tax unit member rather than over the head and spouse only. +values: + 2010-01-01: + # Employer adoption assistance is excluded for the adopting employee + # (26 USC 137(a)); households record it on the adopted child, as the Ohio + # adoption credit reads it. + - qualified_adoption_assistance_expense + # Savings bond interest used for education is excluded for the bond's + # owner, who must have been at least 24 when the bond was issued, and the + # expenses may be a dependent's. "A bond bought by a parent and issued in + # the name of his or her child under age 24 does not qualify for the + # exclusion by the parent or child" (Form 8815). By convention, an amount + # recorded on a dependent is the parent-owned bond's exclusion recorded on + # the student whose expenses it paid. + - us_bonds_for_higher_ed +metadata: + unit: list + period: year + label: Above-the-line amounts summed over dependents + reference: + - title: 26 U.S. Code § 137 - Adoption assistance programs + href: https://www.law.cornell.edu/uscode/text/26/137 + - title: 26 U.S. Code § 135 - Income from United States savings bonds used to pay higher education tuition and fees + href: https://www.law.cornell.edu/uscode/text/26/135 + - title: Form 8815 (2025), Who Can Take the Exclusion + href: https://www.irs.gov/pub/irs-prior/f8815--2025.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml b/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml index 71199b5dbfc..03b60f72f2e 100644 --- a/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml +++ b/policyengine_us/parameters/gov/states/al/tax/income/agi/deductions.yaml @@ -6,8 +6,11 @@ values: - alimony_expense # Line 4 - early_withdrawal_penalty # Line 5 - self_employed_health_insurance_ald # Line 7 + # Alabama AGI counts dependents' income, so their deductions too. + - dependents_self_employed_health_insurance_ald - health_savings_account_ald # Line 8 - self_employed_pension_contribution_ald # Line 9 + - dependents_self_employed_pension_contribution_ald - us_govt_interest - taxable_public_pension_income 2023-01-01: @@ -16,8 +19,11 @@ values: - alimony_expense # Line 4 - early_withdrawal_penalty # Line 5 - self_employed_health_insurance_ald # Line 7 + # Alabama AGI counts dependents' income, so their deductions too. + - dependents_self_employed_health_insurance_ald - health_savings_account_ald # Line 8 - self_employed_pension_contribution_ald # Line 9 + - dependents_self_employed_pension_contribution_ald - us_govt_interest - al_retirement_exemption #Schedule RS Line 10 - taxable_public_pension_income diff --git a/policyengine_us/parameters/gov/states/ms/tax/income/adjustments/adjustments.yaml b/policyengine_us/parameters/gov/states/ms/tax/income/adjustments/adjustments.yaml index aa7765cbb4f..6fcb76f5537 100644 --- a/policyengine_us/parameters/gov/states/ms/tax/income/adjustments/adjustments.yaml +++ b/policyengine_us/parameters/gov/states/ms/tax/income/adjustments/adjustments.yaml @@ -5,9 +5,9 @@ values: - ms_self_employment_adjustment # Line 61 - ms_national_guard_or_reserve_pay_adjustment # Line 55 - alimony_expense # Line 53 - - self_employed_pension_contribution_ald # Line 51 + - ms_self_employed_retirement_adjustment # Line 51 - early_withdrawal_penalty # Line 52 - - self_employed_health_insurance_ald # Line 56 + - ms_self_employed_health_insurance_adjustment # Line 56 - health_savings_account_ald # Line 57 - ms_retirement_income_exemption - us_govt_interest_person diff --git a/policyengine_us/tests/core/test_ald_determinism.py b/policyengine_us/tests/core/test_ald_determinism.py index 3ef4ff22cca..58269f0372b 100644 --- a/policyengine_us/tests/core/test_ald_determinism.py +++ b/policyengine_us/tests/core/test_ald_determinism.py @@ -63,6 +63,7 @@ def test_tax_unit_deductions_are_ordered_unique_and_exclude_person_alds( gross_income=SimpleNamespace(sources=[]), ald=SimpleNamespace( deductions=deductions, + filer_amounts_recorded_on_dependents=[], student_loan_interest=SimpleNamespace(magi=magi), ), ) @@ -85,13 +86,19 @@ class ReachedTaxUnitSum(Exception): person = Person() def capture_add(entity, period, variables): - if entity is person.tax_unit: - captured.extend(variables) - raise ReachedTaxUnitSum + # Tax-unit deductions must be summed over non-dependents only, through + # tax_unit_non_dep_add, never with a plain add over every member. + assert entity is person assert variables == [f"{name}_person" for name in PERSON_ALDS] return np.array([0], dtype=np.float32) + def capture_tax_unit_add(entity, period, variables, include_dependents=()): + assert entity is person.tax_unit + captured.extend(variables) + raise ReachedTaxUnitSum + monkeypatch.setattr(module, "add", capture_add) + monkeypatch.setattr(module, "tax_unit_non_dep_add", capture_tax_unit_add) with pytest.raises(ReachedTaxUnitSum): getattr(module, variable_name).formula(person, 2026, lambda period: parameters) diff --git a/policyengine_us/tests/core/test_dependent_above_the_line_deductions_invariants.py b/policyengine_us/tests/core/test_dependent_above_the_line_deductions_invariants.py new file mode 100644 index 00000000000..b985bf626f1 --- /dev/null +++ b/policyengine_us/tests/core/test_dependent_above_the_line_deductions_invariants.py @@ -0,0 +1,584 @@ +"""Invariants for dependents' above-the-line deductions in the filer's AGI. + +A tax unit dependent's deductions belong on the dependent's own return: their +Schedule SE and Schedule 1 deductions (the deductible part of self-employment +tax, self-employed health insurance and retirement plans, the IRA deduction, +the penalty on early withdrawal of savings, educator expenses, alimony paid) +are figured with the dependent's own income. `irs_gross_income` already +leaves dependents' income off the filer's return, so every sum of +above-the-line deductions over a tax unit must leave dependents out too. +Three amounts are the filer's whatever member they are recorded on, and are +summed over every member: employer adoption assistance and education savings +bond interest (`gov.irs.ald.filer_amounts_recorded_on_dependents`), and, +through 2020, a dependent's tuition in the tuition and fees deduction. + +Hypothesis draws batches of tax units (single, head of household with +dependents, joint with and without dependents, in Texas, Missouri, +Massachusetts and Minnesota), and a seeded population of 200 such units adds +breadth. Each +batch runs as one vectorized simulation, twice: with +the dependents' deduction inputs as drawn and with them set to zero. For +every tax unit: + +1. The dependents' deduction inputs never change the filer's AGI, + above-the-line deductions, any tax-unit deduction aggregate, the Social + Security and unemployment compensation MAGIs, taxable Social Security, + Massachusetts gross income and Part B AGI, Minnesota property tax refund + household income, or the head's and spouse's per-person AGI, student loan + interest MAGI and deduction, Medicaid AGI and Missouri AGI. +2. Accounting identities: the per-person AGIs of the members sum to the tax + unit's AGI, and so do the head's and spouse's Medicaid AGIs. +3. Differential: `above_the_line_deductions` and each aggregate equal an + independent numpy sum over the head and spouse (plus the three filer + amounts over every member). For units without + dependents this is also the previous all-member sum, so they see no change; + for others it is between 0 and that sum. +4. Each person's own deduction amounts are still computed for dependents + (half of each person's self-employment tax, for example), so a + dependent's own return can use them, and a dependent's Medicaid AGI is + their own gross income less their own person-level deductions. +""" + +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.01 # dollars + +# Person-level inputs behind the deductions a dependent could claim on their +# own return. +DEPENDENT_DEDUCTION_INPUTS = [ + "self_employment_income", + "self_employed_health_insurance_premiums", + "self_employed_pension_contributions", + "traditional_ira_contributions", + "early_withdrawal_penalty", + "educator_expense", + "alimony_expense", + "student_loan_interest", +] +# Person-level deduction amounts in above_the_line_deductions that belong on +# each person's own return. +PERSON_DEDUCTIONS = [ + "self_employment_tax_ald_person", + "self_employed_health_insurance_ald_person", + "self_employed_pension_contribution_ald_person", + "alimony_expense_ald_person", + "student_loan_interest_ald", + "early_withdrawal_penalty", + "educator_expense", + "traditional_ira_contributions", +] +# Person-level amounts that are the filer's wherever they are recorded. +FILER_AMOUNTS = [ + "qualified_adoption_assistance_expense", + "us_bonds_for_higher_ed", +] +AGGREGATES = { + "self_employment_tax_ald": "self_employment_tax_ald_person", + "self_employed_health_insurance_ald": "self_employed_health_insurance_ald_person", + "self_employed_pension_contribution_ald": "self_employed_pension_contribution_ald_person", + "alimony_expense_ald": "alimony_expense_ald_person", +} +TAX_UNIT_OUTPUTS = [ + "adjusted_gross_income", + "above_the_line_deductions", + *AGGREGATES, + "loss_ald", + "capped_qualified_tuition_expenses_ald", + "taxable_ss_magi", + "taxable_uc_agi", + "tax_unit_taxable_social_security", + "ma_gross_income", + "ma_part_b_agi", + "mn_homestead_credit_refund_household_income", +] +# Per-person outputs whose head and spouse values must not depend on the +# dependents' deductions. +FILER_PERSON_OUTPUTS = [ + "adjusted_gross_income_person", + "student_loan_interest_ald_magi", + "student_loan_interest_ald", + "medicaid_adjusted_gross_income_person", + "mo_adjusted_gross_income", +] +PERSON_OUTPUTS = [ + *FILER_PERSON_OUTPUTS, + *PERSON_DEDUCTIONS, + *FILER_AMOUNTS, + "self_employment_tax", + "alimony_expense", + "divorce_year", + "medicaid_irs_gross_income", + "irs_gross_income", +] + +amount = st.integers(0, 40_000).map(float) +small = st.one_of(st.just(0.0), st.integers(1, 7_000).map(float)) + + +@st.composite +def person_amounts(draw, *, dependent): + has = st.booleans() + return { + "employment_income": draw(st.one_of(st.just(0.0), amount)), + # Losses are loss_ald's business; draw mostly gains with some losses. + "self_employment_income": ( + draw(st.integers(-10_000, 60_000).map(float)) if draw(has) else 0.0 + ), + "self_employed_health_insurance_premiums": draw(small), + "self_employed_pension_contributions": draw(small), + "traditional_ira_contributions": draw(small), + "early_withdrawal_penalty": draw(st.integers(0, 500).map(float)), + "educator_expense": draw(st.integers(0, 300).map(float)), + "alimony_expense": draw(small), + "divorce_year": draw(st.sampled_from([2010, 2018, 2020])), + "qualified_adoption_assistance_expense": draw(small), + "us_bonds_for_higher_ed": draw(small), + "student_loan_interest": draw(st.integers(0, 3_000).map(float)), + "qualified_tuition_expenses": draw(small), + "taxable_interest_income": draw(st.integers(0, 2_000).map(float)), + "social_security_retirement": ( + 0.0 if dependent else draw(st.one_of(st.just(0.0), amount)) + ), + "taxable_private_pension_income": ( + 0.0 if dependent else draw(st.one_of(st.just(0.0), amount)) + ), + "unemployment_compensation": draw( + st.one_of(st.just(0.0), st.integers(1, 15_000).map(float)) + ), + } + + +@st.composite +def tax_units(draw): + kind = draw(st.sampled_from(["single", "hoh", "joint", "joint_dependents"])) + n_dependents = draw(st.integers(1, 2)) if kind in ("hoh", "joint_dependents") else 0 + return { + "state": draw(st.sampled_from(["TX", "MO", "MA", "MN"])), + "head": draw(person_amounts(dependent=False)), + "spouse": ( + draw(person_amounts(dependent=False)) if kind.startswith("joint") else None + ), + "dependents": [ + { + "age": draw(st.integers(5, 23)), + **draw(person_amounts(dependent=True)), + } + for _ in range(n_dependents) + ], + } + + +SEED = 20261005 + + +def _seeded_amounts(rng, *, dependent): + def some(high, p=0.5): + return float(round(rng.uniform(1, high))) if rng.random() < p else 0.0 + + return { + "employment_income": some(40_000, 0.6), + "self_employment_income": ( + float(round(rng.uniform(-10_000, 60_000))) if rng.random() < 0.6 else 0.0 + ), + "self_employed_health_insurance_premiums": some(7_000), + "self_employed_pension_contributions": some(7_000), + "traditional_ira_contributions": some(7_000), + "early_withdrawal_penalty": some(500), + "educator_expense": some(300), + "alimony_expense": some(7_000, 0.3), + "divorce_year": int(rng.choice([2010, 2018, 2020])), + "qualified_adoption_assistance_expense": some(7_000, 0.2), + "us_bonds_for_higher_ed": some(7_000, 0.2), + "student_loan_interest": some(3_000), + "qualified_tuition_expenses": some(7_000, 0.3), + "taxable_interest_income": some(2_000), + "social_security_retirement": 0.0 if dependent else some(40_000, 0.3), + "taxable_private_pension_income": 0.0 if dependent else some(40_000, 0.3), + "unemployment_compensation": some(15_000, 0.2), + } + + +def _seeded_units(n=200): + rng = np.random.default_rng(SEED) + units = [] + for _ in range(n): + kind = rng.choice(["single", "hoh", "joint", "joint_dependents"]) + n_dependents = ( + int(rng.integers(1, 3)) if kind in ("hoh", "joint_dependents") else 0 + ) + units.append( + { + "state": str(rng.choice(["TX", "MO", "MA", "MN"])), + "head": _seeded_amounts(rng, dependent=False), + "spouse": ( + _seeded_amounts(rng, dependent=False) + if kind.startswith("joint") + else None + ), + "dependents": [ + { + "age": int(rng.integers(5, 24)), + **_seeded_amounts(rng, dependent=True), + } + for _ in range(n_dependents) + ], + } + ) + return units + + +def _situation(units, year, *, zero_dependents): + people, groups = {}, {"tax_units": {}, "households": {}, "marital_units": {}} + for i, u in enumerate(units): + head = f"head_{i}" + people[head] = { + "age": 50, + "is_tax_unit_head": True, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": False, + **u["head"], + } + members, couple = [head], [head] + if u["spouse"] is not None: + spouse = f"spouse_{i}" + people[spouse] = { + "age": 48, + "is_tax_unit_head": False, + "is_tax_unit_spouse": True, + "is_tax_unit_dependent": False, + **u["spouse"], + } + members.append(spouse) + couple.append(spouse) + groups["marital_units"][f"couple_{i}"] = {"members": couple} + for j, d in enumerate(u["dependents"]): + child = f"dependent_{i}_{j}" + amounts = {k: v for k, v in d.items() if k != "age"} + if zero_dependents: + amounts.update({name: 0.0 for name in DEPENDENT_DEDUCTION_INPUTS}) + people[child] = { + "age": d["age"], + "is_full_time_student": d["age"] >= 19, + "is_tax_unit_head": False, + "is_tax_unit_spouse": False, + "is_tax_unit_dependent": True, + **amounts, + } + members.append(child) + groups["marital_units"][f"single_{i}_{j}"] = {"members": [child]} + groups["tax_units"][f"tax_unit_{i}"] = {"members": members} + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {year: u["state"]}, + } + people = { + name: {k: {year: v} for k, v in values.items()} + for name, values in people.items() + } + return {"people": people, **groups} + + +def _run(units, year, *, zero_dependents): + sim = Simulation(situation=_situation(units, year, zero_dependents=zero_dependents)) + out = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in TAX_UNIT_OUTPUTS + PERSON_OUTPUTS + } + out["filing_status"] = np.asarray(sim.calculate("filing_status", year)) + out["is_dependent"] = np.asarray( + sim.calculate("is_tax_unit_dependent", year), dtype=bool + ) + out["unit"] = sim.populations["tax_unit"].members_entity_id + return out + + +def _unit_sum(run, values): + return np.bincount( + run["unit"], weights=values, minlength=len(run["adjusted_gross_income"]) + ) + + +def _check(units, year): + run = _run(units, year, zero_dependents=False) + zeroed = _run(units, year, zero_dependents=True) + filer = ~run["is_dependent"] + has_dependent = _unit_sum(run, run["is_dependent"].astype(float)) > 0 + + # 1. Dependents' deduction inputs never reach the filer's return. + assert (run["filing_status"] == zeroed["filing_status"]).all() + for name in TAX_UNIT_OUTPUTS: + np.testing.assert_allclose( + run[name], zeroed[name], atol=TOLERANCE, err_msg=name + ) + for name in FILER_PERSON_OUTPUTS: + np.testing.assert_allclose( + run[name][filer], zeroed[name][filer], atol=TOLERANCE, err_msg=name + ) + + # 2. Accounting identities. + np.testing.assert_allclose( + _unit_sum(run, run["adjusted_gross_income_person"]), + run["adjusted_gross_income"], + atol=TOLERANCE, + ) + np.testing.assert_allclose( + _unit_sum(run, filer * run["medicaid_adjusted_gross_income_person"]), + run["adjusted_gross_income"], + atol=TOLERANCE, + ) + + # 3. Differential against numpy sums over the head and spouse. Alimony + # is deductible for divorces before 2019 (26 USC 215, repealed by Pub. L. + # 115-97 sec. 11051 for later instruments). + np.testing.assert_allclose( + run["alimony_expense_ald_person"], + run["alimony_expense"] * (run["divorce_year"] < 2019), + atol=TOLERANCE, + ) + for aggregate, person_amount in AGGREGATES.items(): + np.testing.assert_allclose( + run[aggregate], + _unit_sum(run, filer * run[person_amount]), + atol=TOLERANCE, + err_msg=aggregate, + ) + person_total = sum(run[name] for name in PERSON_DEDUCTIONS) + filer_amounts = sum(run[name] for name in FILER_AMOUNTS) + unit_level = run["loss_ald"] + run["capped_qualified_tuition_expenses_ald"] + reference = unit_level + _unit_sum(run, filer * person_total + filer_amounts) + np.testing.assert_allclose( + run["above_the_line_deductions"], reference, atol=TOLERANCE + ) + # Units without dependents: the previous all-member sum, unchanged. + # Units with dependents: never more than it, and never negative. + all_members = unit_level + _unit_sum(run, person_total + filer_amounts) + assert (run["above_the_line_deductions"] >= -TOLERANCE).all() + assert (run["above_the_line_deductions"] <= all_members + TOLERANCE).all() + np.testing.assert_allclose( + run["above_the_line_deductions"][~has_dependent], + all_members[~has_dependent], + atol=TOLERANCE, + ) + np.testing.assert_allclose( + run["adjusted_gross_income"], + _unit_sum(run, run["irs_gross_income"]) - reference, + atol=TOLERANCE, + ) + + # 4. Each person's own amounts survive for their own return. + np.testing.assert_allclose( + run["self_employment_tax_ald_person"], + 0.5 * run["self_employment_tax"], + atol=TOLERANCE, + ) + dependent = run["is_dependent"] + own_person_deductions = person_total - run["student_loan_interest_ald"] + np.testing.assert_allclose( + run["medicaid_adjusted_gross_income_person"][dependent], + (run["medicaid_irs_gross_income"] - own_person_deductions)[dependent], + atol=TOLERANCE, + ) + assert (run["student_loan_interest_ald"][dependent] == 0).all() + assert (run["adjusted_gross_income_person"][dependent] == 0).all() + + +# A batch's cost is mostly per-variable overhead, so each example is a large +# batch and there are few examples. +SETTINGS = dict( + max_examples=5, + 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)) +def test_dependent_deductions_stay_off_the_filers_return_2025(units): + _check(units, 2025) + + +@settings(**SETTINGS) +@given(st.lists(tax_units(), min_size=10, max_size=40)) +def test_dependent_deductions_stay_off_the_filers_return_2020(units): + # 2020 adds the unemployment compensation exclusion, which reads + # taxable_uc_agi, and the tuition and fees deduction. + _check(units, 2020) + + +@pytest.mark.parametrize("year", [2020, 2025]) +def test_seeded_population(year): + _check(_seeded_units(), year) + + +def _single_filer(year, state, **inputs): + return { + "people": { + "filer": { + "age": {year: 45}, + "employment_income": {year: 50_000}, + "is_tax_unit_head": {year: True}, + } + }, + "tax_units": { + "tax_unit": { + "members": ["filer"], + **{name: {year: value} for name, value in inputs.items()}, + } + }, + "households": { + "household": {"members": ["filer"], "state_code": {year: state}} + }, + } + + +def test_person_splits_honor_tax_unit_inputs(): + # A tax-unit deduction supplied as an input still reaches the per-person + # Medicaid AGI and the Mississippi adjustments, attributed to the head. + sim = Simulation( + situation=_single_filer( + 2025, + "MS", + alimony_expense_ald=3_000, + self_employed_health_insurance_ald=200, + self_employed_pension_contribution_ald=100, + ) + ) + agi = sim.calculate("adjusted_gross_income", 2025) + assert np.allclose(agi, 50_000 - 3_000 - 200 - 100) + assert np.allclose( + sim.calculate("medicaid_adjusted_gross_income_person", 2025), agi + ) + assert np.allclose( + sim.calculate("ms_self_employed_health_insurance_adjustment", 2025), 200 + ) + assert np.allclose( + sim.calculate("ms_self_employed_retirement_adjustment", 2025), 100 + ) + + +def test_person_splits_follow_the_deduction_list(): + # Removing alimony from gov.irs.ald.deductions removes it from both the + # federal AGI and the Medicaid AGI built per person. + from policyengine_core.periods import instant + from policyengine_core.reforms import Reform + + class drop_alimony_deduction(Reform): + def apply(self): + def modify(parameters): + node = parameters.gov.irs.ald.deductions + start = instant("2025-01-01") + kept = [d for d in node(start) if d != "alimony_expense_ald"] + node.update(start=start, stop=instant("2025-12-31"), value=kept) + return parameters + + self.modify_parameters(modify) + + situation = _single_filer(2025, "TX") + situation["people"]["filer"].update( + alimony_expense={2025: 1_000}, divorce_year={2025: 2010} + ) + baseline = Simulation(situation=situation) + assert np.allclose(baseline.calculate("adjusted_gross_income", 2025), 49_000) + assert np.allclose( + baseline.calculate("medicaid_adjusted_gross_income_person", 2025), 49_000 + ) + reformed = Simulation(situation=situation, reform=drop_alimony_deduction) + assert np.allclose(reformed.calculate("adjusted_gross_income", 2025), 50_000) + assert np.allclose( + reformed.calculate("medicaid_adjusted_gross_income_person", 2025), 50_000 + ) + + +def _ms_couple(premiums, **tax_unit_inputs): + year = 2025 + return { + "people": { + "head": { + "age": {year: 45}, + "is_tax_unit_head": {year: True}, + "employment_income": {year: 1_000}, + }, + "spouse": { + "age": {year: 43}, + "is_tax_unit_spouse": {year: True}, + "self_employment_income": {year: 1_000}, + "self_employed_health_insurance_premiums": {year: premiums}, + }, + }, + "tax_units": { + "tax_unit": { + "members": ["head", "spouse"], + **{name: {year: value} for name, value in tax_unit_inputs.items()}, + } + }, + "marital_units": {"couple": {"members": ["head", "spouse"]}}, + "households": { + "household": {"members": ["head", "spouse"], "state_code": {year: "MS"}} + }, + } + + +SPLIT_OUTPUTS = [ + "ms_self_employed_health_insurance_adjustment", + "ms_agi", + "medicaid_adjusted_gross_income_person", + "medicaid_magi_person", + "medicaid_household_income", + "adjusted_gross_income", +] + + +def _split_outputs(sim): + return {name: np.asarray(sim.calculate(name, 2025)) for name in SPLIT_OUTPUTS} + + +@pytest.mark.parametrize("aggregate", [0, 500]) +def test_reduced_tax_unit_amount_is_shared_without_negative_shares(aggregate): + # A tax-unit deduction set below the spouse's own amount is shared in + # proportion to the filers' own amounts, so no share turns negative and + # the per-person floors in Mississippi AGI and Medicaid MAGI cannot add + # income. The result equals a couple whose own amount is that deduction. + reduced = Simulation( + situation=_ms_couple(1_000, self_employed_health_insurance_ald=aggregate) + ) + control = Simulation(situation=_ms_couple(aggregate)) + shares = reduced.calculate("ms_self_employed_health_insurance_adjustment", 2025) + assert (np.asarray(shares) >= 0).all() + assert np.allclose(shares, [0, aggregate]) + for name, values in _split_outputs(control).items(): + np.testing.assert_allclose( + _split_outputs(reduced)[name], values, atol=TOLERANCE, err_msg=name + ) + + +def test_reformed_tax_unit_amount_is_shared_like_an_input(): + from policyengine_core.reforms import Reform + + from policyengine_us.model_api import TaxUnit, Variable + + class self_employed_health_insurance_ald(Variable): + value_type = float + entity = TaxUnit + definition_period = "year" + + def formula(tax_unit, period, parameters): + return tax_unit.filled_array(0) + + class repeal_self_employed_health_insurance(Reform): + def apply(self): + self.update_variable(self_employed_health_insurance_ald) + + reformed = Simulation( + situation=_ms_couple(1_000), reform=repeal_self_employed_health_insurance + ) + control = Simulation(situation=_ms_couple(0)) + for name, values in _split_outputs(control).items(): + np.testing.assert_allclose( + _split_outputs(reformed)[name], values, atol=TOLERANCE, err_msg=name + ) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_adjusted_gross_income_person.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_adjusted_gross_income_person.yaml index dd5d926b7f3..b35bcc9e710 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_adjusted_gross_income_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_adjusted_gross_income_person.yaml @@ -38,3 +38,88 @@ output: medicaid_adjusted_gross_income_person: [59_450, 39_450, 5_000] absolute_error_margin: 0.01 + +- name: Case 3, a dependent's own deductions reduce the dependent's AGI, not the head's. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_tax_unit_head: true + employment_income: 50_000 + student: + age: 23 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 6_000 + taxable_interest_income: 500 + traditional_ira_contributions: 2_000 + early_withdrawal_penalty: 100 + educator_expense: 300 + tax_units: + tax_unit: + members: [parent, student] + households: + household: + members: [parent, student] + state_code: TX + output: + # Each person's own Form 1040, line 11. Parent: 50,000 - 0. Student: + # line 9 = 6,000 + 500 = 6,500; Schedule 1, line 11 = 300, line 18 = 100, + # line 20 = 2,000, line 26 = 2,400; line 11 = 4,100. + medicaid_adjusted_gross_income_person: [50_000, 4_100] + +- name: Case 4, a dependent's pre-2019 alimony reduces the dependent's own AGI. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + filer: + age: 45 + is_tax_unit_head: true + employment_income: 50_000 + mother: + age: 72 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + taxable_private_pension_income: 4_000 + alimony_expense: 3_000 + divorce_year: 2010 + tax_units: + tax_unit: + members: [filer, mother] + households: + household: + members: [filer, mother] + state_code: TX + output: + # Each person's own Form 1040 line 11. Filer: 50,000. Mother: pension + # 4,000 less alimony paid under a 2010 divorce (Schedule 1, line 19a) + # 3,000 = 1,000. + alimony_expense_ald_person: [0, 3_000] + medicaid_adjusted_gross_income_person: [50_000, 1_000] + +- name: Case 5, a tax-unit alimony deduction supplied as an input goes to the head. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + filer: + age: 45 + is_tax_unit_head: true + employment_income: 50_000 + tax_units: + tax_unit: + members: [filer] + alimony_expense_ald: 3_000 + households: + household: + members: [filer] + state_code: TX + output: + adjusted_gross_income: 47_000 + medicaid_adjusted_gross_income_person: 47_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/dependent_above_the_line_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/dependent_above_the_line_deductions.yaml new file mode 100644 index 00000000000..7ca510b8424 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/dependent_above_the_line_deductions.yaml @@ -0,0 +1,337 @@ +# A tax unit dependent's above-the-line deductions belong on the dependent's +# own return, not the filer's. irs_gross_income already leaves the +# dependent's income off the filer's return; these tests check that the +# dependent's deductions are left off too. Only spouses aggregate on one +# return (26 USC 6013(a)); 26 USC 1(g)(7) moves only a child's interest and +# dividend gross income to a parent's return. Even under that election, the +# Instructions for Form 8814 (2025), p. 2, list "Penalty on early withdrawal +# of child's savings" among the deductions the parent cannot take. +# +# Expected values follow the TY2025 forms, figured in cents: +# - Schedule SE (Form 1040) 2025, lines 2-13, filed by each person with +# self-employment income: +# https://www.irs.gov/pub/irs-prior/f1040sse--2025.pdf +# - Schedule 1 (Form 1040) 2025, Part II, lines 11-26 (adjustments): +# https://www.irs.gov/pub/irs-prior/f1040s1--2025.pdf +# - Form 1040 (2025) lines 9-11, and the Social Security Benefits Worksheet +# and Student Loan Interest Deduction Worksheet in the 2025 Form 1040 +# instructions: https://www.irs.gov/pub/irs-prior/i1040gi--2025.pdf#page=32 +# and #page=99. +# The model does not round Schedule SE lines to cents, so the margin is 0.01. +# +# Three amounts stay on the filer's return whatever member they are recorded +# on (the last three cases): a dependent's tuition in the tuition and fees +# deduction (Form 8917, through 2020), employer adoption assistance recorded on +# the adopted child, and education savings bond interest recorded on the +# student (gov.irs.ald.filer_amounts_recorded_on_dependents). + +- name: Child's Schedule C income and self-employment tax stay off the head of household parent's return + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 40_000 + child: + age: 17 + self_employment_income: 10_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + is_tax_unit_dependent: [false, true] + # Child's own Schedule SE: line 2 = line 3 = 10,000; line 4a = line 6 = + # 10,000 x 0.9235 = 9,235.00; line 9 = 176,100 (no wages); line 10 = + # 9,235 x 0.124 = 1,145.14; line 11 = 9,235 x 0.029 = 267.82; + # line 12 = 1,412.96; line 13 = 1,412.96 x 0.50 = 706.48. Line 13 goes on + # the child's own Schedule 1, line 15. + self_employment_tax: [0, 1_412.96] + self_employment_tax_ald_person: [0, 706.48] + # Parent's Schedule 1, line 15: no self-employment tax of the parent's. + self_employment_tax_ald: 0 + # Parent's Schedule 1, line 26, and Form 1040, line 10. + above_the_line_deductions: 0 + # Parent's Form 1040: line 1a = line 9 = 40,000; line 11 = 40,000 - 0. + adjusted_gross_income: 40_000 + adjusted_gross_income_person: [40_000, 0] + +- name: Spouse's self-employment deductions stay on the joint return and the child's do not + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + employment_income: 60_000 + spouse: + age: 44 + self_employment_income: 20_000 + child: + age: 16 + self_employment_income: 8_000 + self_employed_health_insurance_premiums: 1_000 + self_employed_pension_contributions: 500 + tax_units: + tax_unit: + members: [head, spouse, child] + households: + household: + members: [head, spouse, child] + state_code: TX + output: + filing_status: JOINT + is_tax_unit_dependent: [false, false, true] + # Spouse's Schedule SE: line 2 = 20,000; line 4a = line 6 = 18,470.00; + # line 10 = 18,470 x 0.124 = 2,290.28; line 11 = 18,470 x 0.029 = + # 535.63; line 12 = 2,825.91; line 13 = 1,412.96 (1,412.955 unrounded). + # Child's own Schedule SE: line 2 = 8,000; line 4a = line 6 = 7,388.00; + # line 10 = 916.11; line 11 = 214.25; line 12 = 1,130.36; + # line 13 = 565.18. The child's line 13, health insurance (1,000) and + # SEP contribution (500) go on the child's own Schedule 1, lines 15-17. + self_employment_tax: [0, 2_825.91, 1_130.36] + self_employment_tax_ald_person: [0, 1_412.96, 565.18] + self_employed_health_insurance_ald_person: [0, 0, 1_000] + self_employed_pension_contribution_ald_person: [0, 0, 500] + # Joint return's Schedule 1: line 15 = the spouse's 1,412.96 only; + # lines 16 and 17 = 0; line 26 = 1,412.96. + self_employment_tax_ald: 1_412.96 + self_employed_pension_contribution_ald: 0 + self_employed_health_insurance_ald: 0 + above_the_line_deductions: 1_412.96 + # Joint Form 1040: line 1a = 60,000; Schedule 1, line 3 = 20,000; line 9 + # = 80,000; line 10 = 1,412.96; line 11 = 78,587.04. + adjusted_gross_income: 78_587.04 + adjusted_gross_income_person: [60_000, 18_587.04, 0] + +- name: Student dependent's IRA deduction, early withdrawal penalty and educator expenses stay off the parent's return + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_tax_unit_head: true + employment_income: 50_000 + student: + age: 23 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 6_000 + taxable_interest_income: 500 + traditional_ira_contributions: 2_000 + early_withdrawal_penalty: 100 + educator_expense: 300 + tax_units: + tax_unit: + members: [parent, student] + households: + household: + members: [parent, student] + state_code: TX + output: + filing_status: HEAD_OF_HOUSEHOLD + is_tax_unit_dependent: [false, true] + # The student's own Schedule 1: line 11 = 300, line 18 = 100, line 20 = + # 2,000, line 26 = 2,400. The parent's Schedule 1, line 26 = 0. + above_the_line_deductions: 0 + # Parent's Form 1040: line 9 = 50,000; line 10 = 0; line 11 = 50,000. + adjusted_gross_income: 50_000 + adjusted_gross_income_person: [50_000, 0] + +- name: Dependent parent's alimony paid stays off the filer's return + period: 2025 + absolute_error_margin: 0.01 + input: + people: + filer: + age: 45 + is_tax_unit_head: true + employment_income: 50_000 + mother: + age: 72 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + alimony_expense: 3_000 + divorce_year: 2010 + tax_units: + tax_unit: + members: [filer, mother] + households: + household: + members: [filer, mother] + state_code: TX + output: + filing_status: HEAD_OF_HOUSEHOLD + # The mother's pre-2019 alimony goes on her own Schedule 1, line 19a. + alimony_expense_ald: 0 + above_the_line_deductions: 0 + adjusted_gross_income: 50_000 + +- name: Grandchild's self-employment tax does not lower the grandparent's taxable Social Security + period: 2025 + absolute_error_margin: 0.01 + input: + people: + grandparent: + age: 67 + social_security_retirement: 24_000 + taxable_private_pension_income: 20_000 + grandchild: + age: 17 + self_employment_income: 10_000 + tax_units: + tax_unit: + members: [grandparent, grandchild] + households: + household: + members: [grandparent, grandchild] + state_code: TX + output: + filing_status: HEAD_OF_HOUSEHOLD + # Social Security Benefits Worksheet (2025): line 1 = 24,000; line 2 = + # 12,000; line 3 = Form 1040 line 5b = 20,000; line 4 = 0; line 5 = + # 32,000; line 6 = the grandparent's Schedule 1, lines 11-20, 23, 25 = 0 + # (the grandchild's line 15 deduction is on the grandchild's return). + # Line 3 + line 4 - line 6: + taxable_ss_magi: 20_000 + # Line 7 = 32,000; line 8 = 25,000; line 9 = 7,000; line 10 = 9,000; + # line 11 = 0; line 12 = 7,000; line 13 = 3,500; line 14 = 3,500; + # line 15 = 0; line 16 = 3,500; line 17 = 20,400; line 18 = 3,500. + tax_unit_taxable_social_security: 3_500 + # Form 1040: line 9 = 20,000 + 3,500 = 23,500; line 10 = 0; line 11. + adjusted_gross_income: 23_500 + +- name: Dependent's IRA deduction does not lower the parent's student loan interest MAGI + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + employment_income: 91_000 + student_loan_interest: 2_500 + student: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 6_000 + traditional_ira_contributions: 2_000 + tax_units: + tax_unit: + members: [parent, student] + households: + household: + members: [parent, student] + state_code: TX + output: + filing_status: HEAD_OF_HOUSEHOLD + # Student Loan Interest Deduction Worksheet (2025): line 1 = 2,500; + # line 2 = Form 1040 line 9 = 91,000; line 3 = the parent's Schedule 1, + # lines 11-20, 23, 25 = 0; line 4 = 91,000. + student_loan_interest_ald_magi: [91_000, 0] + # Line 5 = 85,000; line 6 = 6,000; line 7 = 6,000 / 15,000 = 0.400; + # line 8 = 1,000; line 9 = 1,500 (Schedule 1, line 21). + student_loan_interest_ald: [1_500, 0] + # Form 1040: line 9 = 91,000; line 10 = 1,500; line 11 = 89,500. + adjusted_gross_income: 89_500 + +- name: A dependent student's tuition stays in the parent's tuition and fees deduction + period: 2020 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_tax_unit_head: true + employment_income: 50_000 + student: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + qualified_tuition_expenses: 3_000 + tax_units: + tax_unit: + members: [parent, student] + households: + household: + members: [parent, student] + state_code: TX + output: + # Form 8917 (Rev. January 2020), on the parent's return, which claims the + # student as a dependent: line 1(c) = line 2 = 3,000; line 3 = 50,000; + # line 4 = 0; line 5 = 50,000, not more than 65,000, so line 6 = the + # smaller of 3,000 or 4,000 = 3,000. + capped_qualified_tuition_expenses_ald: 3_000 + above_the_line_deductions: 3_000 + adjusted_gross_income: 47_000 + +- name: Employer adoption assistance recorded on the adopted child stays on the parent's return + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + employment_income: 50_000 + child: + age: 3 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + qualified_adoption_assistance_expense: 5_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + # The 26 USC 137(a) exclusion is the adopting employee's. + above_the_line_deductions: 5_000 + adjusted_gross_income: 45_000 + +- name: Education savings bond interest recorded on the student stays on the parent's return + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_tax_unit_head: true + employment_income: 50_000 + taxable_interest_income: 2_000 + student: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + us_bonds_for_higher_ed: 1_000 + tax_units: + tax_unit: + members: [parent, student] + households: + household: + members: [parent, student] + state_code: TX + output: + # A bond issued in the name of a child under 24 does not qualify (Form + # 8815, 2025, p. 3), so the exclusion is the parent's: 52,000 - 1,000. + above_the_line_deductions: 1_000 + adjusted_gross_income: 51_000 + diff --git a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml index 7aa7440aa51..d1b72912602 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/al/tax/income/al_agi.yaml @@ -40,3 +40,31 @@ output: al_agi: 60_000 al_income_tax: 2_442.75 + +- name: A dependent's self-employed health insurance and plan deductions follow their income into Alabama AGI + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 40_000 + child: + age: 17 + self_employment_income: 10_000 + self_employed_health_insurance_premiums: 1_000 + self_employed_pension_contributions: 500 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: AL + output: + # Alabama AGI sums every member's income, so it counts the child's 10,000 + # of Schedule C income and subtracts the child's own self-employed health + # insurance (1,000) and SEP (500) deductions: 50,000 - 1,500. The federal + # AGI leaves all of them on the child's return. + adjusted_gross_income: 40_000 + al_agi: 48_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.yaml index 838ec7604af..e350f4cb091 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.yaml @@ -7,3 +7,29 @@ traditional_ira_contributions_desired: 1_000 output: ma_part_b_agi: 10_000 + +- name: Dependent's self-employed health insurance deduction does not reduce the parent's Part B AGI + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 40_000 + child: + age: 17 + self_employment_income: 10_000 + self_employed_health_insurance_premiums: 1_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: MA + output: + # Part B gross income starts from federal gross income, which leaves out + # the child's Schedule C income, so the child's self-employed health + # insurance deduction is left out too: it is on the child's own return. + ma_part_b_gross_income: 40_000 + ma_part_b_agi: 40_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/ma_gross_income_loss_adjustment.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/ma_gross_income_loss_adjustment.yaml index f217778eca9..29b26502099 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/ma_gross_income_loss_adjustment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/ma_gross_income_loss_adjustment.yaml @@ -5,3 +5,29 @@ state_code: MA output: ma_gross_income_loss_adjustment: -10_000 + +- name: A dependent's Schedule C loss stays on the dependent's own Form 1 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 40_000 + rental_income: 2_000 + child: + age: 17 + self_employment_income: -3_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: MA + output: + # Federal gross income leaves the child out, so the child's loss is not + # subtracted on the parent's line 6a: MA total 5.0% income is the + # parent's wages (line 3) plus rental income (line 7). + ma_gross_income_loss_adjustment: 0 + ma_gross_income: 42_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund.yaml index f1f3015d458..eeed87ba1f7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund.yaml @@ -156,3 +156,38 @@ traditional_ira_contributions: 3_500 roth_ira_contributions: 3_500 mn_homestead_credit_refund_household_income: 46_500 + +- name: Case 8, a dependent's IRA contribution is not added back to the parent's household income. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + employment_income: 60_000 + traditional_401k_contributions: 7_000 + student: + age: 20 + is_tax_unit_head: false + is_tax_unit_spouse: false + is_tax_unit_dependent: true + is_full_time_student: true + employment_income: 6_000 + traditional_ira_contributions: 2_000 + tax_units: + tax_unit: + members: [parent, student] + households: + household: + members: [parent, student] + state_code: MN + output: + # Federal AGI is the parent's 60,000 - 7,000 of 401(k) deferrals; the + # student's wages and IRA deduction are on the student's own return. + adjusted_gross_income: 53_000 + # 53,000 + the parent's 401(k) addition (7,000) - the retirement + # subtraction (7,000) - the dependent subtraction (7,280). The student's + # IRA contribution is neither added back nor subtracted, as their wages + # are not in AGI. + mn_homestead_credit_refund_household_income: 45_720 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 5a918062d68..4a0f71ad81f 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 @@ -47,3 +47,31 @@ state_code: MO output: mo_adjusted_gross_income: 0 # AGI cannot be negative + +- name: Dependent's self-employment tax deduction neither reduces nor raises the parent's Missouri AGI + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 40_000 + child: + age: 17 + self_employment_income: 10_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: MO + output: + # The child's Schedule SE line 13 (706.48) is on the child's own federal + # and Missouri returns. The parent's federal AGI (Form 1040, line 11) is + # 40,000 and has no Missouri additions or subtractions. The parent's + # personal deductions and the tax unit's deductions both leave the child + # out, so the remaining deductions allocated between spouses are 0, not + # -706.48. + adjusted_gross_income: 40_000 + mo_adjusted_gross_income: [40_000, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/ms_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/ms_agi.yaml index 688e81a7fca..7b0434e0af8 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/ms_agi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ms/tax/income/ms_agi.yaml @@ -41,3 +41,56 @@ state_code: MS output: ms_agi: 100_000 + +- name: A dependent's self-employment adjustments are subtracted once, with the dependent's income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 40_000 + child: + age: 17 + self_employment_income: 10_000 + self_employed_health_insurance_premiums: 1_000 + self_employed_pension_contributions: 500 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: MS + output: + # The child's own adjustments: one-half of self-employment tax (Schedule + # SE line 13, 706.48), health insurance (1,000) and SEP (500), 2,206.48. + ms_agi_adjustments: [0, 2_206.48] + # The child's income and adjustments reach the parent's Mississippi AGI + # once: 40,000 + 10,000 - 2,206.48. + ms_agi: [47_793.52, 0] + +- name: Each spouse subtracts only their own self-employed health insurance + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 45 + self_employment_income: 30_000 + self_employed_health_insurance_premiums: 3_000 + spouse: + age: 44 + employment_income: 20_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: MS + output: + # Head: one-half of self-employment tax (30,000 x 0.9235 x 0.153 / 2 = + # 2,119.43) plus health insurance 3,000. The spouse has no adjustment. + ms_agi_adjustments: [5_119.43, 0] + ms_agi: [24_880.57, 20_000] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml index 1888f420be9..ddcd2cb333c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml @@ -30,3 +30,28 @@ state_code: NJ output: nj_medical_expense_deduction: 2_600 + +- name: A dependent's self-employed health insurance counts where New Jersey counts the dependent's income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 40_000 + child: + age: 17 + self_employment_income: 10_000 + self_employed_health_insurance_premiums: 1_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NJ + output: + # New Jersey AGI includes the child's 10,000 of Schedule C income, so the + # child's 1,000 of self-employed health insurance is deducted too. + nj_agi: 50_000 + nj_medical_expense_deduction: 1_000 diff --git a/policyengine_us/tests/policy/contrib/taxsim/taxsim_v10.yaml b/policyengine_us/tests/policy/contrib/taxsim/taxsim_v10.yaml index ff8421fa8a0..3c1a9bab57f 100644 --- a/policyengine_us/tests/policy/contrib/taxsim/taxsim_v10.yaml +++ b/policyengine_us/tests/policy/contrib/taxsim/taxsim_v10.yaml @@ -1307,7 +1307,10 @@ taxsim_childcare: 0 taxsim_scorp: 0 output: - taxsim_v10: 252_466.67 + # Joint wages 92,500 + 160,002. The 500 of Schedule C income is + # dependent person_3's (TAXSIM's psemp and ssemp are 0), so half of + # its self-employment tax is deducted on person_3's own return. + taxsim_v10: 252_502 - name: Tax unit 17 (CPS ID 1325801) matches TAXSIM35 outputs diff --git a/policyengine_us/tools/general.py b/policyengine_us/tools/general.py index 4bf2cf005d8..afd752d19f7 100644 --- a/policyengine_us/tools/general.py +++ b/policyengine_us/tools/general.py @@ -16,27 +16,66 @@ def tax_unit_non_dep_sum(var, tax_unit, period): ) -def tax_unit_non_dep_add(tax_unit, period, variables): +def tax_unit_non_dep_add(tax_unit, period, variables, include_dependents=()): """ Add variables over a tax unit's head and spouse, leaving out dependents. Like `add`, but a person-level variable is summed only over members who are not tax unit dependents, whose items belong on their own returns. A tax-unit-level variable is added as is, so it must already describe the - filer's own return. + filer's own return. Person-level variables named in `include_dependents` + are summed over every member: they are the filer's amounts even when + recorded on a dependent. Variables are added in the order given. """ total = 0 for variable in variables: variable_entity = tax_unit.entity.get_variable( variable, check_existence=True ).entity - if variable_entity.is_person: - total = total + tax_unit_non_dep_sum(variable, tax_unit, period) - else: + if not variable_entity.is_person: total = total + tax_unit(variable, period) + elif variable in include_dependents: + total = total + tax_unit.sum(tax_unit.members(variable, period)) + else: + total = total + tax_unit_non_dep_sum(variable, tax_unit, period) return total +def person_share_of_tax_unit_amount(person, period, tax_unit_variable, person_variable): + """ + Attribute a tax unit's amount to its members. + + Dependents keep their own person-level amounts, which a tax-unit amount + describing the filer's return leaves out. The head and spouse get their + own amounts when those sum to the tax unit's amount. When the tax-unit + amount differs, as when it is an input or a reform changes its formula, + the head's and spouse's own amounts are scaled to sum to it; if they have + no own amounts, the head takes it. With non-negative own amounts and a + non-negative tax-unit amount, as for deductions, no share is negative. + Every tax unit is assumed to have a head. + """ + # The person-level projector returns tax-unit values for each member. + tax_unit = person.tax_unit + own = person(person_variable, period) + filer = ~person("is_tax_unit_dependent", period) + filers_own = tax_unit.sum(own * filer) + amount = tax_unit(tax_unit_variable, period) + # The tax-unit amount is stored in float32, so compare in its precision. + matches = amount == filers_own.astype(amount.dtype) + has_own = filers_own > 0 + scale = np.divide( + amount, + filers_own, + out=np.zeros_like(filers_own, dtype=float), + where=has_own, + ) + reconciled = own * scale + person("is_tax_unit_head", period) * where( + has_own, 0, amount + ) + filer_share = where(matches, own, reconciled) + return where(filer, filer_share, own) + + def sum_contained_tax_units(var, population, period): tax_unit = population.members.tax_unit.reference_entity values = tax_unit(var, period) diff --git a/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_adjusted_gross_income_person.py b/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_adjusted_gross_income_person.py index 4b83de2ba8b..b1176492612 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_adjusted_gross_income_person.py +++ b/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_adjusted_gross_income_person.py @@ -1,17 +1,13 @@ from policyengine_us.model_api import * -PERSON_LEVEL_MEDICAID_AGI_ALDS = [ - "self_employment_tax_ald_person", - "self_employed_health_insurance_ald_person", - "self_employed_pension_contribution_ald_person", -] - -TAX_UNIT_AGI_ALDS_WITH_PERSON_LEVEL_EQUIVALENTS = ( - "self_employment_tax_ald", - "self_employed_health_insurance_ald", - "self_employed_pension_contribution_ald", -) +# Above-the-line deductions the model attributes to the person who has them. +TAX_UNIT_AGI_ALDS_WITH_PERSON_LEVEL_EQUIVALENTS = { + "self_employment_tax_ald": "self_employment_tax_ald_person", + "self_employed_health_insurance_ald": "self_employed_health_insurance_ald_person", + "self_employed_pension_contribution_ald": "self_employed_pension_contribution_ald_person", + "alimony_expense_ald": "alimony_expense_ald_person", +} class medicaid_adjusted_gross_income_person(Variable): @@ -24,14 +20,46 @@ class medicaid_adjusted_gross_income_person(Variable): def formula(person, period, parameters): gross_income = person("medicaid_irs_gross_income", period) - ald_sum_person = add(person, period, PERSON_LEVEL_MEDICAID_AGI_ALDS) - all_alds = parameters(period).gov.irs.ald.deductions + p = parameters(period).gov.irs.ald + all_alds = p.deductions + # Each listed deduction with a person-level equivalent goes to the + # person who has it, reconciled to the tax-unit amount when that is an + # input or a reform changes it. + ald_sum_person = 0 + for ald in all_alds: + if ald in TAX_UNIT_AGI_ALDS_WITH_PERSON_LEVEL_EQUIVALENTS: + ald_sum_person = ald_sum_person + person_share_of_tax_unit_amount( + person, + period, + ald, + TAX_UNIT_AGI_ALDS_WITH_PERSON_LEVEL_EQUIVALENTS[ald], + ) other_alds = [ ald for ald in all_alds if ald not in TAX_UNIT_AGI_ALDS_WITH_PERSON_LEVEL_EQUIVALENTS ] - ald_sum_taxunit = add(person.tax_unit, period, other_alds) + # The head's and spouse's deductions are shared between them, as on + # their return. A tax unit dependent's own person-level deductions, + # such as their IRA deduction, reduce the dependent's own AGI, since + # the dependent's income is figured on the dependent's own return. + # Amounts that are the filer's even when recorded on a dependent stay + # with the head and spouse. + filer_amounts = p.filer_amounts_recorded_on_dependents + ald_sum_taxunit = tax_unit_non_dep_add( + person.tax_unit, + period, + other_alds, + include_dependents=filer_amounts, + ) + person_level_other_alds = [ + ald + for ald in other_alds + if person.entity.get_variable(ald, check_existence=True).entity.is_person + and ald not in filer_amounts + ] + is_dependent = person("is_tax_unit_dependent", period) + dependent_own_ald = is_dependent * add(person, period, person_level_other_alds) filing_status = person.tax_unit("filing_status", period) frac = where( filing_status == filing_status.possible_values.JOINT, @@ -40,7 +68,7 @@ def formula(person, period, parameters): ) head_or_spouse = person("is_tax_unit_head_or_spouse", period) shared_ald = head_or_spouse * ald_sum_taxunit * frac - agi = gross_income - ald_sum_person - shared_ald + agi = gross_income - ald_sum_person - dependent_own_ald - shared_ald if parameters(period).gov.contrib.ubi_center.basic_income.taxable: basic_income = person.tax_unit("basic_income", period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/above_the_line_deductions.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/above_the_line_deductions.py index e7b18296389..0b6cb864bbb 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/above_the_line_deductions.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/above_the_line_deductions.py @@ -7,9 +7,24 @@ class above_the_line_deductions(Variable): label = "Above-the-line deductions" unit = USD documentation = ( - "Deductions applied to reach adjusted gross income from gross income." + "Deductions applied to reach adjusted gross income from gross income " + "(Schedule 1, line 26). A tax unit dependent's own deductions, such as " + "the deductible part of their self-employment tax, their IRA " + "deduction or their penalty on early withdrawal of savings, are on " + "the dependent's own return, as their income is." ) definition_period = YEAR reference = "https://www.law.cornell.edu/uscode/text/26/62" - adds = "gov.irs.ald.deductions" + def formula(tax_unit, period, parameters): + p = parameters(period).gov.irs.ald + # Person-level deductions are summed over the head and spouse only, + # as irs_gross_income sums their income only, except amounts that are + # the filer's even when recorded on a dependent. Tax-unit-level + # deductions already describe this return. + return tax_unit_non_dep_add( + tax_unit, + period, + p.deductions, + include_dependents=p.filer_amounts_recorded_on_dependents, + ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/alimony_expense_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/alimony_expense_ald.py index 5fede15f1a6..ae7ecd83085 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/alimony_expense_ald.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/alimony_expense_ald.py @@ -11,9 +11,5 @@ class alimony_expense_ald(Variable): reference = "https://www.irs.gov/taxtopics/tc452" def formula(tax_unit, period, parameters): - person = tax_unit.members - divorce_year = person("divorce_year", period) - alimony_expense = person("alimony_expense", period) - p = parameters(period).gov.irs.ald.alimony_expense - eligible_person = p.divorce_year_threshold.calc(divorce_year) - return tax_unit.sum(alimony_expense * eligible_person) + # A tax unit dependent who pays alimony deducts it on their own return. + return tax_unit_non_dep_sum("alimony_expense_ald_person", tax_unit, period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/alimony_expense_ald_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/alimony_expense_ald_person.py new file mode 100644 index 00000000000..b454cbceff0 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/alimony_expense_ald_person.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class alimony_expense_ald_person(Variable): + value_type = float + entity = Person + label = "Alimony expense ALD for each person" + unit = USD + documentation = ( + "Each person's own above-the-line deduction for alimony paid under a " + "divorce or separation instrument executed before 2019 (Schedule 1, " + "line 19a), on that person's own return." + ) + definition_period = YEAR + reference = "https://www.irs.gov/taxtopics/tc452" + + def formula(person, period, parameters): + divorce_year = person("divorce_year", period) + alimony_expense = person("alimony_expense", period) + p = parameters(period).gov.irs.ald.alimony_expense + return alimony_expense * p.divorce_year_threshold.calc(divorce_year) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/dependents_self_employed_health_insurance_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/dependents_self_employed_health_insurance_ald.py new file mode 100644 index 00000000000..92e33dcc21b --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/dependents_self_employed_health_insurance_ald.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class dependents_self_employed_health_insurance_ald(Variable): + value_type = float + entity = TaxUnit + label = "Tax unit dependents' self-employed health insurance deductions" + unit = USD + documentation = ( + "The self-employed health insurance deductions (Schedule 1, line 17) of the tax " + "unit's dependents, which belong on the dependents' own returns and " + "are left out of self_employed_health_insurance_ald. For state " + "income definitions that count dependents' income." + ) + definition_period = YEAR + + def formula(tax_unit, period, parameters): + person = tax_unit.members + is_dependent = person("is_tax_unit_dependent", period) + amount = person("self_employed_health_insurance_ald_person", period) + return tax_unit.sum(is_dependent * amount) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/dependents_self_employed_pension_contribution_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/dependents_self_employed_pension_contribution_ald.py new file mode 100644 index 00000000000..8ad672c26ca --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/dependents_self_employed_pension_contribution_ald.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class dependents_self_employed_pension_contribution_ald(Variable): + value_type = float + entity = TaxUnit + label = ( + "Tax unit dependents' self-employed SEP, SIMPLE and qualified plan deductions" + ) + unit = USD + documentation = ( + "The self-employed SEP, SIMPLE and qualified plan deductions (Schedule 1, line 16) of the tax " + "unit's dependents, which belong on the dependents' own returns and " + "are left out of self_employed_pension_contribution_ald. For state " + "income definitions that count dependents' income." + ) + definition_period = YEAR + + def formula(tax_unit, period, parameters): + person = tax_unit.members + is_dependent = person("is_tax_unit_dependent", period) + amount = person("self_employed_pension_contribution_ald_person", period) + return tax_unit.sum(is_dependent * amount) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.py index af773521b85..67cbe52df57 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald.py @@ -7,9 +7,15 @@ class self_employed_health_insurance_ald(Variable): label = "Self-employed health insurance ALD" unit = USD documentation = ( - "Above-the-line deduction for self-employed health insurance contributions." + "Above-the-line deduction for the head's and spouse's self-employed " + "health insurance (Schedule 1, line 17). A tax unit dependent who is " + "self-employed takes this deduction, limited to their own earned " + "income, on their own return." ) definition_period = YEAR reference = "https://www.law.cornell.edu/uscode/text/26/162#l" - adds = ["self_employed_health_insurance_ald_person"] + def formula(tax_unit, period, parameters): + return tax_unit_non_dep_sum( + "self_employed_health_insurance_ald_person", tax_unit, period + ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald.py index 5ac5dcf9496..9281488390d 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald.py @@ -7,9 +7,18 @@ class self_employed_pension_contribution_ald(Variable): label = "Self-employed pension contribution ALD" unit = USD documentation = ( - "Above-the-line deduction for self-employed pension plan contributions." + "Above-the-line deduction for the head's and spouse's self-employed " + "SEP, SIMPLE and qualified plan contributions (Schedule 1, line 16). " + "A tax unit dependent who is self-employed takes this deduction on " + "their own return." ) definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/162#l" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/62#a_6", + "https://www.law.cornell.edu/uscode/text/26/404", + ) - adds = ["self_employed_pension_contribution_ald_person"] + def formula(tax_unit, period, parameters): + return tax_unit_non_dep_sum( + "self_employed_pension_contribution_ald_person", tax_unit, period + ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py index e027dd1a7c2..33b247938f7 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py @@ -8,7 +8,10 @@ class self_employed_pension_contribution_ald_person(Variable): unit = USD documentation = "Personal above-the-line deduction for self-employed pension plan contributions." definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/162#l" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/62#a_6", + "https://www.law.cornell.edu/uscode/text/26/404", + ) def formula(person, period, parameters): earnings = max_(0, person("total_self_employment_income", period)) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_tax_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_tax_ald.py index 9049293d1f6..d0515850b92 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_tax_ald.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_tax_ald.py @@ -6,8 +6,19 @@ class self_employment_tax_ald(Variable): entity = TaxUnit label = "Self-employment tax ALD deduction" unit = USD - documentation = "Above-the-line deduction for self-employment tax" + documentation = ( + "Above-the-line deduction for the deductible part of the head's and " + "spouse's self-employment tax (Schedule 1, line 15). A tax unit " + "dependent figures their own self-employment tax on their own " + "Schedule SE and deducts its deductible part on their own return." + ) definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/164#f" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/164#f", + "https://www.irs.gov/pub/irs-prior/f1040sse--2025.pdf", + ) - adds = ["self_employment_tax_ald_person"] + def formula(tax_unit, period, parameters): + # A dependent's self-employment income is left off this return by + # irs_gross_income, so the deduction for their tax is left off too. + return tax_unit_non_dep_sum("self_employment_tax_ald_person", tax_unit, period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_magi.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_magi.py index d4d6df1142e..b29ddf98a29 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_magi.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/student_loan_interest/student_loan_interest_ald_magi.py @@ -35,7 +35,12 @@ def formula(person, period, parameters): - set(person_alds) - set(p_irs.ald.student_loan_interest.magi.excluded_alds) ) - ald_sum_taxunit = add(person.tax_unit, period, other_alds) + ald_sum_taxunit = tax_unit_non_dep_add( + person.tax_unit, + period, + other_alds, + include_dependents=p_irs.ald.filer_amounts_recorded_on_dependents, + ) filing_status = person.tax_unit("filing_status", period) joint = filing_status == filing_status.possible_values.JOINT frac = where(joint, 0.5, 1.0) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/adjusted_gross_income_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/adjusted_gross_income_person.py index 1ca12f01e0a..710424f0fdd 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/adjusted_gross_income_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/adjusted_gross_income_person.py @@ -11,6 +11,9 @@ class adjusted_gross_income_person(Variable): def formula(person, period, parameters): gross_income = person("irs_gross_income", period) + # A tax unit dependent's deductions are on their own return, as + # irs_gross_income leaves their income off this one. + not_dependent = ~person("is_tax_unit_dependent", period) # calculate ald sums by person PERSON_ALDS = [ "self_employment_tax_ald", @@ -18,11 +21,17 @@ def formula(person, period, parameters): "self_employed_pension_contribution_ald", ] person_ald_vars = [f"{ald}_person" for ald in PERSON_ALDS] - ald_sum_person = add(person, period, person_ald_vars) + ald_sum_person = not_dependent * add(person, period, person_ald_vars) # split other alds evenly between head and spouse - all_alds = parameters(period).gov.irs.ald.deductions + p = parameters(period).gov.irs.ald + all_alds = p.deductions other_alds = sorted(set(all_alds) - set(PERSON_ALDS)) - ald_sum_taxunit = add(person.tax_unit, period, other_alds) + ald_sum_taxunit = tax_unit_non_dep_add( + person.tax_unit, + period, + other_alds, + include_dependents=p.filer_amounts_recorded_on_dependents, + ) is_head = person("is_tax_unit_head", period) is_spouse = person("is_tax_unit_spouse", period) fstatus = person.tax_unit("filing_status", period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/dependent_taxable_ss_magi.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/dependent_taxable_ss_magi.py index 932e2011bb4..5d750d90555 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/dependent_taxable_ss_magi.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/dependent_taxable_ss_magi.py @@ -22,6 +22,7 @@ "educator_expense": "educator_expense", "early_withdrawal_penalty": "early_withdrawal_penalty", "traditional_ira_contributions": "traditional_ira_contributions", + "alimony_expense_ald": "alimony_expense_ald_person", } @@ -37,8 +38,8 @@ class dependent_taxable_ss_magi(Variable): 221, 911, 931 and 933, plus tax-exempt interest. Unlike the qualifying relative gross income test, losses and above-the-line deductions reduce it. Above-the-line deductions the model records only for the tax unit - (alimony paid, health savings account contributions) are not attributed - to dependents, and the IRC 461(l) excess business loss limit is not + (health savings account contributions) are not attributed to + dependents, and the IRC 461(l) excess business loss limit is not applied. """ definition_period = YEAR diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/taxable_ss_magi.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/taxable_ss_magi.py index 0e8a6beb1fb..1aaf3f09dec 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/taxable_ss_magi.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/taxable_ss_magi.py @@ -42,5 +42,11 @@ def formula(tax_unit, period, parameters): for deduction in above_the_line_deductions if deduction not in revoked_deductions ] - total_deductions = add(tax_unit, period, deductions) + # A tax unit dependent's deductions are on their own return. + total_deductions = tax_unit_non_dep_add( + tax_unit, + period, + deductions, + include_dependents=irs.ald.filer_amounts_recorded_on_dependents, + ) return max_(0, gross_income - total_deductions) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/unemployment_insurance/taxable_uc_agi.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/unemployment_insurance/taxable_uc_agi.py index cf332450c08..20045f03cf3 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/unemployment_insurance/taxable_uc_agi.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/unemployment_insurance/taxable_uc_agi.py @@ -26,5 +26,11 @@ def formula(tax_unit, period, parameters): gross_income += not_dependent * max_(0, add(person, period, [source])) gross_income = tax_unit.sum(gross_income) above_the_line_deductions = irs.ald.deductions - total_deductions = add(tax_unit, period, above_the_line_deductions) + # A tax unit dependent's deductions are on their own return. + total_deductions = tax_unit_non_dep_add( + tax_unit, + period, + above_the_line_deductions, + include_dependents=irs.ald.filer_amounts_recorded_on_dependents, + ) return max_(0, gross_income - total_deductions) diff --git a/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.py b/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.py index fa66c70a8e6..1ab04f513ba 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_b_agi.py @@ -20,7 +20,14 @@ def formula(tax_unit, period, parameters): for deduction in federal_deductions if deduction not in disallowed_deductions ] - deduction_value = add(tax_unit, period, deductions) + # Part B gross income starts from irs_gross_income, which leaves out + # tax unit dependents, so their deductions are left out too. + deduction_value = tax_unit_non_dep_add( + tax_unit, + period, + deductions, + include_dependents=p.irs.ald.filer_amounts_recorded_on_dependents, + ) # U.S. government bond interest is exempt from MA tax. us_govt_interest = add(tax_unit, period, ["us_govt_interest"]) return max_( diff --git a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_gross_income_loss_adjustment.py b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_gross_income_loss_adjustment.py index 720c6be2ced..c09ea512110 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_gross_income_loss_adjustment.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_gross_income_loss_adjustment.py @@ -16,14 +16,20 @@ def formula(tax_unit, period, parameters): # zero, so this variable captures the losses that were dropped. # Line 10 instruction: "Be sure to subtract any losses # in lines 6 or 7." + # irs_gross_income leaves out tax unit dependents, whose items are + # on their own returns, so their losses are left out here too. # Line 6a: Business/profession loss (Schedule C) - se_income = add(tax_unit, period, ["total_self_employment_income"]) + se_income = tax_unit_non_dep_add( + tax_unit, period, ["total_self_employment_income"] + ) # Line 6b: Farm loss (Schedule F) - farm = add(tax_unit, period, ["farm_operations_income"]) + farm = tax_unit_non_dep_add(tax_unit, period, ["farm_operations_income"]) # Line 7: Rental, partnership, S-corp, farm rent losses - rental = add(tax_unit, period, ["rental_income"]) - partnership = add(tax_unit, period, ["partnership_s_corp_income"]) - farm_rent = add(tax_unit, period, ["farm_rent_income"]) + rental = tax_unit_non_dep_add(tax_unit, period, ["rental_income"]) + partnership = tax_unit_non_dep_add( + tax_unit, period, ["partnership_s_corp_income"] + ) + farm_rent = tax_unit_non_dep_add(tax_unit, period, ["farm_rent_income"]) return ( min_(se_income, 0) + min_(farm, 0) diff --git a/policyengine_us/variables/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.py b/policyengine_us/variables/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.py index 529863d2ed5..ef104baa938 100644 --- a/policyengine_us/variables/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.py +++ b/policyengine_us/variables/gov/states/mn/tax/property/homestead_credit_refund/mn_homestead_credit_refund_household_income.py @@ -41,7 +41,10 @@ def formula(tax_unit, period, parameters): max_(0, social_security - taxable_social_security) * ~is_dependent ) - retirement_contributions = add( + # Line 1 is the filer's federal AGI, which leaves out tax unit + # dependents, so the retirement add-backs, contributions and + # compensation count the head and spouse only. + retirement_contributions = tax_unit_non_dep_add( tax_unit, period, [ @@ -54,7 +57,7 @@ def formula(tax_unit, period, parameters): "self_employed_pension_contributions", ], ) - retirement_additions = add( + retirement_additions = tax_unit_non_dep_add( tax_unit, period, [ @@ -64,7 +67,7 @@ def formula(tax_unit, period, parameters): "self_employed_pension_contributions", ], ) - compensation = add( + compensation = tax_unit_non_dep_add( tax_unit, period, ["employment_income", "self_employment_income"] ) retirement_subtraction = min_( 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 12ee4121a06..2c10fab998c 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 @@ -23,8 +23,12 @@ def formula(person, period, parameters): "self_employed_pension_contribution_ald_person", ] tax_unit = person.tax_unit - ind_total_personal_alds = add(person, period, PERSONAL_ALDS) - unit_total_personal_alds = add(tax_unit, period, PERSONAL_ALDS) + # 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 diff --git a/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_health_insurance_adjustment.py b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_health_insurance_adjustment.py new file mode 100644 index 00000000000..b0de576e2e9 --- /dev/null +++ b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_health_insurance_adjustment.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class ms_self_employed_health_insurance_adjustment(Variable): + value_type = float + entity = Person + label = "Mississippi self-employed health insurance adjustment" + unit = USD + documentation = ( + "Each person's own self-employed health insurance deduction (Form " + "80-105 line 56). Mississippi reports each spouse's income and " + "adjustments separately, so the tax unit's deduction is not repeated " + "for every member. A tax-unit deduction that differs from the head's " + "and spouse's own amounts is shared in proportion to them." + ) + definition_period = YEAR + reference = "https://www.dor.ms.gov/sites/default/files/tax-forms/individual/80100251%202.pdf#page=12" + defined_for = StateCode.MS + + def formula(person, period, parameters): + return person_share_of_tax_unit_amount( + person, + period, + "self_employed_health_insurance_ald", + "self_employed_health_insurance_ald_person", + ) diff --git a/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_retirement_adjustment.py b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_retirement_adjustment.py new file mode 100644 index 00000000000..77ac9425c84 --- /dev/null +++ b/policyengine_us/variables/gov/states/ms/tax/income/adjustments/ms_self_employed_retirement_adjustment.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class ms_self_employed_retirement_adjustment(Variable): + value_type = float + entity = Person + label = "Mississippi self-employed retirement plan adjustment" + unit = USD + documentation = ( + "Each person's own self-employed SEP, SIMPLE and qualified plan " + "deduction (Form 80-105 line 51). Mississippi reports each spouse's " + "income and adjustments separately, so the tax unit's deduction is not " + "repeated for every member. A tax-unit deduction that differs from the " + "head's and spouse's own amounts is shared in proportion to them." + ) + definition_period = YEAR + reference = "https://www.dor.ms.gov/sites/default/files/tax-forms/individual/80100251%202.pdf#page=12" + defined_for = StateCode.MS + + def formula(person, period, parameters): + return person_share_of_tax_unit_amount( + person, + period, + "self_employed_pension_contribution_ald", + "self_employed_pension_contribution_ald_person", + ) diff --git a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py index cbd9e8c4fd8..03bbb514baf 100644 --- a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py +++ b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py @@ -11,8 +11,15 @@ class nj_medical_expense_deduction(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.nj.tax.income.deductions.medical_expenses - self_employed_medical_expense_deduction = tax_unit( - "self_employed_health_insurance_ald", period + # nj_agi counts every member's income, dependents included, so the + # dependents' self-employed health insurance deductions count too. + self_employed_medical_expense_deduction = add( + tax_unit, + period, + [ + "self_employed_health_insurance_ald", + "dependents_self_employed_health_insurance_ald", + ], ) medical_expenses = tax_unit("itemized_medical_expenses", period) agi = tax_unit("nj_agi", period)