diff --git a/changelog.d/eitc-dependent-investment-income.fixed.md b/changelog.d/eitc-dependent-investment-income.fixed.md new file mode 100644 index 00000000000..a8ec1ddc496 --- /dev/null +++ b/changelog.d/eitc-dependent-investment-income.fixed.md @@ -0,0 +1 @@ +The EITC investment income test (26 U.S.C. 32(i)) now counts only the head's and spouse's interest, dividends, capital gains, rents and passive income. A tax unit dependent's income no longer denies the credit, and a dependent's losses no longer offset the filer's income. A negative capital gain distributions input is floored at zero for each filer, as in adjusted gross income, instead of offsetting other capital gains. The test now reads the head's and spouse's long_term_capital_gains and short_term_capital_gains, as the net investment income tax does; a net_capital_gains amount supplied for the tax unit is no longer read. diff --git a/policyengine_us/tests/core/test_dependent_eitc_investment_income_invariants.py b/policyengine_us/tests/core/test_dependent_eitc_investment_income_invariants.py new file mode 100644 index 00000000000..2e1d60925d7 --- /dev/null +++ b/policyengine_us/tests/core/test_dependent_eitc_investment_income_invariants.py @@ -0,0 +1,332 @@ +"""Invariants for dependents' income in the filer's EITC investment income test. + +26 USC 32(i)(1) denies the credit when the "disqualified income of the +taxpayer" exceeds the limit. A tax unit dependent's interest, dividends, +capital gains, rents and passive income are on the dependent's own return, and +Publication 596 Worksheet 1 picks up a child's interest and dividends only +through a Form 8814 election, which the model does not implement. So +`eitc_relevant_investment_income` counts the head and spouse only, as +`irs_gross_income` does for AGI. + +Hypothesis draws batches of tax units (single, head of household with +dependents, joint with and without dependents), and a seeded population of 200 +such units adds breadth. Dependents' capital gains are sometimes $16 million +or more, where float32 rounding of a tax unit total would show. Each batch runs +as one vectorized simulation three times: as drawn; with the dependents' +investment income inputs set to zero; and with $1,000 more of the head's +interest. For every tax unit: + +1. The dependents' investment income, of any size or sign, never changes the + filer's `eitc_relevant_investment_income`, `eitc_investment_income_eligible`, + `eitc`, `filer_loss_limited_net_capital_gains` or AGI. +2. Differential: `eitc_relevant_investment_income` equals an independent numpy + computation of Worksheet 1 over the head and spouse: interest, tax-exempt + interest and dividends, plus their Schedule D gain with capital gain + distributions floored at zero (line 7), plus their net rental and passive + income floored at zero (line 13). +3. Bounds: it is at least the filers' interest and dividends (drawn + nonnegative), so never negative, and $1,000 more of the head's interest + raises it by exactly $1,000. +4. For tax units without dependents and without a negative distributions + input, it equals the previous all-member formula, so leaving dependents out + changes nothing for them. (With filer gains in the drawn range. Past about + $16.8 million the previous formula's float32 tax unit total could be off by + a few dollars, which the direct sum corrects.) + +The formula reads the head's and spouse's person-level gains, never the tax +unit's `net_capital_gains`; a YAML case pins that a supplied tax unit amount is +not read. +""" + +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 + +PORTFOLIO_INPUTS = [ + "taxable_interest_income", + "tax_exempt_interest_income", + "qualified_dividend_income", + "non_qualified_dividend_income", +] +CAPITAL_INPUTS = ["long_term_capital_gains", "short_term_capital_gains"] +DISTRIBUTIONS = "non_sch_d_capital_gains" +# passive_partnership_s_corp_income is the passive subset of +# partnership_s_corp_income; both are drawn together below. +PASSIVE_INPUTS = ["rental_income", "passive_partnership_s_corp_income"] +# The filer's amounts, which a dependent's income must not change. +FILER_OUTPUTS = [ + "eitc_relevant_investment_income", + "eitc_investment_income_eligible", + "eitc", + "filer_loss_limited_net_capital_gains", + "adjusted_gross_income", +] +# Sums every member, for the previous formula in property 4. +OUTPUTS = FILER_OUTPUTS + ["net_capital_gains"] +# More of the head's interest, in the third run. +INTEREST_SHIFT = 1_000.0 + +nonnegative = st.one_of(st.just(0.0), st.integers(1, 8_000).map(float)) +signed = st.one_of(st.just(0.0), st.integers(-12_000, 12_000).map(float)) +# Up to $64 million either way, in multiples of 16 so float32 holds them +# exactly; a tax unit total above about $16.8 million rounds in float32. +very_large = st.integers(-4_000_000, 4_000_000).map(lambda x: 16.0 * x) + + +@st.composite +def investment_amounts(draw, dependent=False): + passive = draw(signed) + capital = st.one_of(signed, very_large) if dependent else signed + return { + **{name: draw(nonnegative) for name in PORTFOLIO_INPUTS}, + **{name: draw(capital) for name in CAPITAL_INPUTS}, + # Form 1099-DIV box 2a amounts are not negative, but the input can be; + # the model floors each person's amount at zero. + DISTRIBUTIONS: draw(st.one_of(nonnegative, st.integers(-3_000, -1).map(float))), + "rental_income": draw(signed), + "partnership_s_corp_income": passive, + "passive_partnership_s_corp_income": passive, + } + + +@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 { + # Wages where the credit is available, so eligibility moves eitc. + "head_wages": float(draw(st.integers(0, 40_000))), + "head": draw(investment_amounts()), + "spouse": draw(investment_amounts()) if kind.startswith("joint") else None, + "dependents": [ + { + "age": draw(st.integers(1, 17)), + **draw(investment_amounts(dependent=True)), + } + for _ in range(n_dependents) + ], + } + + +SEED = 20261006 + + +def _seeded_amounts(rng, dependent=False): + def some(low, high, p=0.5): + return float(round(rng.uniform(low, high))) if rng.random() < p else 0.0 + + def capital(): + if dependent and rng.random() < 0.2: + return 16.0 * int(rng.integers(1_000_000, 4_000_000)) + return some(-12_000, 12_000) + + passive = some(-12_000, 12_000) + return { + **{name: some(1, 8_000) for name in PORTFOLIO_INPUTS}, + **{name: capital() for name in CAPITAL_INPUTS}, + DISTRIBUTIONS: some(-3_000, 8_000), + "rental_income": some(-12_000, 12_000), + "partnership_s_corp_income": passive, + "passive_partnership_s_corp_income": passive, + } + + +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( + { + "head_wages": float(round(rng.uniform(0, 40_000))), + "head": _seeded_amounts(rng), + "spouse": (_seeded_amounts(rng) if kind.startswith("joint") else None), + "dependents": [ + { + "age": int(rng.integers(1, 18)), + **_seeded_amounts(rng, dependent=True), + } + for _ in range(n_dependents) + ], + } + ) + return units + + +def _situation(units, year, *, zero_dependents=False, interest_shift=0.0): + people, groups = {}, {"tax_units": {}, "households": {}, "marital_units": {}} + for i, u in enumerate(units): + head = f"head_{i}" + people[head] = { + "age": 35, + "is_tax_unit_head": True, + "is_tax_unit_dependent": False, + "employment_income": u["head_wages"], + **u["head"], + "taxable_interest_income": ( + u["head"]["taxable_interest_income"] + interest_shift + ), + } + members, couple = [head], [head] + if u["spouse"] is not None: + spouse = f"spouse_{i}" + people[spouse] = { + "age": 33, + "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 = {name: 0.0 for name in amounts} + people[child] = { + "age": d["age"], + "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: "TX"}, + } + 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, **kwargs): + sim = Simulation(situation=_situation(units, year, **kwargs)) + out = {name: np.asarray(sim.calculate(name, year), dtype=float) for name in OUTPUTS} + out["is_dependent"] = np.asarray( + sim.calculate("is_tax_unit_dependent", year), dtype=bool + ) + out["unit"] = sim.populations["tax_unit"].members_entity_id + eitc = sim.tax_benefit_system.parameters(f"{year}-01-01").gov.irs.credits.eitc + out["limit"] = eitc.phase_out.max_investment_income + return out + + +def _by_person(units, key): + """Person-level input values (or the dependent flag) in member order.""" + rows = [] + for u in units: + rows.append((u["head"], False)) + if u["spouse"] is not None: + rows.append((u["spouse"], False)) + rows += [(amounts, True) for amounts in u["dependents"]] + if key == "is_dependent": + return np.array([dependent for _, dependent in rows]) + return np.array([amounts[key] for amounts, _ in rows]) + + +def _unit_sum(run, values): + return np.bincount( + run["unit"], + weights=values, + minlength=len(run["eitc_relevant_investment_income"]), + ) + + +def _check(units, year): + run = _run(units, year) + zeroed = _run(units, year, zero_dependents=True) + shifted = _run(units, year, interest_shift=INTEREST_SHIFT) + filer = ~_by_person(units, "is_dependent") + assert np.array_equal(~run["is_dependent"], filer) + has_dependent = _unit_sum(run, (~filer).astype(float)) > 0 + + # 1. The dependents' investment income never reaches the filer's test. + for name in FILER_OUTPUTS: + np.testing.assert_allclose( + run[name], zeroed[name], atol=TOLERANCE, err_msg=name + ) + + # 2. Differential: Worksheet 1 over the head and spouse, from the inputs. + def filer_sum(names): + return _unit_sum(run, filer * sum(_by_person(units, n) for n in names)) + + portfolio = filer_sum(PORTFOLIO_INPUTS) + distributions = _unit_sum( + run, filer * np.maximum(0, _by_person(units, DISTRIBUTIONS)) + ) + capital = np.maximum(0, filer_sum(CAPITAL_INPUTS) + distributions) + passive = np.maximum(0, filer_sum(PASSIVE_INPUTS)) + expected = portfolio + capital + passive + investment_income = run["eitc_relevant_investment_income"] + np.testing.assert_allclose(investment_income, expected, atol=TOLERANCE) + np.testing.assert_array_equal( + run["eitc_investment_income_eligible"].astype(bool), + expected <= run["limit"], + ) + assert not (run["eitc"][expected > run["limit"]] > 0).any() + + # 3. Bounds and an exact shift in the filer's interest. + assert (investment_income >= portfolio - TOLERANCE).all() + assert (investment_income >= -TOLERANCE).all() + np.testing.assert_allclose( + shifted["eitc_relevant_investment_income"] - investment_income, + INTEREST_SHIFT, + atol=TOLERANCE, + ) + + # 4. No dependents and no negative distributions input: the previous + # all-member formula, unchanged. + no_negative_distributions = ( + _unit_sum(run, (_by_person(units, DISTRIBUTIONS) < 0).astype(float)) == 0 + ) + unchanged = ~has_dependent & no_negative_distributions + all_member = ( + _unit_sum(run, sum(_by_person(units, n) for n in PORTFOLIO_INPUTS)) + + np.maximum( + 0, + run["net_capital_gains"] + _unit_sum(run, _by_person(units, DISTRIBUTIONS)), + ) + + np.maximum( + 0, _unit_sum(run, sum(_by_person(units, n) for n in PASSIVE_INPUTS)) + ) + ) + np.testing.assert_allclose( + investment_income[unchanged], all_member[unchanged], atol=TOLERANCE + ) + return has_dependent, unchanged + + +# 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_investment_income_stays_off_the_filers_eitc_test(units): + _check(units, 2025) + + +@pytest.mark.parametrize("year", [2021, 2025]) +def test_seeded_population(year): + has_dependent, unchanged = _check(_seeded_units(), year) + # The population exercises both branches of each property. + assert has_dependent.sum() > 50 + assert unchanged.sum() > 20 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_relevant_investment_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_relevant_investment_income.yaml new file mode 100644 index 00000000000..5499593bd30 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_relevant_investment_income.yaml @@ -0,0 +1,356 @@ +# A tax unit dependent's investment income is on the dependent's own return, +# not the filer's. 26 USC 32(i)(1) denies the credit when the "disqualified +# income of the taxpayer" exceeds the limit, and 32(i)(2)(A) counts interest +# and dividends only "to the extent includible in gross income", which a +# dependent's are not (irs_gross_income leaves them out of AGI). Publication +# 596 Worksheet 1 reads the filer's own Form 1040 and picks up a child's income +# only through a Form 8814 election (lines 2 and 4 directly, lines 3 and 5 +# through Form 1040 lines 3b and 7a), which is not modeled; from $13,500 the +# child must file their own return (2025 Form 8814, line 4). The worksheet's +# capital gain line (line 5) is the filer's own Form 1040 line 7a. So +# eitc_relevant_investment_income counts the head and spouse only, in both +# directions: a dependent's income cannot deny the credit, and a dependent's +# loss cannot offset the filer's income. +# +# Unless noted, the filer is a 30-year-old head of household in TX for 2025 +# with $20,000 of wages and one qualifying child aged 8. 2025 amounts: +# investment income limit 11,950 +# maximum credit with one child 4,328 (34% x 12,730) +# phase-out start with one child 23,350 (30,470 for joint filers) +# Each "Before" figure is the pre-change formula, which summed every member. + +- name: Control, no investment income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + child: + age: 8 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + eitc_relevant_investment_income: 0 + eitc_investment_income_eligible: true + adjusted_gross_income: 20_000 + # Earned income 20,000 is past the 12,730 earned income amount and under + # the 23,350 phase-out start. + eitc: 4_328 + +- name: A dependent's dividends stay off the filer's investment income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + child: + age: 8 + is_tax_unit_dependent: true + qualified_dividend_income: 15_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # Before: 15,000, so the credit was denied (eitc 0). + eitc_relevant_investment_income: 0 + eitc_investment_income_eligible: true + adjusted_gross_income: 20_000 + eitc: 4_328 + +- name: A dependent's long-term capital gain stays off the filer's investment income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + child: + age: 8 + is_tax_unit_dependent: true + long_term_capital_gains: 15_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # Before: 15,000, so the credit was denied (eitc 0). + eitc_relevant_investment_income: 0 + eitc_investment_income_eligible: true + adjusted_gross_income: 20_000 + eitc: 4_328 + +- name: A dependent's taxable and tax-exempt interest stay off the filer's investment income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + child: + age: 8 + is_tax_unit_dependent: true + taxable_interest_income: 12_500 + tax_exempt_interest_income: 2_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # Before: 14,500, so the credit was denied (eitc 0). + eitc_relevant_investment_income: 0 + eitc_investment_income_eligible: true + eitc: 4_328 + +- name: A dependent's capital loss cannot offset the filer's capital gain + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + taxable_interest_income: 9_000 + long_term_capital_gains: 4_000 + child: + age: 8 + is_tax_unit_dependent: true + short_term_capital_gains: -4_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # Worksheet 1: line 1 9,000 + line 7 4,000 (the head's own line 7a). + # Before: 9,000 + max(0, 4,000 - 4,000) = 9,000, so the credit was allowed. + eitc_relevant_investment_income: 13_000 + eitc_investment_income_eligible: false + adjusted_gross_income: 33_000 + eitc: 0 + +- name: A dependent's rental loss cannot offset the filer's rental income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + taxable_interest_income: 9_000 + rental_income: 4_000 + child: + age: 8 + is_tax_unit_dependent: true + rental_income: -4_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # Worksheet 1: line 1 9,000 + line 13 4,000. + # Before: 9,000 + max(0, 4,000 - 4,000) = 9,000. + eitc_relevant_investment_income: 13_000 + eitc_investment_income_eligible: false + eitc: 0 + +- name: A dependent's passive loss cannot offset the filer's passive income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + taxable_interest_income: 9_000 + partnership_s_corp_income: 4_000 + passive_partnership_s_corp_income: 4_000 + child: + age: 8 + is_tax_unit_dependent: true + partnership_s_corp_income: -4_000 + passive_partnership_s_corp_income: -4_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # Worksheet 1: line 1 9,000 + line 13 4,000. + # Before: 9,000 + max(0, 4,000 - 4,000) = 9,000. + eitc_relevant_investment_income: 13_000 + eitc_investment_income_eligible: false + eitc: 0 + +- name: A negative capital gain distributions input does not offset capital gains + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + taxable_interest_income: 7_000 + long_term_capital_gains: 5_000 + non_sch_d_capital_gains: -2_000 + child: + age: 8 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # Capital gain distributions (Form 1099-DIV box 2a) are not negative, so + # each filer's input is floored at zero, as irs_gross_income and + # filer_loss_limited_net_capital_gains floor it. Line 7a and AGI include + # the full 5,000 gain: 7,000 + 5,000 = 12,000 > 11,950. + # Before: 7,000 + max(0, 5,000 - 2,000) = 10,000, so the credit was allowed. + eitc_relevant_investment_income: 12_000 + eitc_investment_income_eligible: false + adjusted_gross_income: 32_000 + eitc: 0 + +- name: Joint filers count both spouses' income and net their capital gains, not a dependent's + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + taxable_interest_income: 3_000 + long_term_capital_gains: 6_000 + spouse: + age: 30 + qualified_dividend_income: 5_000 + short_term_capital_gains: -3_000 + child: + age: 8 + is_tax_unit_dependent: true + taxable_interest_income: 10_000 + long_term_capital_gains: 10_000 + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + couple: + members: [head, spouse] + child_unit: + members: [child] + households: + household: + members: [head, spouse, child] + state_code: TX + output: + filing_status: JOINT + # Worksheet 1: lines 1 and 3, 3,000 + 5,000; line 7, the joint Schedule D + # gain 6,000 - 3,000 = 3,000. Total 11,000 <= 11,950. + # Before: 3,000 + 5,000 + 10,000 + max(0, 6,000 - 3,000 + 10,000) = 31,000, + # so the credit was denied. + eitc_relevant_investment_income: 11_000 + eitc_investment_income_eligible: true + # 20,000 + 3,000 + 5,000 + 3,000 + adjusted_gross_income: 31_000 + # 4,328 - 15.98% x (31,000 - 30,470) = 4,328 - 84.694 + eitc: 4_243.31 + +- name: A dependent's very large gain does not move the filer across the limit from below + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + taxable_interest_income: 11_950 + long_term_capital_gains: 1 + child: + age: 8 + is_tax_unit_dependent: true + long_term_capital_gains: 20_000_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # 11,950 + 1 = 11,951 > 11,950. net_capital_gains is stored in float32, + # where 20,000,001 rounds to 20,000,000, so taking the child's gain out + # of it would give 11,950 and allow the credit. + eitc_relevant_investment_income: 11_951 + eitc_investment_income_eligible: false + +- name: A dependent's very large gain does not move the filer across the limit from above + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + employment_income: 20_000 + taxable_interest_income: 11_947 + long_term_capital_gains: 3 + child: + age: 8 + is_tax_unit_dependent: true + long_term_capital_gains: 20_000_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # 11,947 + 3 = 11,950 <= 11,950. In float32, 20,000,003 rounds to + # 20,000,004, so taking the child's gain out of it would give 11,951. + eitc_relevant_investment_income: 11_950 + eitc_investment_income_eligible: true + +- name: A net capital gains amount supplied for the tax unit is not read + period: 2025 + absolute_error_margin: 0.01 + input: + net_capital_gains: 12_000 + output: + # Capital gains are the head's and spouse's own person-level gains, as in + # irs_gross_income and the NIIT base, so the supplied tax unit total is + # not read. Reading it and taking dependents' gains out of it would let a + # dependent's large gain move the filers' amount through float32 rounding. + # Before: 12,000. + eitc_relevant_investment_income: 0 + adjusted_gross_income: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/credits/mn_child_and_working_families_credits.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/credits/mn_child_and_working_families_credits.yaml index 595b43b987e..3ac3036fd85 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/credits/mn_child_and_working_families_credits.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/credits/mn_child_and_working_families_credits.yaml @@ -655,3 +655,34 @@ eitc_earned_income: 40_000 # 4% x $9,220 + 2 x $1,750 = $3,868.80; less 12% x ($40,000 - $36,880) = $374.40 mn_child_and_working_families_credits: 3_494.40 + +- name: A dependent's interest does not remove Minnesota's child and working family credits + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 35 + employment_income: 22_000 + child: + age: 10 + is_tax_unit_dependent: true + taxable_interest_income: 50_000 + tax_units: + tax_unit: + members: [head, child] + spm_units: + spm_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: MN + output: + # The child's interest is on the child's own return; the head's + # Worksheet 1 amount is 0, under the 2025 limit of 11,950. + eitc_investment_income_eligible: true + mn_wfc_eligible: true + # CTC 1 x 1,750 + WFC 4% x 9,480 = 379.20; income 22,000 is under + # the 31,950 phase-out threshold. Main gives 0. + mn_child_and_working_families_credits: 2_129.20 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/eitc/nm_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/eitc/nm_eitc.yaml index 5496ac4e9d1..2c3965ab3d7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/eitc/nm_eitc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/tax/income/credits/eitc/nm_eitc.yaml @@ -30,3 +30,33 @@ state_code: NM output: nm_eitc: 140 + +- name: A dependent's capital gain does not remove New Mexico's working families tax credit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 35 + employment_income: 22_000 + child: + age: 10 + is_tax_unit_dependent: true + long_term_capital_gains: 50_000 + tax_units: + tax_unit: + members: [head, child] + spm_units: + spm_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: NM + output: + # Federal EITC, one child: the 2025 maximum 4,328 (earnings 22,000 + # are under the 23,350 phase-out start). NM matches 25%: 1,082. + # Main gives 0 for both. + eitc: 4_328 + nm_eitc_eligible: true + nm_eitc: 1_082 diff --git a/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py b/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py index 06ba983fd4f..b43210665ee 100644 --- a/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py +++ b/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py @@ -5,18 +5,35 @@ class eitc_relevant_investment_income(Variable): value_type = float entity = TaxUnit label = "EITC-relevant investment income" + documentation = ( + "Disqualified income of the head and spouse under 26 USC 32(i)(2), " + "figured as on Publication 596 Worksheet 1. A tax unit dependent's " + "interest, dividends, rents, passive income and capital gains are on " + "the dependent's own return, as in irs_gross_income. The worksheet " + "picks up a child's income only through a Form 8814 election: line 2 " + "adds Form 8814 line 1b, line 4 the Schedule 1 line 8z amount, and the " + "elected dividends and capital gain distributions reach lines 3 and 5 " + "through Form 1040 lines 3b and 7a. The election is not modeled, which " + "can overstate the credit for a parent who makes it. Capital gains " + "are the head's and spouse's own long_term_capital_gains and " + "short_term_capital_gains; a net_capital_gains amount supplied for " + "the tax unit, or a capital_gains amount set directly on a person, " + "is not read." + ) unit = USD definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/32#i_2", - "https://www.irs.gov/pub/irs-prior/p596--2021.pdf#page=7", + "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=7", + "https://www.irs.gov/pub/irs-prior/f8814--2025.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/i8814--2025.pdf#page=4", ) def formula(tax_unit, period, parameters): # Publication 596 Worksheet 1 keeps portfolio income, net capital # gains, and net passive income in separate baskets. A loss in one # basket cannot offset positive income in another. - portfolio_income = add( + portfolio_income = tax_unit_non_dep_add( tax_unit, period, [ @@ -25,13 +42,31 @@ def formula(tax_unit, period, parameters): "dividend_income", ], ) - capital_gains = add( - tax_unit, period, ["net_capital_gains", "non_sch_d_capital_gains"] + # Worksheet 1 line 5: Form 1040 line 7a, the head and spouse's + # Schedule D gain with capital gain distributions (line 13), or zero + # if a loss. Sum their own gains and losses directly, as the NIIT base + # (filer_loss_limited_net_capital_gains) does. Reading the tax unit's + # net_capital_gains and taking dependents' gains out of it would let a + # dependent's large gain move the filers' amount through float32 + # rounding of the total, and core does not record which tax units + # supplied that total. So a net_capital_gains amount supplied for the + # tax unit is not read. + filer_gains = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains", "short_term_capital_gains"] + ) + # Capital gain distributions (Form 1099-DIV box 2a) are not negative, + # so each filer's input is floored at zero, as in irs_gross_income. + person = tax_unit.members + distributions = tax_unit.sum( + ~person("is_tax_unit_dependent", period) + * max_(0, person("non_sch_d_capital_gains", period)) ) + capital_gains = filer_gains + distributions # The model's undifferentiated rental input is treated as passive # rental income, consistently with its NIIT income mapping. Net the - # passive amounts across the tax unit before applying the zero floor. - passive_income = add( + # passive amounts across the head and spouse before applying the zero + # floor. + passive_income = tax_unit_non_dep_add( tax_unit, period, ["rental_income", "passive_partnership_s_corp_income"] ) return portfolio_income + max_(0, capital_gains) + max_(0, passive_income)